/*
* Copyright (c) 2011 PonySDK
* Owners:
* Luciano Broussal <luciano.broussal AT gmail.com>
* Mathieu Barbier <mathieu.barbier AT gmail.com>
* Nicolas Ciaravola <nicolas.ciaravola.pro AT gmail.com>
*
* WebSite:
* http://code.google.com/p/pony-sdk/
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.ponysdk.core.server.servlet;
import java.util.concurrent.RunnableScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ponysdk.core.server.application.ApplicationManagerOption;
import com.ponysdk.core.server.application.UIContext;
public class CommunicationSanityChecker {
private static final Logger log = LoggerFactory.getLogger(CommunicationSanityChecker.class);
private static final int CHECK_PERIOD = 1000;
private static final int MAX_THREAD_CHECKER = Integer.parseInt(
System.getProperty("communication.sanity.checker.thread.count", String.valueOf(Runtime.getRuntime().availableProcessors())));
protected static final ScheduledThreadPoolExecutor sanityCheckerTimer = new ScheduledThreadPoolExecutor(MAX_THREAD_CHECKER,
new ThreadFactory() {
private int i = 0;
@Override
public Thread newThread(final Runnable r) {
final Thread t = new Thread(r);
t.setName(CommunicationSanityChecker.class.getName() + "-" + i++);
t.setDaemon(true);
return t;
}
});
private static final long HEARTBEAT_PERIOD_BLOCKED = TimeUnit.MINUTES.toMillis(10);
protected final AtomicBoolean started = new AtomicBoolean(false);
private final UIContext uiContext;
private long heartBeatPeriod;
private long lastReceivedTime;
private RunnableScheduledFuture<?> sanityChecker;
private CommunicationState currentState;
private long suspectTime = -1;
private long oldHeartBeatPeriod;
public CommunicationSanityChecker(final UIContext uiContext) {
this.uiContext = uiContext;
final ApplicationManagerOption options = uiContext.getApplication().getOptions();
setHeartBeatPeriod(options.getHeartBeatPeriod(), options.getHeartBeatPeriodTimeUnit());
enableCommunicationChecker(!uiContext.getApplication().getOptions().isDebugMode());
}
private boolean isStarted() {
return started.get();
}
public void start() {
lastReceivedTime = System.currentTimeMillis();
if (!isStarted()) {
currentState = CommunicationState.OK;
sanityChecker = (RunnableScheduledFuture<?>) sanityCheckerTimer.scheduleWithFixedDelay(() -> {
try {
checkCommunicationState();
} catch (final Throwable e) {
log.error("[{}] Error while checking communication state", uiContext, e);
}
}, 0, CHECK_PERIOD, TimeUnit.MILLISECONDS);
started.set(true);
log.info("Started. HeartbeatPeriod: {} ms, {}", uiContext, heartBeatPeriod);
}
}
public void stop() {
if (isStarted()) {
if (sanityChecker != null) {
sanityChecker.cancel(false);
sanityCheckerTimer.remove(sanityChecker);
sanityChecker = null;
}
started.set(false);
log.info("[{}] Stopped.", uiContext);
}
}
public void onMessageReceived() {
lastReceivedTime = System.currentTimeMillis();
}
public void setHeartBeatPeriod(final long heartbeat, final TimeUnit timeUnit) {
heartBeatPeriod = TimeUnit.MILLISECONDS.convert(heartbeat, timeUnit);
oldHeartBeatPeriod = heartBeatPeriod;
if (heartBeatPeriod <= 0) throw new IllegalArgumentException("'HeartBeatPeriod' parameter must be gretter than 0");
}
private boolean isCommunicationSuspectedToBeNonFunctional(final long now) {
// No message have been received or sent during the HeartbeatPeriod
return now - lastReceivedTime >= heartBeatPeriod;
}
protected void checkCommunicationState() {
final long now = System.currentTimeMillis();
switch (currentState) {
case OK:
if (isCommunicationSuspectedToBeNonFunctional(now)) {
suspectTime = now;
currentState = CommunicationState.SUSPECT;
if (log.isDebugEnabled()) log.debug(
"[{}] No message have been received, communication suspected to be non functional, sending heartbeat...",
uiContext);
//uiContext.sendHeartBeat();
}
break;
case SUSPECT:
if (lastReceivedTime < suspectTime) {
if (now - suspectTime >= heartBeatPeriod) {
// No message have been received since we suspected the
// communication to be non functional
if (log.isInfoEnabled()) log.info(
"[{}] No message have been received since we suspected the communication to be non functional, context will be destroyed",
uiContext);
currentState = CommunicationState.KO;
stop();
uiContext.destroy();
}
} else {
currentState = CommunicationState.OK;
suspectTime = -1;
}
break;
case KO:
default:
break;
}
uiContext.sendHeartBeat();
uiContext.sendRoundTrip();
}
protected enum CommunicationState {
OK,
SUSPECT,
KO
}
/**
* Don't really desactivate the communication checker, only set a long period
* ({@link #HEARTBEAT_PERIOD_BLOCKED}) for the heartbeat
*/
public void enableCommunicationChecker(final boolean enabled) {
if (enabled) {
heartBeatPeriod = oldHeartBeatPeriod;
} else {
oldHeartBeatPeriod = heartBeatPeriod;
heartBeatPeriod = HEARTBEAT_PERIOD_BLOCKED;
}
}
}