/* license-start
*
* Copyright (C) 2008 - 2013 Crispico, <http://www.crispico.com/>.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details, at <http://www.gnu.org/licenses/>.
*
* Contributors:
* Crispico - Initial API and implementation
*
* license-end
*/
package org.flowerplatform.blazeds.heartbeat;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.ConcurrentHashMap;
import org.flowerplatform.blazeds.channel.BlazedsCommunicationChannel;
import org.flowerplatform.communication.CommunicationPlugin;
import org.flowerplatform.communication.channel.CommunicationChannel;
import org.flowerplatform.communication.channel.ICommunicationChannelLifecycleListener;
import org.flowerplatform.communication.service.InvokeServiceMethodServerCommand;
import org.flowerplatform.communication.stateful_service.IStatefulClientLocalState;
import org.flowerplatform.communication.stateful_service.RegularStatefulService;
import org.flowerplatform.communication.stateful_service.RemoteInvocation;
import org.flowerplatform.communication.stateful_service.StatefulServiceInvocationContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Monitors channels to disconnect them by checking if :
* <ul>
* <li> they are still alive, by watching traveling data and heartbeat signal.
* <li> they are still active, by watching incoming traveling data (except hearbeat signal) and activity signal.
* </ul>
*
* <p>
* Service started and destroyed along with the server.
* The service subscribes and unsubscribes for each channel that is created or destroyed.
*
* @author Sorin
*
*/
public class HeartbeatStatefulService extends RegularStatefulService<BlazedsCommunicationChannel, HeartbeatDetails> implements ICommunicationChannelLifecycleListener {
/* package */ static final Logger logger = LoggerFactory.getLogger(HeartbeatStatefulService.class);
public static final String SERVICE_ID = "heartbeatStatefulService";
private static final String CLIENT_ID = "heartbeatStatefulClient";
///////////////////////////////////////////////////////////////
// JMX Methods
///////////////////////////////////////////////////////////////
/**
*
*/
@Override
protected void printStatefulDataForClient(StringBuffer stringBuffer, String linePrefix, BlazedsCommunicationChannel client, HeartbeatDetails data) {
super.printStatefulDataForClient(stringBuffer, linePrefix, client, data);
stringBuffer.append(linePrefix).append(" ").append(data.getClass().getSimpleName()).append("\n");
stringBuffer.append(linePrefix).append(" ").append(data.noHearbeatFromClientTask).append("\n");
stringBuffer.append(linePrefix).append(" ").append(data.warnAboutNoActivityTask).append("\n");
stringBuffer.append(linePrefix).append(" ").append(data.noActivityOnClientTask).append("\n ");
}
public void executeTask_WarnAboutNoActivity(String communicationChannelId) {
getDetailsFromChannelId(communicationChannelId).warnAboutNoActivityTask.timeout();
}
public void executeTask_NoActivityOnClient(String communicationChannelId) {
getDetailsFromChannelId(communicationChannelId).noActivityOnClientTask.timeout();
}
private HeartbeatDetails getDetailsFromChannelId(String communicationChannelId) {
BlazedsCommunicationChannel channel = (BlazedsCommunicationChannel) CommunicationPlugin.getInstance().getCommunicationChannelManager().getCommunicationChannelById(communicationChannelId);
if (channel == null) {
throw new IllegalArgumentException("BlazedsCommunicationChannel not found for id = " + communicationChannelId);
}
HeartbeatDetails details = clients.get(channel);
if (details == null) {
throw new IllegalArgumentException("ChannelObserverDetails not found for id = " + communicationChannelId);
}
return details;
}
@Override
public Collection<String> getStatefulClientIdsForCommunicationChannel(CommunicationChannel communicationChannel) {
return Collections.singleton(CLIENT_ID);
}
///////////////////////////////////////////////////////////////
// Normal methods
///////////////////////////////////////////////////////////////
protected String getStatefulServiceId() {
return SERVICE_ID;
}
public HeartbeatStatefulService() {
HeartbeatProperties.initializeCommunicationProperties();
clients = new ConcurrentHashMap<BlazedsCommunicationChannel, HeartbeatDetails>(); // 2 clients may arrive at the same time.
CommunicationPlugin.getInstance().getCommunicationChannelManager().addWebCommunicationLifecycleListener(this);
}
public void dispose() {
HeartbeatDetails.channelObserverTimeoutManager.shutdown();
}
/**
* All incoming objects :
* <li>
* <ul> update last traveling data time stamp
* <ul> update last client activity time stamp except for heartbeat signal
* </li>
*
*/
public void notifyObjectReceived(BlazedsCommunicationChannel channel, Object object) {
logObject(object, false);
if (object instanceof InvokeServiceMethodServerCommand) { // check if heartbeat method to process it later.
InvokeServiceMethodServerCommand command = (InvokeServiceMethodServerCommand) object;
if (command.getServiceId().equals(SERVICE_ID) && command.getMethodName().equals("signalHeartbeat"))
return; // ignore it, because it will be processed when invoking the service method.
}
HeartbeatDetails details = clients.get(channel);
details.updateTasks(true /* update traveling data */, true /* update activity */);
}
/**
* All outgoing objects :
* <li>
* <ul> update last traveling data time stamp
* <ul> don't count as activity of client.
* </li>
*
*/
public void notifyObjectSent(BlazedsCommunicationChannel channel, Object object) {
logObject(object, true);
HeartbeatDetails details = clients.get(channel);
details.updateTasks(true /* update traveling data */, false /* no activity */);
}
private void logObject(Object object, boolean sending) {
boolean channelObserverSignal = false;
if (object == null)
channelObserverSignal = true;
else if (object instanceof InvokeServiceMethodServerCommand) {
InvokeServiceMethodServerCommand command = (InvokeServiceMethodServerCommand) object;
if (command.getServiceId().equals(SERVICE_ID))
if (command.getMethodName().equals("signalHeartbeat") || command.getMethodName().equals("signalActivity"))
channelObserverSignal = true;
}
String logMessage = sending ? "Sent: {}" : "Received: {}";
if (logger.isDebugEnabled() && !channelObserverSignal)
logger.debug(logMessage, object);
else if (logger.isTraceEnabled() && channelObserverSignal)
logger.trace(logMessage, object);
}
/**
* Opens a dialog on client side notifying that it will be disconnected in <code>secondsUntilDisconnect</code> seconds.
*
*/
public void warnAboutNoActivity(BlazedsCommunicationChannel channel, long secondsUntilDisconnect) {
invokeClientMethod(channel, CLIENT_ID, "warnAboutNoActivity", new Object[] { secondsUntilDisconnect });
}
protected HeartbeatDetails getDataFromStatefulClientLocalState(StatefulServiceInvocationContext context, IStatefulClientLocalState statefulClientLocalState) {
BlazedsCommunicationChannel channel = (BlazedsCommunicationChannel) context.getCommunicationChannel();
if (context.getCommand() != null && "unsubscribe".equals(context.getCommand().getMethodName()))
return clients.get(channel); // Return the already exiting one.
return new HeartbeatDetails(channel);
}
/**
* Subscribes as soon as the channel is created.
*
*/
public void communicationChannelCreated(CommunicationChannel communicationChannel) {
subscribe(new StatefulServiceInvocationContext(communicationChannel, null, null), null);
}
public void communicationChannelDestroyed(CommunicationChannel webCommunicationChannel) {
unsubscribe(new StatefulServiceInvocationContext(webCommunicationChannel, null, null), null);
}
///////////////////////////////////////////////////////////////
//@RemoteInvocation methods
///////////////////////////////////////////////////////////////
/**
* Update last traveling data time stamp.
*
*/
@RemoteInvocation
public void signalHeartbeat(StatefulServiceInvocationContext context) {
HeartbeatDetails details = clients.get(context.getCommunicationChannel());
details.updateTasks(true /* update traveling data */, false /* no activity */);
}
/**
* Update last client activity time stamp.
* Also count as updating last traveling data time stamp.
*
*/
@RemoteInvocation
public void signalActivity(StatefulServiceInvocationContext context) {
HeartbeatDetails details = clients.get(context.getCommunicationChannel());
details.updateTasks(true /* update traveling data */, true /* update activity */);
}
@Override
@RemoteInvocation
public void subscribe(StatefulServiceInvocationContext context, IStatefulClientLocalState statefulClientLocalState) {
if (context.getStatefulClientId() != null) // Subscription has already been done when the channel was created.
return;
super.subscribe(context, statefulClientLocalState);
HeartbeatDetails details = clients.get(context.getCommunicationChannel());
details.schedule();
}
@Override
@RemoteInvocation
public void unsubscribe(StatefulServiceInvocationContext context, IStatefulClientLocalState statefulClientLocalState) {
HeartbeatDetails details = clients.get(context.getCommunicationChannel());
details.unschedule();
super.unsubscribe(context, statefulClientLocalState);
}
}