/* * Copyright 2007-2010 Sun Microsystems, Inc. * * This file is part of Project Darkstar Server. * * Project Darkstar Server is free software: you can redistribute it * and/or modify it under the terms of the GNU General Public License * version 2 as published by the Free Software Foundation and * distributed hereunder to you. * * Project Darkstar Server 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. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * -- */ package com.sun.sgs.impl.service.session; import com.sun.sgs.app.Delivery; import com.sun.sgs.app.NameNotBoundException; import com.sun.sgs.app.ObjectNotFoundException; import com.sun.sgs.app.Task; import com.sun.sgs.app.TransactionNotActiveException; import com.sun.sgs.app.util.ManagedSerializable; import com.sun.sgs.app.util.ScalableHashMap; import com.sun.sgs.auth.Identity; import com.sun.sgs.impl.kernel.ConfigManager; import com.sun.sgs.impl.kernel.StandardProperties; import com.sun.sgs.impl.service.channel.ChannelServiceImpl; import com.sun.sgs.impl.service.session.ClientSessionHandler. SetupCompletionFuture; import com.sun.sgs.impl.service.session.ClientSessionImpl. HandleNextDisconnectedSessionTask; import com.sun.sgs.impl.sharedutil.HexDumper; import com.sun.sgs.impl.sharedutil.LoggerWrapper; import com.sun.sgs.impl.sharedutil.Objects; import com.sun.sgs.impl.sharedutil.PropertiesWrapper; import com.sun.sgs.impl.util.AbstractKernelRunnable; import com.sun.sgs.impl.util.AbstractService; import com.sun.sgs.impl.util.Exporter; import com.sun.sgs.impl.util.TransactionContext; import com.sun.sgs.impl.util.TransactionContextFactory; import com.sun.sgs.kernel.ComponentRegistry; import com.sun.sgs.kernel.KernelRunnable; import com.sun.sgs.kernel.TaskQueue; import com.sun.sgs.kernel.TaskScheduler; import com.sun.sgs.protocol.ProtocolAcceptor; import com.sun.sgs.protocol.ProtocolDescriptor; import com.sun.sgs.protocol.ProtocolListener; import com.sun.sgs.protocol.RelocateFailureException; import com.sun.sgs.protocol.RequestCompletionHandler; import com.sun.sgs.protocol.SessionProtocol; import com.sun.sgs.protocol.SessionProtocolHandler; import com.sun.sgs.protocol.simple.SimpleSgsProtocol; import com.sun.sgs.profile.ProfileCollector; import com.sun.sgs.service.ClientSessionStatusListener; import com.sun.sgs.service.ClientSessionService; import com.sun.sgs.service.DataService; import com.sun.sgs.service.IdentityRelocationListener; import com.sun.sgs.service.Node; import com.sun.sgs.service.NodeMappingListener; import com.sun.sgs.service.NodeMappingService; import com.sun.sgs.service.RecoveryListener; import com.sun.sgs.service.SimpleCompletionHandler; import com.sun.sgs.service.TaskService; import com.sun.sgs.service.Transaction; import com.sun.sgs.service.TransactionProxy; import com.sun.sgs.service.WatchdogService; import java.io.IOException; import java.io.Serializable; import java.math.BigInteger; import java.nio.ByteBuffer; import java.security.SecureRandom; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.Properties; import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.logging.Level; import java.util.logging.Logger; import javax.management.JMException; /** * Manages client sessions. <p> * * The {@link #ClientSessionServiceImpl constructor} requires the <a * href="../../../impl/kernel/doc-files/config-properties.html#com.sun.sgs.app.name"> * <code>com.sun.sgs.app.name</code></a> configuration * property and supports these * public configuration <a * href="../../../impl/kernel/doc-files/config-properties.html#ClientSessionService"> * properties</a>. It also supports the following additional properties: <p> * * <dl style="margin-left: 1em"> * * <dt> <i>Property:</i> <code><b> * {@value #SERVER_PORT_PROPERTY} * </b></code><br> * <i>Default:</i> {@value #DEFAULT_SERVER_PORT} * * <dd style="padding-top: .5em">Specifies the port for the * <code>ClientSessionService</code>'s internal server.<p> * * <dt> <i>Property:</i> <code><b> * {@value #WRITE_BUFFER_SIZE_PROPERTY} * </b></code><br> * <i>Default:</i> {@value #DEFAULT_WRITE_BUFFER_SIZE} * * <dd style="padding-top: .5em">Specifies the approximate write buffer capacity * per client session.<p> * * <dt> <i>Property:</i> <code><b> * {@value #EVENTS_PER_TXN_PROPERTY} * </b></code><br> * <i>Default:</i> {@value #DEFAULT_EVENTS_PER_TXN} * * <dd style="padding-top: .5em">Specifies the number of client session events * to process per transaction.<p> * * <dt> <i>Property:</i> <code><b> * {@value #ALLOW_NEW_LOGIN_PROPERTY} * </b></code><br> * <i>Default:</i> {@code false} * * <dd style="padding-top: .5em">If {@code false}, any connecting client with * the same username as an already connected client will not be permitted * to login. If {@code true}, the user's existing session will be * disconnected and the new login is allowed to proceed.<p> * * <dt> <i>Property:</i> <code><b> * {@value #PROTOCOL_ACCEPTOR_PROPERTY} * </b></code><br> * <i>Default:</i> {@value #DEFAULT_PROTOCOL_ACCEPTOR} * * <dd style="padding-top: .5em">Specifies the name of the class * which will be used as the protocol acceptor. The default value uses * an acceptor based on the {@link SimpleSgsProtocol}. Other values should * specify the fully qualified name of a non-abstract class that implements * {@link ProtocolAcceptor}.<p> * * <dt> <i>Property:</i> <code><b> * {@value #RELOCATION_KEY_LENGTH_PROPERTY} * </b></code><br> * <i>Default:</i> {@value #DEFAULT_RELOCATION_KEY_LENGTH} * * <dd style="padding-top: .5em">Specifies the length, in bytes, of a * relocation key.<p> * * <dt> <i>Property:</i> <code><b> * {@value * com.sun.sgs.impl.kernel.StandardProperties#SESSION_RELOCATION_TIMEOUT_PROPERTY} * </b></code><br> * <i>Default:</i> * {@value * com.sun.sgs.impl.kernel.StandardProperties#DEFAULT_SESSION_RELOCATION_TIMEOUT} * * <dd style="padding-top: .5em">Specifies the timeout, in milliseconds, * for client session relocation.<p> * * </dl> <p> */ public final class ClientSessionServiceImpl extends AbstractService implements ClientSessionService { /** The package name. */ private static final String PKG_NAME = "com.sun.sgs.impl.service.session"; /** The name of this class. */ private static final String CLASSNAME = ClientSessionServiceImpl.class.getName(); /** The logger for this class. */ private static final LoggerWrapper logger = new LoggerWrapper(Logger.getLogger(PKG_NAME)); /** The name of the version key. */ private static final String VERSION_KEY = PKG_NAME + ".service.version"; /** The name of the key for the protocol descriptors map. */ private static final String PROTOCOL_DESCRIPTORS_MAP_KEY = PKG_NAME + ".service.protocol.descriptors.map"; /** The major version. */ private static final int MAJOR_VERSION = 2; /** The minor version. */ private static final int MINOR_VERSION = 0; /** The name of the server port property. */ static final String SERVER_PORT_PROPERTY = PKG_NAME + ".server.port"; /** The default server port. */ static final int DEFAULT_SERVER_PORT = 0; /** The name of the write buffer size property. */ static final String WRITE_BUFFER_SIZE_PROPERTY = PKG_NAME + ".buffer.write.max"; /** The default write buffer size: {@value #DEFAULT_WRITE_BUFFER_SIZE} */ static final int DEFAULT_WRITE_BUFFER_SIZE = 128 * 1024; /** The events per transaction property. */ static final String EVENTS_PER_TXN_PROPERTY = PKG_NAME + ".events.per.txn"; /** The default events per transaction. */ static final int DEFAULT_EVENTS_PER_TXN = 1; /** The name of the allow new login property. */ static final String ALLOW_NEW_LOGIN_PROPERTY = PKG_NAME + ".allow.new.login"; /** The protocol acceptor property name. */ static final String PROTOCOL_ACCEPTOR_PROPERTY = PKG_NAME + ".protocol.acceptor"; /** The default protocol acceptor class. */ static final String DEFAULT_PROTOCOL_ACCEPTOR = "com.sun.sgs.impl.protocol.simple.SimpleSgsProtocolAcceptor"; /** The relocation key length property. */ static final String RELOCATION_KEY_LENGTH_PROPERTY = PKG_NAME + ".relocation.key.length"; /** The default length of a relocation key, in bytes. */ static final int DEFAULT_RELOCATION_KEY_LENGTH = 16; /** A random number generator for relocation keys. */ private static final SecureRandom random = new SecureRandom(); /** The write buffer size for new connections. */ private final int writeBufferSize; /** The local node's ID. */ private final long localNodeId; /** The registered session disconnect listeners. */ private final Set<ClientSessionStatusListener> sessionStatusListeners = Collections.synchronizedSet( new HashSet<ClientSessionStatusListener>()); /** A map of local session handlers, keyed by session ID . */ private final Map<BigInteger, ClientSessionHandler> handlers = Collections.synchronizedMap( new HashMap<BigInteger, ClientSessionHandler>()); /** Queue of contexts that are prepared (non-readonly) or committed. */ private final Queue<Context> contextQueue = new ConcurrentLinkedQueue<Context>(); /** Thread for flushing committed contexts. */ private final Thread flushContextsThread = new FlushContextsThread(); /** Lock for notifying the thread that flushes committed contexts. */ private final Object flushContextsLock = new Object(); /** The transaction context factory. */ private final TransactionContextFactory<Context> contextFactory; /** The watchdog service. */ final WatchdogService watchdogService; /** The node mapping service. */ final NodeMappingService nodeMapService; /** The task service. */ final TaskService taskService; /** The channel service. */ private volatile ChannelServiceImpl channelService; /** The exporter for the ClientSessionServer. */ private final Exporter<ClientSessionServer> exporter; /** The ClientSessionServer remote interface implementation. */ private final SessionServerImpl serverImpl; /** The proxy for the ClientSessionServer. */ private final ClientSessionServer serverProxy; /** The protocol listener. */ private final ProtocolListener protocolListener; /** The protocol acceptor. */ private final ProtocolAcceptor protocolAcceptor; /** The map of logged in {@code ClientSessionHandler}s, keyed by * identity. */ private final ConcurrentHashMap<Identity, ClientSessionHandler> loggedInIdentityMap = new ConcurrentHashMap<Identity, ClientSessionHandler>(); /** The map of session task queues, keyed by session ID. */ private final ConcurrentHashMap<BigInteger, TaskQueue> sessionTaskQueues = new ConcurrentHashMap<BigInteger, TaskQueue>(); /** Information for preparing a session to relocate from this node, * keyed by session ID. */ private final ConcurrentHashMap<BigInteger, PrepareRelocationInfo> prepareRelocationMap = new ConcurrentHashMap<BigInteger, PrepareRelocationInfo>(); /** The map of relocation information for sessions relocating to * this node, keyed by relocation key. */ private final ConcurrentHashMap<BigInteger, RelocationInfo> incomingSessionRelocationKeys = new ConcurrentHashMap<BigInteger, RelocationInfo>(); /** The map of relocation information for sessions relocating to this * node, keyed by session ID. */ private final ConcurrentHashMap<BigInteger, RelocationInfo> incomingSessionRelocationInfo = new ConcurrentHashMap<BigInteger, RelocationInfo>(); /** The set of identities that are relocating to this node. */ private final Set<Identity> incomingRelocatingIdentities = Collections.synchronizedSet(new HashSet<Identity>()); /** The maximum number of session events to service per transaction. */ final int eventsPerTxn; /** The flag that indicates how to handle same user logins. If {@code * true}, then if the same user logs in, the existing session will be * disconnected, and the new login is allowed to proceed. If {@code * false}, then if the same user logs in, the new login will be denied. */ final boolean allowNewLogin; /** The session relocation key length. */ final int relocationKeyLength; /** The session relocation timeout. */ final long relocationTimeout; /** Our JMX exposed statistics. */ final ClientSessionServiceStats serviceStats; /** * Constructs an instance of this class with the specified properties. * * @param properties service properties * @param systemRegistry system registry * @param txnProxy transaction proxy * @throws Exception if a problem occurs when creating the service */ public ClientSessionServiceImpl(Properties properties, ComponentRegistry systemRegistry, TransactionProxy txnProxy) throws Exception { super(properties, systemRegistry, txnProxy, logger); logger.log(Level.CONFIG, "Creating ClientSessionServiceImpl"); PropertiesWrapper wrappedProps = new PropertiesWrapper(properties); try { /* * Get the property for controlling session event processing * and connection disconnection. */ writeBufferSize = wrappedProps.getIntProperty( WRITE_BUFFER_SIZE_PROPERTY, DEFAULT_WRITE_BUFFER_SIZE, 8192, Integer.MAX_VALUE); eventsPerTxn = wrappedProps.getIntProperty( EVENTS_PER_TXN_PROPERTY, DEFAULT_EVENTS_PER_TXN, 1, Integer.MAX_VALUE); allowNewLogin = wrappedProps.getBooleanProperty( ALLOW_NEW_LOGIN_PROPERTY, false); relocationKeyLength = wrappedProps.getIntProperty( RELOCATION_KEY_LENGTH_PROPERTY, DEFAULT_RELOCATION_KEY_LENGTH, 16, Integer.MAX_VALUE); relocationTimeout = wrappedProps.getLongProperty( StandardProperties.SESSION_RELOCATION_TIMEOUT_PROPERTY, StandardProperties.DEFAULT_SESSION_RELOCATION_TIMEOUT, 1000L, Long.MAX_VALUE); /* Export the ClientSessionServer. */ int serverPort = wrappedProps.getIntProperty( SERVER_PORT_PROPERTY, DEFAULT_SERVER_PORT, 0, 65535); serverImpl = new SessionServerImpl(); exporter = new Exporter<ClientSessionServer>(ClientSessionServer.class); try { int port = exporter.export(serverImpl, serverPort); serverProxy = exporter.getProxy(); if (logger.isLoggable(Level.CONFIG)) { logger.log(Level.CONFIG, "export successful. port:{0,number,#}", port); } } catch (Exception e) { try { exporter.unexport(); } catch (RuntimeException re) { } throw e; } /* Get services and check service version. */ flushContextsThread.start(); contextFactory = new ContextFactory(txnProxy); watchdogService = txnProxy.getService(WatchdogService.class); nodeMapService = txnProxy.getService(NodeMappingService.class); taskService = txnProxy.getService(TaskService.class); localNodeId = dataService.getLocalNodeId(); watchdogService.addRecoveryListener( new ClientSessionServiceRecoveryListener()); transactionScheduler.runTask( new AbstractKernelRunnable("CheckServiceVersion") { public void run() { checkServiceVersion( VERSION_KEY, MAJOR_VERSION, MINOR_VERSION); } }, taskOwner); /* Store the ClientSessionServer proxy in the data store. */ transactionScheduler.runTask( new AbstractKernelRunnable("StoreClientSessionServiceProxy") { public void run() { // TBD: this could use a BindingKeyedMap. dataService.setServiceBinding( getClientSessionServerKey(localNodeId), new ManagedSerializable<ClientSessionServer>( serverProxy)); } }, taskOwner); /* Register the identity relocation and node mapping listeners. */ nodeMapService.addIdentityRelocationListener( new IdentityRelocationListenerImpl()); nodeMapService.addNodeMappingListener( new NodeMappingListenerImpl()); /* * Create the protocol listener and acceptor. */ protocolListener = new ProtocolListenerImpl(); protocolAcceptor = wrappedProps.getClassInstanceProperty( PROTOCOL_ACCEPTOR_PROPERTY, DEFAULT_PROTOCOL_ACCEPTOR, ProtocolAcceptor.class, new Class[] { Properties.class, ComponentRegistry.class, TransactionProxy.class }, properties, systemRegistry, txnProxy); assert protocolAcceptor != null; /* Create our service profiling info and register our MBean */ ProfileCollector collector = systemRegistry.getComponent(ProfileCollector.class); serviceStats = new ClientSessionServiceStats(collector); try { collector.registerMBean(serviceStats, ClientSessionServiceStats.MXBEAN_NAME); } catch (JMException e) { logger.logThrow(Level.CONFIG, e, "Could not register MBean"); } /* Set the protocol descriptor in the ConfigMXBean. */ ConfigManager config = (ConfigManager) collector.getRegisteredMBean(ConfigManager.MXBEAN_NAME); if (config == null) { logger.log(Level.CONFIG, "Could not find ConfigMXBean"); } else { config.setProtocolDescriptor( protocolAcceptor.getDescriptor().toString()); } logger.log(Level.CONFIG, "Created ClientSessionServiceImpl with properties:" + "\n " + ALLOW_NEW_LOGIN_PROPERTY + "=" + allowNewLogin + "\n " + WRITE_BUFFER_SIZE_PROPERTY + "=" + writeBufferSize + "\n " + EVENTS_PER_TXN_PROPERTY + "=" + eventsPerTxn + "\n " + RELOCATION_KEY_LENGTH_PROPERTY + "=" + relocationKeyLength + "\n " + StandardProperties.SESSION_RELOCATION_TIMEOUT_PROPERTY + "=" + relocationTimeout + "\n " + PROTOCOL_ACCEPTOR_PROPERTY + "=" + protocolAcceptor.getClass().getName() + "\n " + SERVER_PORT_PROPERTY + "=" + serverPort); } catch (Exception e) { if (logger.isLoggable(Level.CONFIG)) { logger.logThrow( Level.CONFIG, e, "Failed to create ClientSessionServiceImpl"); } doShutdown(); throw e; } } /* -- Implement AbstractService -- */ /** {@inheritDoc} */ protected void handleServiceVersionMismatch( Version oldVersion, Version currentVersion) { throw new IllegalStateException( "unable to convert version:" + oldVersion + " to current version:" + currentVersion); } /** {@inheritDoc} */ public void doReady() throws Exception { channelService = txnProxy.getService(ChannelServiceImpl.class); try { protocolAcceptor.accept(protocolListener); } catch (IOException e) { if (logger.isLoggable(Level.CONFIG)) { logger.logThrow( Level.CONFIG, e, "Failed to start accepting connections"); } throw e; } transactionScheduler.runTask( new AbstractKernelRunnable("AddProtocolDescriptorMapping") { public void run() { getProtocolDescriptorsMap(). put(localNodeId, Collections.singleton( protocolAcceptor.getDescriptor())); } }, taskOwner); } /** {@inheritDoc} */ public void doShutdown() { if (protocolAcceptor != null) { try { protocolAcceptor.close(); } catch (IOException ignore) { } } for (ClientSessionHandler handler : handlers.values()) { handler.shutdown(); } handlers.clear(); if (exporter != null) { try { exporter.unexport(); logger.log(Level.FINEST, "client session server unexported"); } catch (RuntimeException e) { logger.logThrow(Level.FINE, e, "unexport server throws"); // swallow exception } } synchronized (flushContextsLock) { flushContextsLock.notifyAll(); } } /** * Returns the proxy for the client session server on the specified * {@code nodeId}, or {@code null} if no server exists. * * @param nodeId a node ID * @return the proxy for the client session server on the specified * {@code nodeId}, or {@code null} */ ClientSessionServer getClientSessionServer(long nodeId) { if (nodeId == localNodeId) { return serverImpl; } else { String sessionServerKey = getClientSessionServerKey(nodeId); try { ManagedSerializable wrappedProxy = (ManagedSerializable) dataService.getServiceBinding(sessionServerKey); return (ClientSessionServer) wrappedProxy.get(); } catch (NameNotBoundException e) { return null; } catch (ObjectNotFoundException e) { logger.logThrow( Level.SEVERE, e, "ClientSessionServer binding:{0} exists, " + "but object removed", sessionServerKey); throw e; } } } /* -- Implement ClientSessionService -- */ /** {@inheritDoc} */ public void addSessionStatusListener( ClientSessionStatusListener listener) { Objects.checkNull("listener", listener); checkNonTransactionalContext(); serviceStats.addSessionStatusListenerOp.report(); sessionStatusListeners.add(listener); } /** {@inheritDoc} */ public SessionProtocol getSessionProtocol(BigInteger sessionRefId) { Objects.checkNull("sessionRefId", sessionRefId); // This operation is used within a transaction by ChannelService. //checkNonTransactionalContext(); serviceStats.getSessionProtocolOp.report(); ClientSessionHandler handler = handlers.get(sessionRefId); return handler != null ? handler.getSessionProtocol() : null; } /** {@inheritDoc} */ public boolean isRelocatingToLocalNode(BigInteger sessionRefId) { Objects.checkNull("sessionRefId", sessionRefId); checkNonTransactionalContext(); serviceStats.isRelocatingToLocalNodeOp.report(); return incomingSessionRelocationInfo.containsKey(sessionRefId); } /* -- Implement IdentityRelocationListener -- */ /** * This listener receives notifications of identities that are going * to be relocated from this node to give services a chance to * prepare for the relocation. <p> * * Before an identity is relocated from this node the {@link * #prepareToRelocate} method is invoked on this listener. If the * identity corresponds to a local client session, then this service * starts to prepare the client session for relocation. First, it adds * a {@code MoveEvent} to the client session's event queue so that the * client session can be marked as relocating. */ private class IdentityRelocationListenerImpl implements IdentityRelocationListener { /** {@inheritDoc} */ public void prepareToRelocate(Identity id, final long newNodeId, SimpleCompletionHandler handler) { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "identity:{0} localNode:{1} newNodeId:{2}", id, localNodeId, newNodeId); } final ClientSessionHandler sessionHandler = loggedInIdentityMap.get(id); if (sessionHandler != null) { if (!sessionHandler.supportsRelocation()) { // Ignore request if client session does not suppport // relocation. If request is ignored, then identity // mapping will not be modified. logger.log( Level.SEVERE, "Attempt to relocate client:{0} that does not " + "support relocation", id); return; } // The specified identity corresponds to a local client session, // so prepare to move it. final BigInteger sessionRefId = sessionHandler.sessionRefId; PrepareRelocationInfo prepareInfo = new PrepareRelocationInfo(sessionRefId, newNodeId, handler); PrepareRelocationInfo oldPrepareInfo = prepareRelocationMap.putIfAbsent(sessionRefId, prepareInfo); if (oldPrepareInfo == null) { if (sessionHandler.isRelocating() || !sessionHandler.isConnected()) { // Request to prepare identity for relocation is // received after preparation has already been completed // and session is in the process of relocating. prepareRelocationMap.remove(sessionRefId); handler.completed(); return; } transactionScheduler.scheduleTask( new AbstractKernelRunnable("AddMoveEvent") { public void run() { ClientSessionImpl session = ClientSessionImpl.getSession( dataService, sessionHandler.sessionRefId); if (session != null) { session.addMoveEvent(newNodeId); } } }, id); // Add a task to monitor preparation and relocation and // disconnect client session if preparation and // relocation has not completed in time. taskScheduler.scheduleTask( new MonitorSessionRelocatingFromLocalNodeTask( sessionRefId), id, System.currentTimeMillis() + relocationTimeout); } else { // Duplicate request to prepare for relocation; add // completion handler to be notified oldPrepareInfo.addNmsCompletionHandler(handler); } } else { // The specified identity does not correspond to a local // client session. handler.completed(); } } } /* -- Implement NodeMappingListener -- */ /** * The client session service's {@code NodeMappingListener} * implementation is interested in mappings that have been removed from * the local node that correspond to client sessions that are * relocating from this node. When the node mapping service removes * the identity mapping of a relocating session from this node, that * means relocation preparation is complete and the client session * corresponding to the identity can be notified to relocate its * connection to the new node. */ private class NodeMappingListenerImpl implements NodeMappingListener { /** {@inheritDoc} */ public void mappingAdded(Identity id, Node oldNode) { } /** {@inheritDoc} */ public void mappingRemoved(Identity id, Node newNode) { ClientSessionHandler sessionHandler = loggedInIdentityMap.get(id); if (sessionHandler != null) { // The specified identity corresponds to a local client session, // which should have already been prepared to move BigInteger sessionRefId = sessionHandler.sessionRefId; // Remove session from table of relocation preparers. if (prepareRelocationMap.remove(sessionRefId) != null) { // Notify client to start relocating its connection. sessionHandler.setRelocatePreparationComplete(); } else { if (newNode != null) { if (logger.isLoggable(Level.WARNING)) { logger.log( Level.WARNING, "Disconnecting unprepared session:{0} whose " + "identity:{1} was remapped from " + "localNodeId:{2} to node:{3}", HexDumper.toHexString(sessionRefId), id, localNodeId, newNode.getId()); } } else { if (logger.isLoggable(Level.WARNING)) { logger.log( Level.WARNING, "Disconnecting session:{0} whose " + "identity:{1} was removed prematurely " + "from localNodeId:{2} ", HexDumper.toHexString(sessionRefId), id, localNodeId); } } sessionHandler.handleDisconnect(false, false); } } } } /* -- Implement ProtocolListener -- */ /** * This {@code ProtocolListener} implementation handles new and * relocated client sessions. */ private class ProtocolListenerImpl implements ProtocolListener { /** {@inheritDoc} */ public void newLogin( Identity identity, SessionProtocol protocol, RequestCompletionHandler<SessionProtocolHandler> completionHandler) { new ClientSessionHandler( ClientSessionServiceImpl.this, dataService, protocol, identity, completionHandler); } /** {@inheritDoc} */ public void relocatedSession( BigInteger relocationKey, SessionProtocol protocol, RequestCompletionHandler<SessionProtocolHandler> completionHandler) { RelocationInfo info = incomingSessionRelocationKeys.remove(relocationKey); if (info == null) { // No information for specified relocation key. // Session is already relocated, or it's a possible // DOS attack, so notify completion handler of failure. if (logger.isLoggable(Level.FINE)) { logger.log( Level.FINE, "Attempt to relocate to node:{0} with " + "invalid relocation key:{1}", localNodeId, HexDumper.toHexString(relocationKey)); } (new SetupCompletionFuture(null, completionHandler)). setException( new RelocateFailureException( ClientSessionHandler.RELOCATE_REFUSED_REASON, RelocateFailureException.FailureReason.OTHER)); return; } new ClientSessionHandler( ClientSessionServiceImpl.this, dataService, protocol, info.identity, completionHandler, info.sessionRefId); } } /* -- Implement TransactionContextFactory -- */ private class ContextFactory extends TransactionContextFactory<Context> { ContextFactory(TransactionProxy txnProxy) { super(txnProxy, CLASSNAME); } /** {@inheritDoc} */ public Context createContext(Transaction txn) { return new Context(txn); } } /* -- Context class to hold transaction state -- */ final class Context extends TransactionContext { /** Map of client session IDs to an object containing a list of * actions to perform upon transaction commit. */ private final Map<BigInteger, CommitActions> commitActions = new HashMap<BigInteger, CommitActions>(); /** * Constructs a context with the specified transaction. */ private Context(Transaction txn) { super(txn); } /** * Adds an action for the specified session to be performed when * the associated transaction commits. If {@code first} is * {@code true}, then the action is added as the first commit * action for the specified session. */ void addCommitAction( BigInteger sessionRefId, Action action, boolean first) { try { if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, "Context.addCommitAction session:{0} action:{1}", sessionRefId, action); } checkPrepared(); CommitActions sessionActions = commitActions.get(sessionRefId); if (sessionActions == null) { sessionActions = new CommitActions(); commitActions.put(sessionRefId, sessionActions); } if (first) { sessionActions.addFirst(action); } else { sessionActions.add(action); } } catch (RuntimeException e) { if (logger.isLoggable(Level.FINE)) { logger.logThrow( Level.FINE, e, "Context.addCommitAction exception"); } throw e; } } /** * Throws a {@code TransactionNotActiveException} if this * transaction is prepared. */ private void checkPrepared() { if (isPrepared) { throw new TransactionNotActiveException("Already prepared"); } } /** * Marks this transaction as prepared, and if there are * pending changes, adds this context to the context queue and * returns {@code false}. Otherwise, if there are no pending * changes returns {@code true} indicating readonly status. */ public boolean prepare() { isPrepared = true; boolean readOnly = commitActions.isEmpty(); if (!readOnly) { contextQueue.add(this); } else { isCommitted = true; } return readOnly; } /** * Removes the context from the context queue containing * pending actions, and checks for flushing committed contexts. */ public void abort(boolean retryable) { contextQueue.remove(this); checkFlush(); } /** * Marks this transaction as committed, and checks for * flushing committed contexts. */ public void commit() { isCommitted = true; checkFlush(); } /** * Wakes up the thread to process committed contexts in the * context queue if the queue is non-empty and the first * context in the queue is committed. */ private void checkFlush() { Context context = contextQueue.peek(); if ((context != null) && (context.isCommitted)) { synchronized (flushContextsLock) { flushContextsLock.notifyAll(); } } } /** * Sends all message enqueued during this context's * transaction (via the {@code addMessage} and {@code * addMessageFirst} methods), and disconnects any session * whose disconnection was requested via the {@code * requestDisconnect} method. */ private boolean flush() { if (shuttingDown()) { return false; } else if (isCommitted) { for (CommitActions actions : commitActions.values()) { actions.flush(); } return true; } else { return false; } } } /** * An action to perform during commit. */ interface Action { /** * Performs the commit action and returns {@code true} if * further actions should be processed after this one, and * returns {@code false} otherwise. A {@code false} value would * be returned, for example, if an action disconnects the client * session or notifies the client of login failure. */ boolean flush(); } /** * Contains pending changes for a given client session. */ private static class CommitActions extends LinkedList<Action> { /** * Flushes all actions enqueued with this instance. */ void flush() { for (Action action : this) { if (!action.flush()) { break; } } } } /** * Thread to process the context queue, in order, to flush any * committed changes. */ private class FlushContextsThread extends Thread { /** * Constructs an instance of this class as a daemon thread. */ public FlushContextsThread() { super(CLASSNAME + "$FlushContextsThread"); setDaemon(true); } /** * Processes the context queue, in order, to flush any * committed changes. This thread waits to be notified that a * committed context is at the head of the queue, then * iterates through the context queue invoking {@code flush} * on the {@code Context} returned by {@code next}. Iteration * ceases when either a context's {@code flush} method returns * {@code false} (indicating that the transaction associated * with the context has not yet committed) or when there are * no more contexts in the context queue. */ public void run() { while (true) { /* * Wait for a non-empty context queue, returning if * the service is shutting down. */ synchronized (flushContextsLock) { if (contextQueue.isEmpty()) { if (shuttingDown()) { return; } try { flushContextsLock.wait(); } catch (InterruptedException e) { logger.log(Level.SEVERE, "FlushContextsThread interrupted, " + "node:{0}", localNodeId); return; } } } if (shuttingDown()) { return; } /* * Remove committed contexts from head of context * queue, and enqueue them to be flushed. */ if (!contextQueue.isEmpty()) { Iterator<Context> iter = contextQueue.iterator(); while (iter.hasNext()) { if (shuttingDown()) { return; } Context context = iter.next(); if (context.flush()) { iter.remove(); } else { break; } } } } } } /* -- Implement ClientSessionServer -- */ /** * Implements the {@code ClientSessionServer} that receives * requests from {@code ClientSessionService}s on other nodes to * forward messages local client sessions or to service a client * session's event queue. */ private class SessionServerImpl implements ClientSessionServer { /** {@inheritDoc} */ public void serviceEventQueue(byte[] sessionId) { callStarted(); try { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "serviceEventQueue sessionId:{0}", HexDumper.toHexString(sessionId)); } addServiceEventQueueTask(sessionId); } finally { callFinished(); } } /** {@inheritDoc} */ public byte[] relocatingSession( Identity identity, byte[] sessionId, long oldNodeId) { callStarted(); try { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "sessionId:{0} oldNodeId:{1}", HexDumper.toHexString(sessionId), oldNodeId); } // Cache relocation information. byte[] relocationKey = getNextRelocationKey(); final BigInteger sessionRefId = new BigInteger(1, sessionId); RelocationInfo info = new RelocationInfo(identity, sessionRefId); BigInteger key = new BigInteger(1, relocationKey); incomingSessionRelocationKeys.put(key, info); incomingSessionRelocationInfo.put(sessionRefId, info); incomingRelocatingIdentities.add(identity); // Modify ClientSession's state to indicate that it has been // relocated to the local node. try { transactionScheduler.runTask( new AbstractKernelRunnable( "RelocateSessionToLocalNode") { public void run() { ClientSessionImpl session = ClientSessionImpl.getSession( dataService, sessionRefId); session.move(localNodeId); } }, taskOwner); } catch (Exception e) { // This exception probably means that the client // session is gone, so throw an exception. This will // cause the requester to disconnect the client session // because it can't assign the new node ID to the // session. logger.logThrow( Level.WARNING, e, "Assigning new node ID:{0} to session:{1} throws", localNodeId, HexDumper.toHexString(sessionId)); throw new RuntimeException(e); } // Schedule task to monitor relocation. taskScheduler.scheduleTask( new MonitorSessionRelocatingToLocalNodeTask(key, info), identity, System.currentTimeMillis() + relocationTimeout); return relocationKey; } finally { callFinished(); } } /** {@inheritDoc} */ public void send(byte[] sessionId, byte[] message, byte deliveryOrdinal) { callStarted(); try { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "sessionId:{0} message:{1}", HexDumper.toHexString(sessionId), HexDumper.toHexString(message)); } SessionProtocol sessionProtocol = getSessionProtocol(new BigInteger(1, sessionId)); if (sessionProtocol != null) { Delivery delivery = Delivery.values()[deliveryOrdinal]; try { sessionProtocol.sessionMessage( ByteBuffer.wrap(message), delivery); } catch (IOException e) { // TBD: should we disconnect the session because // messages can't be delivered? if (logger.isLoggable(Level.FINE)) { logger.logThrow( Level.FINE, e, "sending message: sessionId:{0} message:{1} " + "throws", HexDumper.toHexString(sessionId), HexDumper.toHexString(message)); } } catch (RuntimeException e) { // i.e., IllegalStateException // This shouldn't happen because client session // events are not processed while a session is // relocating. logger.logThrow( Level.SEVERE, e, "Attempted send to session:{0} during " + "relocation, message:{1}", HexDumper.toHexString(sessionId), HexDumper.toHexString(message)); } } else { if (logger.isLoggable(Level.FINE)) { logger.log( Level.FINE, "nonexistent session: dropping message for " + "sessionId:{0} message:{1}", HexDumper.toHexString(sessionId), HexDumper.toHexString(message)); } } } finally { callFinished(); } } } /* -- Other methods -- */ /** * Returns the transaction proxy. */ TransactionProxy getTransactionProxy() { return txnProxy; } /** * Returns the local node's ID. * @return the local node's ID */ long getLocalNodeId() { return localNodeId; } /** * Returns the size of the write buffer to use for new connections. * * @return the size of the write buffer to use for new connections */ int getWriteBufferSize() { return writeBufferSize; } /** * Returns the {@code ClientSessionHandler} for the specified client * session ID. * @param sessionRefId a client session ID * @return the handler */ ClientSessionHandler getHandler(BigInteger sessionRefId) { return handlers.get(sessionRefId); } /** * Returns the next relocation key. * * @return the next relocation key */ private byte[] getNextRelocationKey() { byte[] key = new byte[relocationKeyLength]; random.nextBytes(key); return key; } /** * Returns the key for accessing the {@code ClientSessionServer} * instance (which is wrapped in a {@code ManagedSerializable}) * for the specified {@code nodeId}. */ private static String getClientSessionServerKey(long nodeId) { return PKG_NAME + ".server." + nodeId; } /** * Checks if the local node is considered alive, and throws an * {@code IllegalStateException} if the node is no longer alive. * This method should be called within a transaction. */ private void checkLocalNodeAlive() { if (!watchdogService.isLocalNodeAlive()) { throw new IllegalStateException( "local node is not considered alive"); } } /** * Obtains information associated with the current transaction, * throwing TransactionNotActiveException if there is no current * transaction, and throwing IllegalStateException if there is a * problem with the state of the transaction or if this service * has not been initialized with a transaction proxy. */ Context checkContext() { checkLocalNodeAlive(); return contextFactory.joinTransaction(); } /** * Returns the client session service relevant to the current * context. * * @return the client session service relevant to the current * context */ static synchronized ClientSessionServiceImpl getInstance() { if (txnProxy == null) { throw new IllegalStateException("Service not initialized"); } else { return (ClientSessionServiceImpl) txnProxy.getService(ClientSessionService.class); } } /** * Validates the {@code identity} of the user logging in and returns * {@code true} if the login is allowed to proceed, and {@code false} * if the login is denied. * * <p>A user with the specified {@code identity} is allowed to log in * if one of the following conditions holds: * * <ul> * <li>the {@code identity} is not currently logged in and is not * relocating to the current node, or * <li>the {@code identity} is logged in, and the {@code * com.sun.sgs.impl.service.session.allow.new.login} property is * set to {@code true}. * </ul> * In the latter case (new login allowed), the existing user session logged * in with {@code identity} is forcibly disconnected. * * <p>If this method returns {@code true}, the {@link #removeUserLogin} * method must be invoked when the user with the specified {@code * identity} is disconnected. * * @param identity the user identity * @param handler the client session handler * @param loggingIn if {@code true} session with specified * identity is loggingIn; otherwise it is relocating * @return {@code true} if the user is allowed to log in with the * specified {@code identity}, otherwise returns {@code false} */ boolean validateUserLogin(Identity identity, ClientSessionHandler handler, boolean loggingIn) { if (loggingIn && incomingRelocatingIdentities.contains(identity)) { return false; } ClientSessionHandler previousHandler = loggedInIdentityMap.putIfAbsent(identity, handler); if (previousHandler == null) { // No user logged in with the same idenity; allow login. return true; } else if (!allowNewLogin) { // Same user logged in; new login not allowed, so deny login. return false; } else if (!previousHandler.loginHandled()) { // Same user logged in; can't preempt user in the // process of logging in; deny login. return false; } else { if (loggedInIdentityMap.replace( identity, previousHandler, handler)) { // Disconnect current user; allow new login. previousHandler.handleDisconnect(false, true); return true; } else { // Another same user login beat this one; deny login. return false; } } } /** * Notifies this service that the specified {@code identity} is no * longer logged in using the specified {@code handler} so that * internal bookkeeping can be adjusted accordingly. * * @param identity the user identity * @param handler the client session handler */ boolean removeUserLogin(Identity identity, ClientSessionHandler handler) { return loggedInIdentityMap.remove(identity, handler); } /** * Adds the handler for the specified session to the internal * session handler map. This method is invoked by the handler * once the client has successfully logged in or has * successfully relocated. If the client has relocated, the * {@code identity} should be non-null, otherwise, the identity * should be {@code null}. * * @param sessionRefId the session ID, as a {@code BigInteger} * @param handler the client session handler to cache * @param identity if the session has been relocated, a non-null * identity to be removed from the * {@code incomingRelocatingIdentities} cache */ void addHandler(BigInteger sessionRefId, ClientSessionHandler handler, Identity identity) { assert handler != null; handlers.put(sessionRefId, handler); incomingSessionRelocationInfo.remove(sessionRefId); if (identity != null) { incomingRelocatingIdentities.remove(identity); // Notify status listeners that the specified client session // has completed relocating to this node. for (ClientSessionStatusListener statusListener : sessionStatusListeners) { try { statusListener.relocated(sessionRefId); } catch (Exception e) { if (logger.isLoggable(Level.FINE)) { logger.logThrow( Level.FINE, e, "Invoking 'relocated' method " + "on listener:{0} throws", statusListener); } } } } } /** * Removes the specified session from the internal session handler map, * cleans up other session-related transient data, and if {@code * isDisconnecting} is {@code true} notifies all {@link * ClientSessionStatusListener}s of the session's disconnection. This * method is invoked by the handler (in order to clean up the session's * transient data structures) when the session is disconnecting * its connection due to session termination or session relocation. If * a session is disconnecting because the session relocating to another * node, then {@code isDisconnecting} will be {@code false}. */ void removeHandler(BigInteger sessionRefId, boolean isRelocating) { if (shuttingDown()) { return; } // Notify session listeners of disconnection notifyStatusListenersOfDisconnection(sessionRefId, isRelocating); handlers.remove(sessionRefId); sessionTaskQueues.remove(sessionRefId); prepareRelocationMap.remove(sessionRefId); // just in case... } /** * Notifies all registered {@code ClientSessionStatusListener}s that * the session with the specified {@code sessionRefId} has disconnected. */ private void notifyStatusListenersOfDisconnection( BigInteger sessionRefId, boolean isRelocating) { for (ClientSessionStatusListener statusListener : sessionStatusListeners) { try { statusListener.disconnected(sessionRefId, isRelocating); } catch (Exception e) { if (logger.isLoggable(Level.WARNING)) { logger.logThrow( Level.WARNING, e, "notifying listener:{0} of " + "disconnected session:{1} throws", statusListener, HexDumper.toHexString(sessionRefId)); } } } } /** * Schedules a transactional task to service the event queue for the * session with the specified {@code sessionId}. If there is no * locally-connected session with the specified {@code sessionId}, then * no action is taken. <p> * * This method should be invoked outside of a transaction. * * @param sessionId a session ID */ void addServiceEventQueueTask(final byte[] sessionId) { final BigInteger sessionRefId = new BigInteger(1, sessionId); if (!handlers.containsKey(sessionRefId)) { // The session is not local or is disconnected, so this node // should not service the event queue. return; } TaskQueue taskQueue = sessionTaskQueues.get(sessionRefId); if (taskQueue == null) { TaskQueue newTaskQueue = transactionScheduler.createTaskQueue(); taskQueue = sessionTaskQueues. putIfAbsent(sessionRefId, newTaskQueue); if (taskQueue == null) { taskQueue = newTaskQueue; } } taskQueue.addTask( new AbstractKernelRunnable("ServiceEventQueue") { public void run() { if (getHandler(sessionRefId) != null) { ClientSessionImpl.serviceEventQueue(sessionId); } } }, taskOwner); } /** * Notifies each registered {@link ClientSessionStatusListener} that * the session with the specified {@code sessionRefId} is moving to a * new node (specified by {@code newNodeId}). The specified completion * {@code handler} should be notified (via its {@code completed} * method), when all listeners are finished preparing for relocation. * * @param sessionRefId the ID for the relocating client session * @param newNodeId the ID of the new node for the client session */ void notifyPrepareToRelocate(final BigInteger sessionRefId, final long newNodeId) { if (logger.isLoggable(Level.INFO)) { logger.log(Level.INFO, "Preparing session:{0} to relocate to node:{1}", sessionRefId, newNodeId); } PrepareRelocationInfo info = prepareRelocationMap.get(sessionRefId); if (info != null) { info.prepareToRelocate(); } else { logger.log(Level.WARNING, "Ignoring request for session:{0} whichi is not " + "relocating from local node:{1}", sessionRefId, localNodeId); } } /** * Schedules a non-durable, transactional task using the given * {@code Identity} as the owner. */ void scheduleTask(KernelRunnable task, Identity ownerIdentity) { Objects.checkNull("ownerIdentity", ownerIdentity); transactionScheduler.scheduleTask(task, ownerIdentity); } /** * Schedules a non-durable, transactional task using the task service. */ void scheduleTaskOnCommit(KernelRunnable task) { taskService.scheduleNonDurableTask(task, true); } /** * Runs the specified {@code task} immediately, in a transaction. */ void runTransactionalTask(KernelRunnable task, Identity ownerIdentity) throws Exception { Objects.checkNull("ownerIdentity", ownerIdentity); transactionScheduler.runTask(task, ownerIdentity); } /** * Returns the task service. */ static TaskService getTaskService() { return txnProxy.getService(TaskService.class); } /** * Returns the task scheduler. * @return the task scheduler */ TaskScheduler getTaskScheduler() { return taskScheduler; } /** * Returns the channel service. */ ChannelServiceImpl getChannelService() { return channelService; } /** * The {@code RecoveryListener} for handling requests to recover * for a failed {@code ClientSessionService}. */ private class ClientSessionServiceRecoveryListener implements RecoveryListener { /** {@inheritDoc} */ public void recover(final Node node, SimpleCompletionHandler handler) { final long nodeId = node.getId(); final TaskService taskService = getTaskService(); try { if (logger.isLoggable(Level.INFO)) { logger.log(Level.INFO, "Node:{0} recovering for node:{1}", localNodeId, nodeId); } /* * Schedule persistent tasks to perform recovery. */ transactionScheduler.runTask( new AbstractKernelRunnable("ScheduleRecoveryTasks") { public void run() { /* * For each session on the failed node, notify * the session's ClientSessionListener and * clean up the session's persistent data and * bindings. */ taskService.scheduleTask( new HandleNextDisconnectedSessionTask(nodeId)); /* * Remove client session server proxy and * associated binding for failed node, as * well as protocol descriptors for the * failed node. */ taskService.scheduleTask( new RemoveNodeSpecificDataTask(nodeId)); } }, taskOwner); handler.completed(); } catch (Exception e) { logger.logThrow( Level.WARNING, e, "Node:{0} recovering for node:{1} throws", localNodeId, nodeId); // TBD: what should it do if it can't recover? } } } /** * A persistent task to remove the client session server proxy * and protocol descriptors for a failed node. */ private static class RemoveNodeSpecificDataTask implements Task, Serializable { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** The node ID. */ private final long nodeId; /** * Constructs an instance of this class with the specified * {@code nodeId}. */ RemoveNodeSpecificDataTask(long nodeId) { this.nodeId = nodeId; } /** * Removes the client session server proxy and binding and * also removes the protocol descriptors for the node * specified during construction. */ public void run() { String sessionServerKey = getClientSessionServerKey(nodeId); DataService dataService = getInstance().getDataService(); try { dataService.removeObject( dataService.getServiceBinding(sessionServerKey)); getProtocolDescriptorsMap().remove(nodeId); } catch (NameNotBoundException e) { // already removed return; } catch (ObjectNotFoundException e) { } dataService.removeServiceBinding(sessionServerKey); } } /** * Returns a set of protocol descriptors for the specified * {@code nodeId}, or {@code null} if there are no descriptors * for the node. This method must be run outside a transaction. */ Set<ProtocolDescriptor> getProtocolDescriptors(long nodeId) { checkNonTransactionalContext(); GetProtocolDescriptorsTask protocolDescriptorsTask = new GetProtocolDescriptorsTask(nodeId); try { transactionScheduler.runTask(protocolDescriptorsTask, taskOwner); return protocolDescriptorsTask.descriptors; } catch (Exception e) { logger.logThrow(Level.WARNING, e, "GetProtocolDescriptorsTask for node:{0} throws", nodeId); return null; } } /** * A task to obtain the protocol descriptors for a given node. */ private static class GetProtocolDescriptorsTask extends AbstractKernelRunnable { private final long nodeId; volatile Set<ProtocolDescriptor> descriptors = null; /** Constructs an instance with the specified {@code nodeId}. */ GetProtocolDescriptorsTask(long nodeId) { super(null); this.nodeId = nodeId; } /** {@inheritDoc} */ public void run() { descriptors = getProtocolDescriptorsMap().get(nodeId); } } /** * Returns the protocol descriptors map, keyed by node ID. Creates and * stores the map if it doesn't already exist. This method must be run * within a transaction. */ private static Map<Long, Set<ProtocolDescriptor>> getProtocolDescriptorsMap() { DataService dataService = getInstance().getDataService(); Map<Long, Set<ProtocolDescriptor>> protocolDescriptorsMap; try { protocolDescriptorsMap = Objects.uncheckedCast( dataService.getServiceBinding(PROTOCOL_DESCRIPTORS_MAP_KEY)); } catch (NameNotBoundException e) { protocolDescriptorsMap = new ScalableHashMap<Long, Set<ProtocolDescriptor>>(); dataService.setServiceBinding(PROTOCOL_DESCRIPTORS_MAP_KEY, protocolDescriptorsMap); } return protocolDescriptorsMap; } /** * Contains information about a client session relocating to the * local node. */ private static class RelocationInfo { final Identity identity; final BigInteger sessionRefId; RelocationInfo(Identity identity, BigInteger sessionRefId) { this.identity = identity; this.sessionRefId = sessionRefId; } } /** * A task (run after a delay) that checks to see if a client session * with the {@code sessionRefId} specified during construction has * relocated from the local node. If not, the associated client session * is cleaned up. */ private class MonitorSessionRelocatingFromLocalNodeTask extends AbstractKernelRunnable { final BigInteger sessionRefId; /** * Constructs an instance with the specified {@code sessionRefId}. * @param sessionRefId a session ID */ MonitorSessionRelocatingFromLocalNodeTask(BigInteger sessionRefId) { super(null); this.sessionRefId = sessionRefId; } /** * If the associated client hasn't finished preparing to relocate * before this task runs, then clean up the client session. Either * the original server failed to inform the client that it should * relocate or the client session has failed. In either case, the * client session needs to be removed. */ public void run() { ClientSessionHandler sessionHandler = handlers.get(sessionRefId); if (sessionHandler != null) { prepareRelocationMap.remove(sessionRefId); logger.log( Level.WARNING, "Disconnecting session:{0} that timed out " + "relocating from node:{1}", sessionHandler.identity, localNodeId); sessionHandler.handleDisconnect(false, true); } } } /** * A task (run after a delay) that checks to see if a client session * with the associated relocation key has finished relocating to this * node. If not, the associated client session is cleaned up and all * client session status listeners are notified that the client session * has been disconnected. */ private class MonitorSessionRelocatingToLocalNodeTask extends AbstractKernelRunnable { final BigInteger relocationKey; final RelocationInfo info; /** * Constructs an instance with the specified relocation info. * @param relocationKey a relocation key * @param info a client session's relocation info */ MonitorSessionRelocatingToLocalNodeTask( BigInteger relocationKey, RelocationInfo info) { super(null); this.relocationKey = relocationKey; this.info = info; } /** * If the associated client doesn't attempt reestablish the * client session before this task runs, then clean up the * client session. Either the original server failed to * inform the client that it should relocate or the client * session has failed. In either case, the client session * needs to be removed. */ public void run() { if (incomingSessionRelocationKeys.remove(relocationKey) != null) { logger.log( Level.FINE, "Scheduling clean up of session:{0} that " + "failed to relocate to local node:{1}", info.identity, localNodeId); transactionScheduler.scheduleTask( new AbstractKernelRunnable("RemoveNonrelocatedSession") { public void run() { ClientSessionImpl sessionImpl = ClientSessionImpl.getSession( dataService, info.sessionRefId); if (sessionImpl != null) { sessionImpl.notifyListenerAndRemoveSession( dataService, false, true); } } }, info.identity); incomingSessionRelocationInfo.remove(info.sessionRefId); incomingRelocatingIdentities.remove(info.identity); notifyStatusListenersOfDisconnection(info.sessionRefId, false); } } } /** * An abstraction to keep track of the progress of session's * relocation preparation and notify all NodeMappingService's * completion handlers when relocation preparation is complete. * * * An instance of this class is constructed when the * ClientSessionService's IdentityRelocationListener is notified to * prepare to relocate (via the {@link * IdentityRelocationListener#prepareToRelocate prepareToRelocate} * method. */ private final class PrepareRelocationInfo { /** The session that is relocating. */ private final BigInteger sessionRefId; /** The ID of the session's new node. */ private final long newNodeId; /** Completion handlers for the node mapping service. */ private final Set<SimpleCompletionHandler> nmsCompletionHandlers = new HashSet<SimpleCompletionHandler>(); /** Completion handlers for {@code ClientSessionStatusListener}s * that need to prepare before the node mapping service completion * handler(s) are notified. */ private final Set<PrepareCompletionHandler> preparers = new HashSet<PrepareCompletionHandler>(); /** Indicates whether the {@code ClientSessionStatusListener}s have * been notified to prepare for relocation. */ private boolean isPreparing = false; /** * Constructs an instance with the specified {@code sessionRefId}, * {@code newNodeId}, and completion {@code handler} from the node * mapping service. <p> * * This constructor takes a snapshot of all registered {@code * ClientSessionStatusListener}s that need to be notified to * prepare to relocate after the client session service has * finished preparing the client session for relocation. Once the * client session has been prepared to relocate, the client * session service invokes this instance's {@link * #prepareToRelocate} method so that it can notify each {@code * ClientSessionStatusListener} to prepare to relocate. <p> * * When all {@code ClientSessionStatusListener}s have finished * preparing, this instance notifies all node mapping service * completion handlers that relocation preparation has been * completed for the session. There may be more than one * completion handler from the node mapping service because the * node mapping service may notify the client session service more * than once to prepare to relocate a given client session. */ PrepareRelocationInfo(BigInteger sessionRefId, long newNodeId, SimpleCompletionHandler handler) { this.sessionRefId = sessionRefId; this.newNodeId = newNodeId; nmsCompletionHandlers.add(handler); for (ClientSessionStatusListener listener : sessionStatusListeners) { preparers.add(new PrepareCompletionHandler(listener)); } } /** * Notifies all {@code ClientSessionStatusListener}s to prepare for * the client session (specified at construction) to relocate. If * preparing the {@code ClientSessionStatusListener}s is or has * already taken place, this method takes no action. */ synchronized void prepareToRelocate() { if (isPreparing) { return; } isPreparing = true; for (final PrepareCompletionHandler handler : preparers) { taskScheduler.scheduleTask( new AbstractKernelRunnable("PrepareToRelocateSession") { public void run() { try { handler.listener.prepareToRelocate( sessionRefId, newNodeId, handler); } catch (Exception e) { logger.logThrow( Level.WARNING, e, "Notifying listener:{0} to prepare " + "session:{1} to relocate to node:{2} throws", handler.listener, sessionRefId, newNodeId); } } }, taskOwner); } } /** * Adds the specified completion {@code handler} from the {@code * NodeMappingService} to the set of completion handlers that need * to be notified when all {@code ClientSessionStatusListener}s are * finished preparing for relocation. */ synchronized void addNmsCompletionHandler( SimpleCompletionHandler handler) { if (preparers.isEmpty()) { handler.completed(); } else { nmsCompletionHandlers.add(handler); } } /** * Notifies this instance that the {@code * ClientSessionStatusListener} for the specified {@code * listenerCompletionhandler} has completed preparing for * relocation. If all listeners have completed preparation, then * all {@code NodeMappingService} completion handlers are notified * that preparation is complete. Once the node mapping service has * removed the mapping (and subsequently invoked this client session * service's {@code NodeMappingListener.mappingRemoved} method), * the client can be informed that it can start relocating its * connection. */ synchronized void preparationCompleted( PrepareCompletionHandler listenerCompletionHandler) { preparers.remove(listenerCompletionHandler); if (preparers.isEmpty()) { // Notify NodeMappingService completion handlers that // preparation is complete. for (SimpleCompletionHandler nmsCompletionHandler : nmsCompletionHandlers) { if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, "Notifying NMS relocate preparation complete, " + "session:{0} localNodeId:{1} newNodeId:{2}", sessionRefId, getLocalNodeId(), newNodeId); } nmsCompletionHandler.completed(); } } } /** * A completion handler for a {@code ClientSessionStatusListener} * preparing for relocation, specified as an argument to the {@link * ClientSessionStatusListener#prepareToRelocate prepareToRelocate} * method. */ private final class PrepareCompletionHandler implements SimpleCompletionHandler { private final ClientSessionStatusListener listener; /** Indicates whether preparation is completed. */ private boolean completed = false; /** Constructs an instance. */ PrepareCompletionHandler(ClientSessionStatusListener listener) { this.listener = listener; } /** {@inheritDoc} */ public void completed() { synchronized (this) { if (completed) { return; } completed = true; } PrepareRelocationInfo.this.preparationCompleted(this); } } } }