/* * 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.ClientSession; import com.sun.sgs.app.ClientSessionListener; import com.sun.sgs.app.Delivery; import com.sun.sgs.app.DeliveryNotSupportedException; import com.sun.sgs.app.ManagedObject; import com.sun.sgs.app.ManagedObjectRemoval; import com.sun.sgs.app.ManagedReference; import com.sun.sgs.app.MessageRejectedException; import com.sun.sgs.app.NameNotBoundException; import com.sun.sgs.app.ObjectNotFoundException; import com.sun.sgs.app.ResourceUnavailableException; import com.sun.sgs.app.Task; import com.sun.sgs.app.TransactionException; import com.sun.sgs.auth.Identity; import com.sun.sgs.impl.service.session.ClientSessionHandler.DisconnectAction; import com.sun.sgs.impl.service.session.ClientSessionHandler.MoveAction; import com.sun.sgs.impl.service.session.ClientSessionHandler.SendMessageAction; import com.sun.sgs.impl.sharedutil.HexDumper; import com.sun.sgs.impl.sharedutil.LoggerWrapper; import com.sun.sgs.impl.util.AbstractKernelRunnable; import com.sun.sgs.impl.util.IoRunnable; import static com.sun.sgs.impl.util.AbstractService.isRetryableException; import com.sun.sgs.impl.util.ManagedQueue; import com.sun.sgs.service.DataService; import com.sun.sgs.service.Node; import com.sun.sgs.service.TaskService; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.math.BigInteger; import java.nio.ByteBuffer; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * Implements a client session. The non-static, non-transient fields of an * instance of this class are the persistent state of a client session and * are only accessed transactionally. */ public class ClientSessionImpl implements ClientSession, NodeAssignment, Serializable { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** The logger name and prefix for the various session keys. */ private static final String PKG_NAME = "com.sun.sgs.impl.service.session."; /** The session component in a session key. */ private static final String SESSION_COMPONENT = "impl."; /** The listener component in a session's listener key. */ private static final String LISTENER_COMPONENT = "listener."; /** The event queue component in a session's event queue key. */ private static final String QUEUE_COMPONENT = "queue."; /** The node component in a session's node key. */ private static final String NODE_COMPONENT = "node."; /** The logger for this class. */ private static final LoggerWrapper logger = new LoggerWrapper(Logger.getLogger(PKG_NAME + "impl")); /** The local ClientSessionService. */ private transient ClientSessionServiceImpl sessionService; /** The session ID. */ private transient BigInteger id; /** The session ID bytes. * TBD: this should be a transient field. */ private final byte[] idBytes; /** The wrapped client session instance. */ private final ManagedReference<ClientSessionWrapper> wrappedSessionRef; /** The identity for this session. */ private final Identity identity; /** The set of delivery requirements for this session. */ private final Set<Delivery> deliveries; /** The node ID for this session. */ private long nodeId; /** Indicates whether this session is connected. */ private boolean connected = true; /** Maximum message length for session messages. */ private final int maxMessageLength; /** The capacity of the write buffer, in bytes. */ private final int writeBufferCapacity; /** If the value is not {@code -1}, indicates the node ID that this * session is relocating to. */ private long relocatingToNode = -1; /* * TBD: Should a managed reference to the ClientSessionListener be * cached in the ClientSessionImpl for efficiency? */ /** * Constructs an instance of this class with the specified {@code * sessionService}, {@code identity}, and supported {@code deliveries}, * and stores this instance with the following bindings:<p> * * <pre> * com.sun.sgs.impl.service.session.impl.<idBytes> * com.sun.sgs.impl.service.session.node.<nodeId>.impl.<idBytes> *</pre> * This method should only be called within a transaction. * * @param sessionService a client session service * @param identity the session's identity * @param deliveries the session's supported delivery requirements * @param maxMessageLength the maximum session message length * @throws TransactionException if there is a problem with the * current transaction */ ClientSessionImpl(ClientSessionServiceImpl sessionService, Identity identity, Set<Delivery> deliveries, int maxMessageLength) { if (sessionService == null) { throw new NullPointerException("null sessionService"); } else if (identity == null) { throw new NullPointerException("null identity"); } else if (deliveries == null) { throw new NullPointerException("null deliveries"); } this.sessionService = sessionService; this.identity = identity; this.deliveries = deliveries; this.nodeId = sessionService.getLocalNodeId(); this.maxMessageLength = maxMessageLength; writeBufferCapacity = sessionService.getWriteBufferSize(); DataService dataService = sessionService.getDataService(); ManagedReference<ClientSessionImpl> sessionRef = dataService.createReference(this); id = sessionRef.getId(); this.wrappedSessionRef = dataService.createReference(new ClientSessionWrapper(sessionRef)); idBytes = id.toByteArray(); // TBD: these service bindings could be stored in a BindingKeyedMap // instead. dataService.setServiceBinding(getSessionKey(), this); dataService.setServiceBinding(getSessionNodeKey(), this); dataService.setServiceBinding(getEventQueueKey(), new EventQueue(this)); logger.log(Level.FINEST, "Stored session, identity:{0} id:{1}", identity, id); } /* -- Implement ClientSession -- */ /** {@inheritDoc} */ public String getName() { if (!isConnected()) { throw new IllegalStateException("client session is not connected"); } String name = identity.getName(); return name; } /** {@inheritDoc} */ public Set<Delivery> supportedDeliveries() { return deliveries; } /** {@inheritDoc} */ public int getMaxMessageLength() { return maxMessageLength; } /** {@inheritDoc} */ public boolean isConnected() { return connected; } /** {@inheritDoc} * * Enqueues a send event to this client session's event queue for servicing. */ public ClientSession send(ByteBuffer message) { return send(message, Delivery.RELIABLE); } /** {@inheritDoc} * * Enqueues a send event to this client session's event queue for servicing. */ public ClientSession send(ByteBuffer message, final Delivery delivery) { try { if (!isConnected()) { throw new IllegalStateException("client session not connected"); } else if (message == null) { throw new NullPointerException("null message"); } else if (message.remaining() > maxMessageLength) { throw new IllegalArgumentException( "message too long: " + message.remaining() + " > " + maxMessageLength); } else { checkDelivery(delivery); } /* * TBD: Possible optimization: if we have passed our own special * buffer to the app, we can detect that here and possibly avoid a * copy. Our special buffer could be one we passed to the * receivedMessage callback, or we could add a special API to * pre-allocate buffers. -JM */ final byte[] msgBytes = new byte[message.remaining()]; message.asReadOnlyBuffer().get(msgBytes); if (delivery.equals(Delivery.UNRELIABLE)) { // Forward unreliable message directly to client session's // server node. final ClientSessionServer server = sessionService.getClientSessionServer(nodeId); sessionService.taskService.scheduleNonDurableTask( new AbstractKernelRunnable("SendUnreliableMessage") { public void run() { try { server.send(idBytes, msgBytes, (byte) delivery.ordinal()); } catch (IOException e) { if (logger.isLoggable(Level.FINE)) { logger.logThrow( Level.FINE, e, "send message:{0} throws", HexDumper.format(msgBytes, 0x50)); } } } }, false); } else { // Enqueue reliable message for ordered delivery by the // client session's server node. addEvent(new SendEvent(msgBytes, delivery)); } return getWrappedClientSession(); } catch (RuntimeException e) { if (logger.isLoggable(Level.FINEST)) { logger.logThrow(Level.FINEST, e, "send message:{0} throws", HexDumper.format(message, 0x50)); } throw e; } } /** * Throws {@link DeliveryNotSupportedException} if the specified * {@code delivery} guarantee is not supported by any of this session's * delivery guarantees. * * @param delivery a delivery guarantee * @throws DeliveryNotSupportedException if the specified {@code * delivery} guarantee is not supported by any of this * session's delivery guarantees */ private void checkDelivery(Delivery delivery) { if (delivery == null) { throw new NullPointerException("null delivery"); } if (deliveries.contains(delivery)) { return; } for (Delivery d : deliveries) { if (d.supportsDelivery(delivery)) { return; } } throw new DeliveryNotSupportedException( "client session:" + this + " does not support the delivery guarantee", delivery); } /** * Initiates a relocation of this client session from the current node * to {@code newNodeId}. If the client session is no longer connected * or the session is already relocating then this request is ignored. * * @param newNodeId the ID of the node this session is relocating to */ void addMoveEvent(long newNodeId) { if (isConnected() && !isRelocating()) { addEvent(new MoveEvent(newNodeId)); } } /** * Sets the {@code relocatingToNode} field to the specified node ID. * This method is invoked when a move event is processed on the * original node to flag this client session as one that is * relocating. When relocation is complete, the {@link * #relocationComplete} method should be invoked on the client * session's new node to mark the client session as having moved. * * @param newNodeId the new node that client session is relocating to * @throws IllegalStateException if this method is invoked from a node * other than the session's local node */ private void setRelocatingToNode(long newNodeId) { if (!isLocalSession()) { throw new IllegalStateException( "'setRelocating' can only be invoked on the local node:" + nodeId + " for this session: " + toString()); } sessionService.getDataService().markForUpdate(this); relocatingToNode = newNodeId; } /** * Marks this client session as having completed relocation, and then * services this session's event queue. This method is invoked when * the associated client connects to the new node to re-establish the * client session. * * @throws IllegalStateException if this method is invoked from a node * other than the session's local node */ void relocationComplete() { if (relocatingToNode != sessionService.getLocalNodeId()) { throw new IllegalStateException( "'relocationComplete' can only be invoked on the local node:" + nodeId + " for this session: " + toString()); } sessionService.getDataService().markForUpdate(this); relocatingToNode = -1; getEventQueue().serviceEvent(); } /** * Returns {@code true} if the session is relocating, and {@code * false} otherwise. * * @return {@code true} if the session is relocating, and {@code * false} otherwise */ public boolean isRelocating() { return relocatingToNode != -1; } /** * Updates this client session's node ID and bindings to the client * session to reflect its reassignment to {@code newNodeId}. * * @param newNodeId the node this session is relocating to * @throws IllegalArgumentException if {@code newNodeId} does not match * the local node ID */ void move(long newNodeId) { if (newNodeId != sessionService.getLocalNodeId()) { throw new IllegalArgumentException( "newNodeId:" + newNodeId + " must match the local node ID:" + sessionService.getLocalNodeId()); } DataService dataService = sessionService.getDataService(); dataService.markForUpdate(this); dataService.removeServiceBinding(getSessionNodeKey()); nodeId = newNodeId; // TBD: this could use a BindingKeyedMap. dataService.setServiceBinding(getSessionNodeKey(), this); } /** * If the session is connected, enqueues a disconnect event to this * client session's event queue, and marks this session as disconnected. */ void disconnect() { if (isConnected()) { addEvent(new DisconnectEvent()); sessionService.getDataService().markForUpdate(this); connected = false; } logger.log(Level.FINEST, "disconnect returns"); } /* -- Implement NodeAssignment -- */ /** {@inheritDoc} */ public long getNodeId() { return nodeId; } /** {@inheritDoc} */ public long getRelocatingToNodeId() { return relocatingToNode; } /* -- Implement Object -- */ /** {@inheritDoc} */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } else if (obj != null && obj.getClass() == this.getClass()) { ClientSessionImpl session = (ClientSessionImpl) obj; return equalsInclNull(identity, session.identity) && equalsInclNull(id, session.id); } return false; } /** * Returns {@code true} if the given objects are either both * null, or both non-null and invoking {@code equals} on the first * object passing the second object returns {@code true}. */ private static boolean equalsInclNull(Object obj1, Object obj2) { if (obj1 == null) { return obj2 == null; } else if (obj2 == null) { return false; } else { return obj1.equals(obj2); } } /** {@inheritDoc} */ @Override public int hashCode() { return id.hashCode(); } /** {@inheritDoc} */ @Override public String toString() { return getClass().getName() + "[" + identity.getName() + "]@[id:0x" + id.toString(16) + ",node:" + nodeId + "]"; } /* -- Serialization methods -- */ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); sessionService = ClientSessionServiceImpl.getInstance(); this.id = new BigInteger(1, idBytes); } /* -- Other methods -- */ /** * Returns the ID of this instance as a {@code BigInteger}. * * @return the ID of this instance as a {@code BigInteger} */ BigInteger getId() { return id; } /** * Returns the {@code ClientSession} instance for the given {@code * id}, retrieved from the specified {@code dataService}, or * {@code null} if the client session isn't bound in the data * service. This method should only be called within a * transaction. * * @param dataService a data service * @param id a session ID * @return the session for the given session {@code id}, * or {@code null} * @throws TransactionException if there is a problem with the * current transaction */ static ClientSessionImpl getSession( DataService dataService, BigInteger id) { ClientSessionImpl sessionImpl = null; try { ManagedReference<?> sessionRef = dataService.createReferenceForId(id); sessionImpl = (ClientSessionImpl) sessionRef.get(); } catch (ObjectNotFoundException e) { } return sessionImpl; } /** * Returns the wrapped client session for this instance. * @return the wrapped client session */ public ClientSessionWrapper getWrappedClientSession() { return wrappedSessionRef.get(); } /** * Invokes the {@code disconnected} callback on this session's {@code * ClientSessionListener} (if present and {@code notify} is * {@code true}), removes the listener and its binding (if present), * and then removes this session and its bindings from the specified * {@code dataService}. If the bindings have already been removed from * the {@code dataService} this method takes no action. This method * should only be called within a transaction. * * @param dataService a data service * @param graceful {@code true} if disconnection is graceful, * and {@code false} otherwise * @param notify {@code true} if the {@code disconnected} * callback should be invoked * @throws TransactionException if there is a problem with the * current transaction */ void notifyListenerAndRemoveSession( final DataService dataService, final boolean graceful, boolean notify) { String sessionKey = getSessionKey(); String sessionNodeKey = getSessionNodeKey(); String listenerKey = getListenerKey(); String eventQueueKey = getEventQueueKey(); // Mark this session as disconnected. dataService.markForUpdate(this); connected = false; /* * Get ClientSessionListener, and remove its binding and * wrapper if applicable. The listener may not be bound * in the data service if: the AppListener.loggedIn callback * either threw a non-retryable exception or returned a * null listener, or the application removed the * ClientSessionListener object from the data service. */ ClientSessionListener listener = null; try { ManagedObject obj = dataService.getServiceBinding(listenerKey); dataService.removeServiceBinding(listenerKey); if (obj instanceof ListenerWrapper) { dataService.removeObject(obj); listener = ((ListenerWrapper) obj).get(); } else { listener = (ClientSessionListener) obj; } } catch (NameNotBoundException e) { logger.logThrow( Level.FINE, e, "removing ClientSessionListener for session:{0} throws", this); } /* * Remove event queue and associated binding. */ try { ManagedObject eventQueue = dataService.getServiceBinding(eventQueueKey); dataService.removeServiceBinding(eventQueueKey); dataService.removeObject(eventQueue); } catch (NameNotBoundException e) { logger.logThrow( Level.FINE, e, "removing EventQueue for session:{0} throws", this); } /* * Invoke listener's 'disconnected' callback if 'notify' * is true and a listener exists for this client session. If the * 'disconnected' callback throws a non-retryable exception, * schedule a task to remove this session and its associated * bindings without invoking the listener, and rethrow the * exception so that the currently executing transaction aborts. */ if (notify && listener != null) { try { listener.disconnected(graceful); } catch (RuntimeException e) { if (!isRetryableException(e)) { logger.logThrow( Level.WARNING, e, "invoking disconnected callback on listener:{0} " + "for session:{1} throws", listener, this); sessionService.scheduleTask( new AbstractKernelRunnable( "NotifyListenerAndRemoveSession") { public void run() { ClientSessionImpl sessionImpl = ClientSessionImpl.getSession( dataService, id); sessionImpl.notifyListenerAndRemoveSession( dataService, graceful, false); } }, identity); } throw e; } } /* * Remove this session's state and bindings. */ try { dataService.removeServiceBinding(sessionKey); dataService.removeServiceBinding(sessionNodeKey); dataService.removeObject(this); } catch (NameNotBoundException e) { logger.logThrow( Level.WARNING, e, "session binding already removed:{0}", sessionKey); } /* * Remove this session's wrapper object, if it still exists. */ try { dataService.removeObject(wrappedSessionRef.get()); } catch (ObjectNotFoundException e) { // already removed } } /** * Returns the {@code ClientSessionServer} for this instance. */ private ClientSessionServer getClientSessionServer() { return sessionService.getClientSessionServer(nodeId); } /** * Returns the key to access this instance from the data service. * * @return a key for accessing this {@code ClientSessionImpl} instance */ private String getSessionKey() { return PKG_NAME + SESSION_COMPONENT + HexDumper.toHexString(idBytes); } /** * Returns the key to access from the data service the {@code * ClientSessionListener} instance for this instance. If the {@code * ClientSessionListener} does not implement {@code ManagedObject}, * then the key will be bound to a {@code ListenerWrapper}. * * @return a key for accessing the {@code ClientSessionListener} instance */ private String getListenerKey() { return PKG_NAME + LISTENER_COMPONENT + HexDumper.toHexString(idBytes); } /** * Returns the key to access the event queue of the session with the * specified {@code sessionId}. */ private static String getEventQueueKey(byte[] sessionId) { return PKG_NAME + QUEUE_COMPONENT + HexDumper.toHexString(sessionId); } /** * Returns the key to access this session's event queue. */ private String getEventQueueKey() { return getEventQueueKey(idBytes); } /** * Returns the key to access this instance from the data service (by * {@code nodeId} and session {@code idBytes}). * * @return a key for accessing the {@code ClientSessionImpl} instance */ private String getSessionNodeKey() { return getNodePrefix(nodeId) + HexDumper.toHexString(idBytes); } /** * Returns the prefix to access from the data service {@code * ClientSessionImpl} instances with the the specified {@code nodeId}. */ private static String getNodePrefix(long nodeId) { return PKG_NAME + NODE_COMPONENT + nodeId + "."; } /** * Stores the specified client session listener in the specified * {@code dataService} with following binding: * <pre> * com.sun.sgs.impl.service.session.listener.<idBytes> * </pre> * This method should only be called within a transaction. * * @param dataService a data service * @param listener a client session listener * @throws TransactionException if there is a problem with the * current transaction */ void putClientSessionListener( DataService dataService, ClientSessionListener listener) { ManagedObject managedObject = (listener instanceof ManagedObject) ? (ManagedObject) listener : new ListenerWrapper(listener); String listenerKey = getListenerKey(); // TBD: this could use a BindingKeyedMap. dataService.setServiceBinding(listenerKey, managedObject); } /** * Returns the client session listener, obtained from the * specified {@code dataService}, for this session. This method * should only be called within a transaction. * * @param dataService a data service * @return the client session listener for this session * @throws TransactionException if there is a problem with the * current transaction */ ClientSessionListener getClientSessionListener(DataService dataService) { String listenerKey = getListenerKey(); ManagedObject obj = dataService.getServiceBinding(listenerKey); return (obj instanceof ListenerWrapper) ? ((ListenerWrapper) obj).get() : (ClientSessionListener) obj; } /** * A {@code ManagedObject} wrapper for a {@code ClientSessionListener}. */ private static class ListenerWrapper implements ManagedObject, Serializable { private static final long serialVersionUID = 1L; private ClientSessionListener listener; ListenerWrapper(ClientSessionListener listener) { assert listener != null && listener instanceof Serializable; this.listener = listener; } ClientSessionListener get() { return listener; } } /** * Returns the event queue for the client session with the specified * {@code sessionId}, or null if the event queue is not bound in the * data service. */ private static EventQueue getEventQueue(byte[] sessionId) { DataService dataService = ClientSessionServiceImpl.getInstance().getDataService(); String eventQueueKey = getEventQueueKey(sessionId); try { return (EventQueue) dataService.getServiceBinding(eventQueueKey); } catch (NameNotBoundException e) { return null; } } /** * Returns this client session's event queue, or null if the event * queue is not bound in the data service. */ private EventQueue getEventQueue() { return getEventQueue(idBytes); } /** * Returns {@code true} if session is on local node and is not * relocating, and returns {@code false} otherwise. */ private boolean isLocalSession() { return nodeId == sessionService.getLocalNodeId() && relocatingToNode == -1; } /** * Adds the specified session {@code event} to this session's event * queue and notifies the client session service on the session's node * that there is an event to service. */ private void addEvent(SessionEvent event) { EventQueue eventQueue = getEventQueue(); if (eventQueue == null) { throw new IllegalStateException( "event queue removed; session is disconnected"); } boolean isLocalSession = isLocalSession(); /* * If this session is connected to the local node, the event queue * is empty, and the session is not relocating, then service the * event immediately without adding it to the event queue. * * Otherwise, add the event to the event queue. If the session is * not relocating, then if the session is connected locally service * the head of the event queue, otherwise schedule a task to send a * request to this session's client session server to service this * session's event queue. * * If the session is relocating, then the servicing of events will * resume when the client connects to the new node to re-establish * the client session. */ if (isLocalSession && eventQueue.isEmpty() && !isRelocating()) { logger.log(Level.FINEST, "immediately processing event:{0}", event); event.serviceEvent( eventQueue, sessionService, sessionService.getHandler(eventQueue.getSessionRefId())); } else if (!eventQueue.offer(event)) { throw new ResourceUnavailableException( "not enough resources to add client session event"); } else if (!isRelocating()) { if (isLocalSession) { eventQueue.serviceEvent(); } else { final ClientSessionServer sessionServer = getClientSessionServer(); if (sessionServer == null) { /* * If the ClientSessionServer for this session has been * removed, then this session's node has failed and the * session has been disconnected. The event queue will be * cleaned up eventually, so there is no need to flag an * error here. */ return; } sessionService.getTaskScheduler().scheduleTask( new AbstractKernelRunnable("ServiceEventQueue") { public void run() { sessionService.runIoTask( new IoRunnable() { public void run() throws IOException { sessionServer.serviceEventQueue(idBytes); } }, nodeId); } }, identity); } } } /** * Services the event queue for the session with the specified {@code * sessionId}. */ static void serviceEventQueue(byte[] sessionId) { EventQueue eventQueue = getEventQueue(sessionId); if (eventQueue != null) { eventQueue.serviceEvent(); } } /** * Returns the write buffer capacity for this session. * * @return the write buffer capacity */ int getWriteBufferCapacity() { return writeBufferCapacity; } /** * Represents an event for a client session. */ private abstract static class SessionEvent implements ManagedObject, Serializable { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** * Services this event, taken from the head of the given {@code * eventQueue}. */ abstract void serviceEvent(EventQueue eventQueue, ClientSessionServiceImpl sessionService, ClientSessionHandler handler); /** * Returns the cost of this event, which the {@code EventQueue} may * use to reject events when the total cost is too large. The cost * of the event is the size (in bytes) of a message generated as a * result of processing the event. <p> * * The default implementation returns a cost of zero. * * @return the cost of this event */ int getCost() { return 0; } } /** * A client session 'send' event, enqueued by a {@code * ClientSession.send} invocation. When this event commits, a task is * scheduled to deliver the message, specified during construction, to * the client session. */ static class SendEvent extends SessionEvent { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; final byte[] message; final Delivery delivery; /** * Constructs a send event with the given {@code message}. */ SendEvent(byte[] message, Delivery delivery) { this.message = message; this.delivery = delivery; } /** {@inheritDoc} */ void serviceEvent(EventQueue eventQueue, ClientSessionServiceImpl sessionService, ClientSessionHandler handler) { if (eventQueue == null) { throw new NullPointerException("null eventQueue"); } else if (sessionService == null) { throw new NullPointerException("null sessionService"); } else if (handler == null) { throw new NullPointerException("null handler"); } sessionService.checkContext().addCommitAction( eventQueue.getSessionRefId(), handler.new SendMessageAction(this), false); } /** Use the message length as the cost for sending messages. */ @Override int getCost() { return message.length; } /** {@inheritDoc} */ @Override public String toString() { return getClass().getName(); } } /** * A client session 'move' event. This event is processed on the * client session's old node to mark the session as relocating. Once * the session is marked for relocation, the associated session's event * processing is suspended until the session is relocated to the new * node. <p> * * When this event commits, a task is scheduled to commence preparation * for the client session to relocate. The first action is to obtain a * relocation key from the new node and notify interested parties to * prepare for relocation. See {@link ClientSessionHandler#MoveAction} * for details. */ private static class MoveEvent extends SessionEvent { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; private final long newNodeId; /** Constructs a move event. */ MoveEvent(long newNodeId) { this.newNodeId = newNodeId; } /** {@inheritDoc} */ void serviceEvent(EventQueue eventQueue, ClientSessionServiceImpl sessionService, ClientSessionHandler handler) { ClientSessionImpl sessionImpl = eventQueue.getClientSession(); sessionImpl.setRelocatingToNode(newNodeId); Node newNode = sessionService.watchdogService.getNode(newNodeId); if (newNode == null) { if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "Session:{0} unable to relocate from node:{1} " + "to FAILED node:{2}", this, sessionService.getLocalNodeId(), newNodeId); } } else if (handler == null) { if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "DISCONNECTED Session:{0} unable to relocate " + "from node:{1} to node:{2}", this, sessionService.getLocalNodeId(), newNodeId); } } else { if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "Session:{0} to relocate " + "from node:{1} to node:{2}", this, sessionService.getLocalNodeId(), newNodeId); } sessionService.checkContext().addCommitAction( eventQueue.getSessionRefId(), handler.new MoveAction(newNode), false); } } } /** * A client session 'disconnect' event, enqueued when the session's * associated {@code ClientSessionWrapper} is removed, or if there is a * problem during client login, after the client session has been * persisted. <p> * * When this event commits, a task is scheduled to disconnect the * client session, cleanup its persistent data, and notify the client * session's listener of the disconnection. */ private static class DisconnectEvent extends SessionEvent { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** Constructs a disconnect event. */ DisconnectEvent() { } /** {@inheritDoc} */ void serviceEvent(EventQueue eventQueue, ClientSessionServiceImpl sessionService, ClientSessionHandler handler) { sessionService.checkContext().addCommitAction( eventQueue.getSessionRefId(), handler.new DisconnectAction(), false); } /** {@inheritDoc} */ @Override public String toString() { return getClass().getName(); } } /** * The session's event queue. */ private static class EventQueue implements ManagedObjectRemoval, Serializable { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** The managed reference to the queue's session. */ private final ManagedReference<ClientSessionImpl> sessionRef; /** The managed reference to the managed queue. */ private final ManagedReference<ManagedQueue<SessionEvent>> queueRef; /** The number of bytes of the write buffer currently available. */ private int writeBufferAvailable; /** * Constructs an event queue for the specified {@code sessionImpl}. */ EventQueue(ClientSessionImpl sessionImpl) { DataService dataService = sessionImpl.sessionService.getDataService(); sessionRef = dataService.createReference(sessionImpl); queueRef = dataService.createReference( new ManagedQueue<SessionEvent>()); writeBufferAvailable = sessionImpl.writeBufferCapacity; } /** * Attempts to enqueue the specified {@code event}, and returns * {@code true} if successful, and {@code false} otherwise. * * @param event the event * @return {@code true} if successful, and {@code false} otherwise * @throws MessageRejectedException if the cost of the event * exceeds the available buffer space in the queue */ boolean offer(SessionEvent event) { int cost = event.getCost(); if (cost > writeBufferAvailable) { throw new MessageRejectedException( "Not enough queue space: " + writeBufferAvailable + " bytes available, " + cost + " requested"); } if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, "Adding event:{0} to event queue, localNodeId:{1}", event, ClientSessionServiceImpl.getInstance().getLocalNodeId()); } boolean success = getQueue().offer(event); if (success && cost > 0) { ClientSessionServiceImpl.getInstance(). getDataService().markForUpdate(this); writeBufferAvailable -= cost; if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "{0} reserved {1,number,#} leaving {2,number,#}", this, cost, writeBufferAvailable); } } return success; } /** * Returns the client session for this queue. */ ClientSessionImpl getClientSession() { return sessionRef.get(); } /** * Returns the client session ID for this queue. */ BigInteger getSessionRefId() { return sessionRef.getId(); } /** * Returns the managed queue object. */ ManagedQueue<SessionEvent> getQueue() { return queueRef.get(); } /** * Returns {@code true} if the event queue is empty. */ boolean isEmpty() { return getQueue().isEmpty(); } /** * Throws a retryable exception if the event queue is not in a * state to process the next event. */ void checkState() { // TBD: is there any state to check here? } /** * Processes (at least) the first event in the queue. */ void serviceEvent() { checkState(); ClientSessionServiceImpl sessionService = ClientSessionServiceImpl.getInstance(); ClientSessionHandler handler = sessionService.getHandler(getSessionRefId()); ClientSessionImpl sessionImpl = getClientSession(); if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "Servicing event queue, node:{0} session:{1}", sessionService.getLocalNodeId(), getSessionRefId()); } if (handler == null || !sessionImpl.isLocalSession() || sessionImpl.isRelocating()) { // Only service events on the session's local node, so return. // The session may be moving, and this might be a left over // serviceEventQueue request if (logger.isLoggable(Level.FINE)) { logger.log( Level.FINE, "Attempt to service event queue, localNodeId:{0} " + "session:{1} handler:{2} sessionNodeId:{3} " + "relocatingToNodeId:{4}", sessionService.getLocalNodeId(), getSessionRefId(), handler, sessionImpl.nodeId, sessionImpl.relocatingToNode); } return; } ManagedQueue<SessionEvent> eventQueue = getQueue(); DataService dataService = ClientSessionServiceImpl.getInstance().getDataService(); for (int i = 0; i < sessionService.eventsPerTxn; i++) { SessionEvent event = eventQueue.poll(); if (event == null) { // no more events // TBD: should the session's task queue for servicing // events be cleared? return; } logger.log(Level.FINEST, "processing event:{0}", event); int cost = event.getCost(); if (cost > 0) { // TBD: this update is costly. dataService.markForUpdate(this); writeBufferAvailable += cost; if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "{0} cleared reservation of " + "{1,number,#} bytes, leaving {2,number,#}", this, cost, writeBufferAvailable); } } event.serviceEvent(this, sessionService, handler); } // Make sure the next event gets serviced. if (eventQueue.peek() != null) { sessionService.addServiceEventQueueTask(sessionImpl.idBytes); } } /* -- Implement ManagedObjectRemoval -- */ /** {@inheritDoc} */ public void removingObject() { try { DataService dataService = ClientSessionServiceImpl.getInstance().getDataService(); dataService.removeObject(queueRef.get()); } catch (ObjectNotFoundException e) { // already removed. } } } /** * A persistent task to schedule tasks to notify (in succession) the * client session listener of each disconnected session on a given * failed node and to clean up the persistent data and bindings of * those client sessions. In a single task, one disconnected session * is scheduled to be handled, and then this task is rescheduled to * schedule the handling of the next disconnected client session (if * one exists). */ static class HandleNextDisconnectedSessionTask implements Task, Serializable { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** The prefix for client sessions on the failed node. */ private final String nodePrefix; /** The last session key handled, initially the {@code nodePrefix}. */ private String lastKey; /** * Constructs an instance of this class with the specified * {@code nodeId}. */ HandleNextDisconnectedSessionTask(long nodeId) { nodePrefix = getNodePrefix(nodeId); lastKey = nodePrefix; } /** {@inheritDoc} */ public void run() { DataService dataService = ClientSessionServiceImpl.getInstance().getDataService(); // TBD: this could use a BindingKeyedMap. String key = dataService.nextServiceBoundName(lastKey); if (key != null && key.startsWith(nodePrefix)) { TaskService taskService = ClientSessionServiceImpl.getTaskService(); taskService.scheduleTask( new CleanupDisconnectedSessionTask(key)); lastKey = key; taskService.scheduleTask(this); } } } /** * A persistent task to clean up a client session bound to a * given {@code key} (specified during construction), by * invoking the {@code notifyListenerAndRemoveSession} method * on that client session. */ private static class CleanupDisconnectedSessionTask implements Task, Serializable { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** The key for the client session. */ private final String key; /** * Constructs an instance of this class with the specified * {@code key}. */ CleanupDisconnectedSessionTask(String key) { this.key = key; } /** {@inheritDoc} */ public void run() { DataService dataService = ClientSessionServiceImpl.getInstance().getDataService(); ClientSessionImpl sessionImpl = (ClientSessionImpl) dataService.getServiceBinding(key); sessionImpl.notifyListenerAndRemoveSession( dataService, false, true); } } }