Package com.sun.sgs.impl.service.session

Source Code of com.sun.sgs.impl.service.session.ClientSessionServiceImpl$FlushContextsThread

/*
* 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.Node.Health;
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 #LOGIN_HIGH_WATER_PROPERTY}
</b></code><br>
<i>Default:</i> {@code Integer.MAX_VALUE / 2}
*
* <dd style="padding-top: .5em">Specifies the login high water. When the
* number of logins reaches the high water, the service's health is set
* to {@link Health#YELLOW}. If the number of logins exceeds 10% above the high water
* the service's health is set to {@link Health#ORANGE}. Legal values are between 0 and
* {@code Integer.MAX_VALUE}.<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 name of the login high water property. */
    static final String LOGIN_HIGH_WATER_PROPERTY =
        PKG_NAME + ".login.high.water";

    /** 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;

    /** Health of the client session service. */
    private Health health = Health.GREEN;

    /** Login high water. */
    private int loginHighWater;

    /** 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);
            loginHighWater = wrappedProps.getIntProperty(
                LOGIN_HIGH_WATER_PROPERTY,
                Integer.MAX_VALUE / 2, 0, Integer.MAX_VALUE / 2);
      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, this);
            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  " + LOGIN_HIGH_WATER_PROPERTY + "=" + loginHighWater +
                       "\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");
  }
    }

    /**
     * Checks if the number of logins has passed the high water mark, and if
     * so, sets this service's health accordingly.
     */
    private synchronized void checkHighWater() {
        if (handlers.size() > loginHighWater * 1.1) {
            setHealth(Health.ORANGE);
        } else if (handlers.size() >= loginHighWater) {
            setHealth(Health.YELLOW);
        } else {
            setHealth(Health.GREEN);
        }
    }

    /**
     * Set the health of this service. If the specified health has changed,
     * report it.
     * @param newHealth the service's health
     */
    private void setHealth(Health newHealth) {
        if (newHealth == health) {
            return;
        }
        health = newHealth;

        if (logger.isLoggable(Level.FINE)) {
            logger.log(Level.FINE, "Reporting change in health to " + health);
        }
        taskScheduler.scheduleTask(
            new AbstractKernelRunnable("ReportHealth") {
                public void run() {
                   watchdogService.reportHealth(health, CLASSNAME);
                }
            }, taskOwner);
    }

    /**
     * Get the number of connected sessions.
     * @return the number of connected sessions
     */
    int getNumSessions() {
        return handlers.size();
    }

    /**
     * Get the login high water.
     * @return the login high water
     */
    int getLoginHighWater() {
        return loginHighWater;
    }

    /**
     * Set the login high water. This call may cause the service's
     * health to change.
     * @param highWater the login high water
     */
    void setLoginHighWater(int highWater) {
        loginHighWater = highWater;
        logger.log(Level.CONFIG, "Login high water set to {0}", loginHighWater);
        checkHighWater();
    }

    /**
     * Get the service's health.
     * @return the service's health
     */
    Health getHealth() {
        return health;
    }

   /**
     * 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);
        checkHighWater();
  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);
        checkHighWater();
  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);
      }
  }
    }
}
TOP

Related Classes of com.sun.sgs.impl.service.session.ClientSessionServiceImpl$FlushContextsThread

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.