/*
* 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.AppListener;
import com.sun.sgs.app.ClientSessionListener;
import com.sun.sgs.app.ManagedObject;
import com.sun.sgs.app.util.ManagedSerializable;
import com.sun.sgs.auth.Identity;
import com.sun.sgs.impl.service.session.ClientSessionImpl.SendEvent;
import com.sun.sgs.impl.service.session.ClientSessionServiceImpl.Action;
import com.sun.sgs.impl.kernel.StandardProperties;
import com.sun.sgs.impl.sharedutil.LoggerWrapper;
import static com.sun.sgs.impl.sharedutil.Objects.checkNull;
import com.sun.sgs.impl.util.AbstractCompletionFuture;
import com.sun.sgs.impl.util.AbstractKernelRunnable;
import static com.sun.sgs.impl.util.AbstractService.isRetryableException;
import com.sun.sgs.kernel.KernelRunnable;
import com.sun.sgs.kernel.TaskQueue;
import com.sun.sgs.protocol.LoginFailureException;
import com.sun.sgs.protocol.LoginRedirectException;
import com.sun.sgs.protocol.ProtocolDescriptor;
import com.sun.sgs.protocol.RelocateFailureException;
import com.sun.sgs.protocol.RequestCompletionHandler;
import com.sun.sgs.protocol.RequestFailureException;
import com.sun.sgs.protocol.SessionProtocol;
import com.sun.sgs.protocol.SessionProtocolHandler;
import com.sun.sgs.protocol.SessionRelocationProtocol;
import com.sun.sgs.service.ClientSessionStatusListener;
import com.sun.sgs.service.DataService;
import com.sun.sgs.service.Node;
import com.sun.sgs.service.SimpleCompletionHandler;
import java.io.IOException;
import java.io.Serializable;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Handles sending/receiving messages to/from a client session and
* disconnecting a client session.
*/
class ClientSessionHandler implements SessionProtocolHandler {
/** Connection state. */
private static enum State {
/** Session is connected */
CONNECTED,
/** Session login ack (success, failure, redirect) has been sent */
LOGIN_HANDLED,
/** Disconnection is in progress */
DISCONNECTING,
/** Session is disconnected */
DISCONNECTED
}
/** The logger for this class. */
private static final LoggerWrapper logger =
new LoggerWrapper(Logger.getLogger(
"com.sun.sgs.impl.service.session.handler"));
/** Message indicating login was refused for a non-specific reason. */
private static final String LOGIN_REFUSED_REASON = "Login refused";
/** Message indicating relocation was refused for a non-specific reason. */
static final String RELOCATE_REFUSED_REASON = "Relocate refused";
/** The client session service that created this client session. */
private final ClientSessionServiceImpl sessionService;
/** The data service. */
private final DataService dataService;
/** The I/O channel for sending messages to the client. */
private final SessionProtocol protocol;
/** The session ID as a BigInteger. */
volatile BigInteger sessionRefId;
/** The identity for this session. */
final Identity identity;
/** The login status. */
private volatile boolean loggedIn;
/** The lock for accessing the following fields: {@code state},
* {@code disconnectHandled}, {@code relocatePrepareCompletionHandler},
* and {@code shutdown}.
*/
private final Object lock = new Object();
/** The connection state. */
private State state = State.CONNECTED;
/** Indicates whether session disconnection has been handled. */
private boolean disconnectHandled = false;
/** If non-null, contains the completion handler for
* preparing this session to relocate to a new node. */
private MoveAction relocatePrepareCompletionHandler = null;
/** Indicates whether this session is shut down. */
private boolean shutdown = false;
/** Completion future for setting up the client session. */
private final SetupCompletionFuture setupCompletionFuture;
/** The queue of tasks for notifying listeners of received messages. */
private volatile TaskQueue taskQueue = null;
/**
* Constructs an handler for a client session that is logging in.
*
* @param sessionService the ClientSessionService instance
* @param dataService the DataService instance
* @param sessionProtocol a session protocol
* @param identity an identity
* @param completionHandler a completion handler for the associated
* request
*/
ClientSessionHandler(
ClientSessionServiceImpl sessionService,
DataService dataService,
SessionProtocol sessionProtocol,
Identity identity,
RequestCompletionHandler<SessionProtocolHandler> completionHandler)
{
this(sessionService, dataService, sessionProtocol, identity,
completionHandler, null);
}
/**
* Constructs an handler for a client session. If {@code sessionRefId}
* is non-{@code null}, then the associated client session is relocating
* from another node, otherwise it is considered a new client session
* logging in.
*
* @param sessionService the ClientSessionService instance
* @param dataService the DataService instance
* @param sessionProtocol a session protocol
* @param identity an identity
* @param completionHandler a completion handler for the associated
* request
* @param sessionRefId the client session ID, or {@code null}
*/
ClientSessionHandler(
ClientSessionServiceImpl sessionService,
DataService dataService,
SessionProtocol sessionProtocol,
Identity identity,
RequestCompletionHandler<SessionProtocolHandler> completionHandler,
BigInteger sessionRefId)
{
checkNull("sessionService", sessionService);
checkNull("dataService", dataService);
checkNull("sessionProtocol", sessionProtocol);
checkNull("identity", identity);
checkNull("completionHandler", completionHandler);
this.sessionService = sessionService;
this.dataService = dataService;
this.protocol = sessionProtocol;
this.identity = identity;
this.sessionRefId = sessionRefId;
setupCompletionFuture =
new SetupCompletionFuture(this, completionHandler);
final boolean loggingIn = sessionRefId == null;
scheduleNonTransactionalTask(
new AbstractKernelRunnable("HandleLoginOrRelocateRequest") {
public void run() {
setupClientSession(loggingIn);
} });
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST,
"creating new ClientSessionHandler on nodeId:{0}",
sessionService.getLocalNodeId());
}
}
/* -- Implement SessionProtocolHandler -- */
/** {@inheritDoc} */
public void sessionMessage(
final ByteBuffer message,
RequestCompletionHandler<Void> completionHandler)
{
RequestCompletionFuture future =
new RequestCompletionFuture(completionHandler);
if (!readyForRequests(future)) {
return;
}
taskQueue.addTask(
new AbstractKernelRunnable("NotifyListenerMessageReceived") {
public void run() {
ClientSessionImpl sessionImpl =
ClientSessionImpl.getSession(dataService, sessionRefId);
if (sessionImpl != null) {
if (isConnected()) {
sessionImpl.getClientSessionListener(dataService).
receivedMessage(
message.asReadOnlyBuffer());
}
} else {
scheduleHandleDisconnect(false, true);
}
} }, identity);
// Wait until processing is complete before notifying future
enqueueCompletionNotification(future);
}
/** {@inheritDoc} */
public void channelMessage(final BigInteger channelId,
final ByteBuffer message,
RequestCompletionHandler<Void> completionHandler)
{
RequestCompletionFuture future =
new RequestCompletionFuture(completionHandler);
if (!readyForRequests(future)) {
return;
}
taskQueue.addTask(
new AbstractKernelRunnable("HandleChannelMessage") {
public void run() {
ClientSessionImpl sessionImpl =
ClientSessionImpl.getSession(dataService, sessionRefId);
if (sessionImpl != null) {
if (isConnected()) {
sessionService.getChannelService().
handleChannelMessage(
channelId,
sessionImpl.getWrappedClientSession(),
message.asReadOnlyBuffer());
}
} else {
scheduleHandleDisconnect(false, true);
}
} }, identity);
// Wait until processing is complete before notifying future
enqueueCompletionNotification(future);
}
/** {@inheritDoc} */
public void logoutRequest(
RequestCompletionHandler<Void> completionHandler)
{
RequestCompletionFuture future =
new RequestCompletionFuture(completionHandler);
if (!readyForRequests(future)) {
return;
}
scheduleHandleDisconnect(isConnected(), false);
// Enable protocol message channel to read immediately
future.done();
}
/** {@inheritDoc} */
public void disconnect(RequestCompletionHandler<Void> completionHandler) {
RequestCompletionFuture future =
new RequestCompletionFuture(completionHandler);
// TBD: should this be allowed to disconnect no matter what?
if (!readyForRequests(future)) {
return;
}
scheduleHandleDisconnect(false, true);
future.done();
}
/* -- Implement Object -- */
/** {@inheritDoc} */
public String toString() {
return getClass().getName() + "[" + identity + "]@" + sessionRefId;
}
/* -- Instance methods -- */
/**
* Returns {@code true} if this handler is connected, otherwise
* returns {@code false}.
*
* @return {@code true} if this handler is connected
*/
boolean isConnected() {
State currentState = getCurrentState();
return
currentState == State.CONNECTED ||
currentState == State.LOGIN_HANDLED;
}
/**
* Returns {@code true} if this session's protocol handler supports
* relocation (i.e., implements the {@link SessionRelocationProtocol}
* interface; otherwise returns {@code false}.
*/
boolean supportsRelocation() {
return protocol instanceof SessionRelocationProtocol;
}
/**
* Returns {@code true} if this client session has begun preparing to
* relocate, or has relocated to another node.
*/
boolean isRelocating() {
synchronized (lock) {
return relocatePrepareCompletionHandler != null;
}
}
/**
* Returns {@code true} if this client session is disconnecting from
* the local node AND terminating the associated client session. The
* client session is considered to be disconnecting iff the following
* conditions are true:
*
* 1) this handler has been marked for disconnection
* 2) the session is not relocating, OR, the session is relocating
* and its relocation has not completed.
*/
private boolean isTerminating() {
synchronized (lock) {
return !isConnected() &&
(relocatePrepareCompletionHandler == null ||
!relocatePrepareCompletionHandler.isCompleted());
}
}
/**
* Indicates that all parties are done with relocation preparation, and
* notifies the client that it should suspend messages (before
* notifying the client to relocate to another node).
*/
void setRelocatePreparationComplete() {
synchronized (lock) {
if (relocatePrepareCompletionHandler != null) {
relocatePrepareCompletionHandler.suspend();
}
}
}
/**
* Returns {@code true} if the associated client session is ready for
* requests (i.e., it has completed login and it is not relocating);
* otherwise, sets the appropriate {@code RequestFailureException} on
* the specified {@code future} and returns {@code false}. <p>
*
* This method is invoked before proceeding with processing a
* request, and if this method returns {@code false}, the request
* should be dropped.
*
* @param future a future on which to set an exception if this
* session is not ready to process requests
* @return {@code true} if requests can be processed by the
* associated client session and {@code false} otherwise
*/
private boolean readyForRequests(RequestCompletionFuture future) {
if (!loggedIn) {
logger.log(
Level.FINE,
"request received before login completed:{0}", this);
future.setException(
new RequestFailureException(
"session is not logged in",
RequestFailureException.FailureReason.LOGIN_PENDING));
return false;
} else if (relocatePrepareCompletionHandler != null &&
relocatePrepareCompletionHandler.isCompleted()) {
logger.log(
Level.FINE,
"request received while session is relocating:{0}", this);
future.setException(
new RequestFailureException(
"session is relocating",
RequestFailureException.FailureReason.RELOCATE_PENDING));
return false;
} else if (!isConnected()) {
logger.log(
Level.FINE,
"request received while session is disconnecting:{0}", this);
future.setException(
new RequestFailureException(
"session is disconnecting",
RequestFailureException.FailureReason.DISCONNECT_PENDING));
return false;
} else {
return true;
}
}
/**
* Returns the protocol for the associated client session, or {@code
* null} if the session is relocating.
*
* @return a protocol, or {@code null} if the session is relocating
*/
SessionProtocol getSessionProtocol() {
return isRelocating() ? null : protocol;
}
/**
* Returns {@code true} if the login for this session has been handled,
* otherwise returns {@code false}.
*
* @return {@code true} if the login for this session has been handled
*/
boolean loginHandled() {
return getCurrentState() != State.CONNECTED;
}
/**
* Notifies the "setup" future that the setup was successful so that it can
* send the appropriate success indication (either successful login
* or relocation) to the client, and sets local state indicating that
* the request has been handled and the client is logged in.
*/
private void setupSuccess() {
synchronized (lock) {
checkConnectedState();
loggedIn = true;
setupCompletionFuture.done();
state = State.LOGIN_HANDLED;
}
}
/**
* Notifies the "setup" future that setup failed with the specified
* {@code exception} so that it can send the appropriate failure
* indication to the client, and sets local state indicating that
* request has been handled.
*
* @param exception the login failure exception
*/
private void setupFailure(Exception exception) {
checkNull("exception", exception);
synchronized (lock) {
checkConnectedState();
setupCompletionFuture.setException(exception);
state = State.LOGIN_HANDLED;
}
}
/**
* Handles disconnecting the associated client session (if not already
* handled) by doing the following: <ol>
*
* <li> notifies the client session service to clean up the client
* session's handler and login information,
*
* <li> notifies the node mapping service to deativate the client's
* identity if the identity is no longer active on this node,
*
* <li> if {@code closeConnection} is {@code true}, closes this
* session's connection,
*
* <li> if the session is terminating (not relocating), schedules a
* transactional task to invoke, on this session's {@code
* ClientSessionListener}, the {@code disconnected} callback
* with {@code graceful} as its argument and then clean up the
* session's persistent data, and also schedules a task to notify
* the identity that its corresponding session has logged out.
* </ol>
*
* <p>Note:if {@code graceful} is {@code true}, then {@code
* closeConnection} must be {@code false} so that the client's {@code
* SessionProtocol} can send a notification of logout success to the
* client. The client may not receive such a notification if the
* connection is disconnected immediately.
*
* <p>In the cases of login redirection, session relocation, and
* graceful logout, it is the responsibility of the client's {@code
* SessionProtocol} to close the client's connection in a timely manner
* after notifying the client.
*
* @param graceful if {@code true}, indicates that disconnection is
* due to a (graceful) logout request
* @param closeConnection if {@code true}, close this session's
* connection immediately
*/
void handleDisconnect(final boolean graceful, boolean closeConnection) {
synchronized (lock) {
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "handleDisconnect handler:{0} " +
"disconnectHandled:{1}", this, disconnectHandled);
}
if (disconnectHandled) {
return;
}
disconnectHandled = true;
if (state != State.DISCONNECTED) {
state = State.DISCONNECTING;
}
}
if (sessionRefId != null) {
sessionService.removeHandler(sessionRefId, !isTerminating());
}
if (sessionService.removeUserLogin(identity, this)) {
deactivateIdentity();
}
if (getCurrentState() != State.DISCONNECTED) {
if (graceful) {
assert !closeConnection;
}
if (closeConnection) {
closeConnection();
}
}
if (sessionRefId != null && isTerminating()) {
scheduleTask(
new AbstractKernelRunnable("NotifyListenerAndRemoveSession") {
public void run() {
ClientSessionImpl sessionImpl =
ClientSessionImpl.getSession(dataService, sessionRefId);
sessionImpl.notifyListenerAndRemoveSession(
dataService, graceful, true);
}
});
// TBD: Due to the scheduler's behavior, this notification
// may happen out of order with respect to the
// 'notifyLoggedIn' callback. Also, this notification may
// also happen even though 'notifyLoggedIn' was not invoked.
// Are these behaviors okay? -- ann (3/19/07)
scheduleTask(new AbstractKernelRunnable("NotifyLoggedOut") {
public void run() {
identity.notifyLoggedOut();
} });
}
}
/**
* Schedules a non-transactional task to handle disconnecting the
* associated client session.
*
* @param graceful if {@code true}, indicates that disconnection is
* due to a (graceful) logout request
* @param closeConnection if {@code true}, close this session's
* connection immediately
*/
private void scheduleHandleDisconnect(
final boolean graceful, final boolean closeConnection)
{
synchronized (lock) {
if (disconnectHandled) {
return;
}
if (state != State.DISCONNECTED) {
state = State.DISCONNECTING;
}
}
scheduleNonTransactionalTask(
new AbstractKernelRunnable("HandleDisconnect") {
public void run() {
handleDisconnect(graceful, closeConnection);
} });
}
/**
* Closes the connection associated with this instance.
*/
private void closeConnection() {
if (protocol.isOpen()) {
try {
protocol.close();
} catch (IOException e) {
if (logger.isLoggable(Level.WARNING)) {
logger.logThrow(
Level.WARNING, e,
"closing connection for handle:{0} throws",
protocol);
}
}
}
synchronized (lock) {
state = State.DISCONNECTED;
}
}
/**
* Flags this session as shut down, and closes the connection.
*/
void shutdown() {
synchronized (lock) {
if (shutdown) {
return;
}
shutdown = true;
disconnectHandled = true;
closeConnection();
}
}
/* -- other private methods and classes -- */
/**
* Schedules a task to notify the completion handler. Use this method
* to delay notification until a task resulting from an earlier request
* has been completed.
*
* @param completionHandler a completion handler
* @param future a completion future
*/
private void enqueueCompletionNotification(
final RequestCompletionFuture future)
{
taskQueue.addTask(
new AbstractKernelRunnable("ScheduleCompletionNotification") {
public void run() {
future.done();
} }, identity);
}
/**
* Invokes the {@code setStatus} method on the node mapping service
* with {@code false} to mark the identity as inactive. This method
* is invoked when a login is redirected and also when this client
* session is disconnected.
*/
private void deactivateIdentity() {
try {
/*
* Set identity's status for this class to 'false'.
*/
sessionService.nodeMapService.setStatus(
ClientSessionHandler.class, identity, false);
} catch (Exception e) {
logger.logThrow(
Level.WARNING, e,
"setting status for identity:{0} throws",
identity.getName());
}
}
/**
* Returns the current state.
*/
private State getCurrentState() {
State currentState;
synchronized (lock) {
currentState = state;
}
return currentState;
}
/**
* If {@code loggingIn} is {@code true} handles a login request to
* establish a client session); otherwise handles a relocate request
* to re-establish a client session. In either case, this method
* notifies the completion handler specified during construction when
* the request is completed.
*/
private void setupClientSession(final boolean loggingIn) {
logger.log(
Level.FINEST,
"setting up client session for identity:{0} loggingIn:{1}",
identity, loggingIn);
/*
* Get node assignment.
*/
long assignedNodeId = -1L;
try {
assignedNodeId = sessionService.nodeMapService.assignNode(
ClientSessionHandler.class, identity);
} catch (Exception e) {
logger.logThrow(
Level.WARNING, e,
"getting node assignment for identity:{0} throws", identity);
}
if (assignedNodeId < 0) {
logger.log(Level.WARNING,
"getting node assignment for identity:{0} failed",
identity);
notifySetupFailureAndDisconnect(
loggingIn ?
new LoginFailureException(
LOGIN_REFUSED_REASON,
LoginFailureException.FailureReason.SERVER_UNAVAILABLE) :
new RelocateFailureException(
RELOCATE_REFUSED_REASON,
RelocateFailureException.FailureReason.SERVER_UNAVAILABLE));
return;
}
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "identity:{0} assigned to node:{1}",
identity, assignedNodeId);
}
if (assignedNodeId == sessionService.getLocalNodeId()) {
/*
* Handle this login (or relocation) request locally. First,
* validate that the user is allowed to log in (or relocate).
*/
if (!sessionService.validateUserLogin(
identity, ClientSessionHandler.this, loggingIn))
{
// This client session is not allowed to proceed.
if (logger.isLoggable(Level.FINE)) {
logger.log(
Level.FINE,
"{0} rejected to node:{1} identity:{2}",
(loggingIn ? "User login" : "Session relocation"),
sessionService.getLocalNodeId(), identity);
}
notifySetupFailureAndDisconnect(
loggingIn ?
new LoginFailureException(
LOGIN_REFUSED_REASON,
LoginFailureException.FailureReason.DUPLICATE_LOGIN) :
new RelocateFailureException(
RELOCATE_REFUSED_REASON,
RelocateFailureException.
FailureReason.DUPLICATE_LOGIN));
return;
}
taskQueue = sessionService.createTaskQueue();
/*
* If logging in, store the client session in the data store.
*/
if (loggingIn) {
CreateClientSessionTask createTask =
new CreateClientSessionTask();
try {
sessionService.runTransactionalTask(createTask, identity);
} catch (Exception e) {
logger.logThrow(
Level.WARNING, e,
"Storing ClientSession for identity:{0} throws",
identity);
notifySetupFailureAndDisconnect(
new LoginFailureException(LOGIN_REFUSED_REASON, e));
return;
}
sessionRefId = createTask.getId();
}
/*
* Inform the session service that this handler is available. If
* logging in, schedule a task to perform client login (which calls
* the AppListener.loggedIn method), otherwise set the "relocating"
* flag in the client session's state to false to indicate that
* relocation is complete.
*/
sessionService.addHandler(
sessionRefId, ClientSessionHandler.this,
loggingIn ? null : identity);
if (loggingIn) {
scheduleTask(new LoginTask());
} else {
try {
sessionService.runTransactionalTask(
new AbstractKernelRunnable("SetSessionRelocated") {
public void run() {
ClientSessionImpl sessionImpl =
ClientSessionImpl.getSession(
dataService, sessionRefId);
sessionImpl.relocationComplete();
} }, identity);
setupSuccess();
} catch (Exception e) {
logger.logThrow(
Level.WARNING, e,
"Relocating ClientSession for identity:{0} " +
"to local node:{1} throws",
identity, sessionService.getLocalNodeId());
notifySetupFailureAndDisconnect(
new RelocateFailureException(
RELOCATE_REFUSED_REASON, e));
return;
}
}
} else {
/*
* Redirect login to assigned (non-local) node.
*/
if (logger.isLoggable(Level.FINE)) {
logger.log(
Level.FINE,
"redirecting login for identity:{0} " +
"from nodeId:{1} to node:{2}",
identity, sessionService.getLocalNodeId(), assignedNodeId);
}
final long nodeId = assignedNodeId;
scheduleNonTransactionalTask(
new AbstractKernelRunnable("SendLoginRedirectMessage") {
public void run() {
try {
try {
loginRedirect(nodeId);
} catch (Exception ex) {
setupFailure(
new LoginFailureException("Redirect failed",
ex));
}
} finally {
handleDisconnect(false, false);
}
} });
}
}
/**
* Notifies the "setup" future that login has been redirected to the
* specified {@code nodeId}, and sets local state indicating that the
* login request has been handled.
*
* @param node a nodeId
*/
private void loginRedirect(long nodeId) {
synchronized (lock) {
checkConnectedState();
Set<ProtocolDescriptor> descriptors =
sessionService.getProtocolDescriptors(nodeId);
setupCompletionFuture.setException(
new LoginRedirectException(nodeId, descriptors));
state = State.LOGIN_HANDLED;
}
}
/**
* Throws an {@code IllegalStateException} if the associated client
* session handler is not in the {@code CONNECTED} state.
*/
private void checkConnectedState() {
assert Thread.holdsLock(lock);
if (state != State.CONNECTED) {
if (logger.isLoggable(Level.WARNING)) {
logger.log(
Level.WARNING,
"unexpected state:{0} for login protocol message, " +
"session:{1}", state.toString(), this);
}
throw new IllegalStateException("unexpected state: " +
state.toString());
}
}
/**
* Schedules a task to notify the "setup" future of failure and then
* disconnect the client session.
*
* @param exception an exception that occurred while setting up the
* client session, or {@code null}
*/
private void notifySetupFailureAndDisconnect(final Exception exception) {
scheduleNonTransactionalTask(
new AbstractKernelRunnable("NotifySetupFailureAndDisconnect") {
public void run() {
try {
setupFailure(exception);
} finally {
handleDisconnect(false, false);
}
} });
}
/**
* Schedules a non-durable, transactional task.
*/
private void scheduleTask(KernelRunnable task) {
sessionService.scheduleTask(task, identity);
}
/**
* Schedules a non-durable, non-transactional task.
*/
private void scheduleNonTransactionalTask(KernelRunnable task) {
sessionService.getTaskScheduler().scheduleTask(task, identity);
}
/**
* Constructs the ClientSession.
*/
private class CreateClientSessionTask extends AbstractKernelRunnable {
private volatile BigInteger id;;
/** Constructs and instance. */
CreateClientSessionTask() {
super(null);
}
/** {@inheritDoc} */
public void run() {
ClientSessionImpl sessionImpl =
new ClientSessionImpl(sessionService,
identity,
protocol.getDeliveries(),
protocol.getMaxMessageLength());
id = sessionImpl.getId();
}
/**
* Returns the session ID for the created client session.
*/
BigInteger getId() {
return id;
}
}
/**
* This is a transactional task to notify the application's
* {@code AppListener} that this session has logged in.
*/
private class LoginTask extends AbstractKernelRunnable {
/** Constructs an instance. */
LoginTask() {
super(null);
}
/**
* Retrieve the {@code AppListener} from the {@code DataService},
* unwrapping it from its {@code ManagedSerializable} if necessary.
*
* @return the {@code AppListener} for the application
*/
@SuppressWarnings("unchecked")
private AppListener getAppListener() {
ManagedObject obj = dataService.getServiceBinding(
StandardProperties.APP_LISTENER);
return (obj instanceof AppListener) ?
(AppListener) obj :
((ManagedSerializable<AppListener>) obj).get();
}
/**
* Invokes the {@code AppListener}'s {@code loggedIn}
* callback, which returns a client session listener. If the
* returned listener is serializable, then this method does
* the following:
*
* a) queues the appropriate acknowledgment to be
* sent when this transaction commits, and
* b) schedules a task (on transaction commit) to call
* {@code notifyLoggedIn} on the identity.
*
* If the client session needs to be disconnected (if {@code
* loggedIn} returns a non-serializable listener (including
* {@code null}), or throws a non-retryable {@code
* RuntimeException}, then this method submits a
* non-transactional task to disconnect the client session.
* If {@code loggedIn} throws a retryable {@code
* RuntimeException}, then that exception is thrown to the
* caller.
*/
public void run() {
AppListener appListener = getAppListener();
logger.log(
Level.FINEST,
"invoking AppListener.loggedIn session:{0}", identity);
ClientSessionListener returnedListener = null;
RuntimeException ex = null;
ClientSessionImpl sessionImpl =
ClientSessionImpl.getSession(dataService, sessionRefId);
try {
returnedListener =
appListener.loggedIn(sessionImpl.getWrappedClientSession());
} catch (RuntimeException e) {
ex = e;
}
if (returnedListener instanceof Serializable) {
logger.log(
Level.FINEST,
"AppListener.loggedIn returned {0}", returnedListener);
sessionImpl.putClientSessionListener(
dataService, returnedListener);
sessionService.checkContext().
addCommitAction(sessionRefId,
new LoginResultAction(true, null), true);
sessionService.scheduleTaskOnCommit(
new AbstractKernelRunnable("NotifyLoggedIn") {
public void run() {
logger.log(
Level.FINEST,
"calling notifyLoggedIn on identity:{0}",
identity);
// notify that this identity logged in,
// whether or not this session is connected at
// the time of notification.
identity.notifyLoggedIn();
} });
} else {
LoginFailureException loginFailureEx;
if (ex == null) {
logger.log(
Level.WARNING,
"AppListener.loggedIn returned non-serializable " +
"ClientSessionListener:{0}", returnedListener);
loginFailureEx = new LoginFailureException(
LOGIN_REFUSED_REASON,
LoginFailureException.FailureReason.REJECTED_LOGIN);
} else if (!isRetryableException(ex)) {
logger.logThrow(
Level.WARNING, ex,
"Invoking loggedIn on AppListener:{0} with " +
"session: {1} throws",
appListener, ClientSessionHandler.this);
loginFailureEx =
new LoginFailureException(LOGIN_REFUSED_REASON, ex);
} else {
throw ex;
}
sessionService.checkContext().
addCommitAction(
sessionRefId,
new LoginResultAction(false, loginFailureEx),
true);
sessionImpl.disconnect();
}
}
}
/**
* This future is constructed with the {@code RequestCompletionHandler}
* passed to one of the {@link SessionProtocolHandler} methods.
*/
private static class RequestCompletionFuture
extends AbstractCompletionFuture<Void>
{
/**
* Constructs an instance with the specified {@code completionHandler}.
*
* @param completionHandler a completionHandler
*/
RequestCompletionFuture(
RequestCompletionHandler<Void> completionHandler)
{
super(completionHandler);
}
/** {@inheritDoc} */
protected Void getValue() {
return null;
}
/** {@inheritDoc} */
public void setException(Throwable throwable) {
super.setException(throwable);
}
public void done() {
super.done();
}
}
/**
* This future is constructed with the {@link RequestCompletionHandler}
* passed to one of the {@code ProtocolListener}'s methods: {@code
* newLogin} or {@code relocatedSession}.
*/
static class SetupCompletionFuture
extends AbstractCompletionFuture<SessionProtocolHandler>
{
/** The session protocol handler. */
private final SessionProtocolHandler protocolHandler;
/**
* Constructs an instance with the specified {@code protocolHandler}
* and {@code completionHandler}.
*
* @param protocolHandler a session protocol handler
* @param completionHandler a completionHandler
*/
SetupCompletionFuture(
SessionProtocolHandler protocolHandler,
RequestCompletionHandler<SessionProtocolHandler> completionHandler)
{
super(completionHandler);
this.protocolHandler = protocolHandler;
}
/** {@inheritDoc} */
protected SessionProtocolHandler getValue() {
return protocolHandler;
}
/** {@inheritDoc} */
public void setException(Throwable throwable) {
super.setException(throwable);
}
/** {@inheritDoc} */
public void done() {
super.done();
}
}
/* -- Implement Commit Actions -- */
/**
* An action to report the result of a login.
*/
private class LoginResultAction implements Action {
/** The login result. */
private final boolean loginSuccess;
/** The login exception. */
private final LoginFailureException loginException;
/**
* Records the login result in this context, so that the specified
* client {@code session} can be notified when this context
* commits. If {@code success} is {@code false}, the specified
* {@code exception} will be used as the cause of the {@code
* ExecutionException} in the {@code Future} passed to the {@link
* RequestCompletionHandler} for the login request, and no
* subsequent session messages will be forwarded to the session,
* even if they have been enqueued during the current transaction.
* If success is {@code true}, then the {@code Future} passed to
* the {@code RequestCompletionHandler} for the login request will
* contain this {@link SessionProtocolHandler}.
*
* <p>When the transaction commits, the session's associated {@code
* ClientSessionHandler} is notified of the login result, and if
* {@code success} is {@code true}, all enqueued messages will be
* delivered to the client session.
*
* @param success if {@code true}, login was successful
* @param ex a login failure exception, or {@code null}
* (only valid if {@code success} is {@code false}
* @throws TransactionException if there is a problem with the
* current transaction
*/
LoginResultAction(boolean success, LoginFailureException ex) {
loginSuccess = success;
loginException = ex;
}
/** {@inheritDoc} */
public boolean flush() {
if (!isConnected()) {
return false;
} else if (loginSuccess) {
setupSuccess();
return true;
} else {
setupFailure(loginException);
return false;
}
}
}
/**
* An action to send a message. This commit action is created by the
* associated session's {@code ClientSessionImpl} when processing a
* request to send a message to the client.<p>
*
* When this action is executed, it notifies the session's {@link
* SessionProtocol} to send the message obtained from the
* {@link SendEvent} specified during construction.
*/
class SendMessageAction implements Action {
private final SendEvent sendEvent;
/**
* Constructs and instance with the specified {@code sendEvent}.
*
* @param sendEvent a send event containing a message and delivery
* guarantee
*/
SendMessageAction(SendEvent sendEvent) {
this.sendEvent = sendEvent;
}
/** {@inheritDoc} */
public boolean flush() {
if (!isConnected()) {
return false;
}
try {
protocol.sessionMessage(
ByteBuffer.wrap(sendEvent.message),
sendEvent.delivery);
} catch (Exception e) {
logger.logThrow(Level.WARNING, e,
"sessionMessage throws");
}
return true;
}
}
/**
* An action to start the process of moving the associated client
* session from this node to another node (the {@code newNode}
* specified during construction). This commit action is created by
* the associated session's {@code ClientSessionImpl} when processing a
* request to move the client to a new node. <p>
*
* When this action is executed, it sends a {@link
* ClientSessionServer#relocatingSession relocatingSession}
* notification to the new node's {@code ClientSessionServer} to obtain
* a relocation key for the client session. Once the relocation key is
* obtained, it notifies the client session service to notify all
* registered {@link ClientSessionStatusListener}s (i.e., the
* {@code ChannelService}) to prepare to relocate the client session.<p>
*
* When all {@code ClientSessionStatusListener}s have completed
* preparing the client session to relocate, this instance's
* {@link MoveAction#suspend suspend} method is invoked which
* notifies the associated session's {@code SessionProtocol} to
* suspend sending messages to the server.<p>
*
* When the suspend operation's {@code SuspendCompletionHandler} is
* notified as {@code completed}, that completion handler notifies
* this action's {@code completed} method, which, in turn notifies
* the associated session's {@link SessionProtocol} to {@code
* relocate} the client's connection, supplying the new node's
* information and the relocation key.
*/
class MoveAction implements Action, SimpleCompletionHandler {
/** The new node. */
private final Node newNode;
/** The client session server. */
private final ClientSessionServer server;
/** The relocation key. */
private byte[] relocationKey;
private boolean isCompleted = false;
/**
* Constructs an instance with the specified {@code newNode}.
*
* @param newNode the new node for the session
*/
MoveAction(Node newNode) {
this.newNode = newNode;
this.server =
sessionService.getClientSessionServer(newNode.getId());
}
/** {@inheritDoc} */
public boolean flush() {
if (!isConnected()) {
return false;
}
byte[] key;
try {
/*
* Notify new node that session is being relocated there and
* obtain relocation key.
*/
key = server.relocatingSession(
identity, sessionRefId.toByteArray(),
sessionService.getLocalNodeId());
} catch (Exception e) {
// If there is a problem contacting the destination node or
// obtaining the relocation key, disconnect the client
// session immediately.
if (logger.isLoggable(Level.WARNING)) {
logger.logThrow(
Level.WARNING, e,
"relocating client session:{0} throws", this);
}
handleDisconnect(false, true);
return false;
}
synchronized (this) {
this.relocationKey = key;
}
/*
* Notify client to relocate its session to the new node
* specifying the relocation key.
*/
synchronized (lock) {
relocatePrepareCompletionHandler = this;
}
sessionService.notifyPrepareToRelocate(
sessionRefId, newNode.getId());
// TBD: why is this return value false?
return false;
}
/**
* Returns {@code true} if relocation preparation is completed.
* @return {@code true} if relocation preparation is completed
*/
public synchronized boolean isCompleted() {
return isCompleted;
}
/**
* Suspends messages to the client before sending the 'relocate'
* notification. This method is invoked after the session has
* been prepared for relocation. When message suspension is
* complete, this instance's {@code completed} method is invoked
* to notify the client to relocate.
*/
public void suspend() {
try {
if (!supportsRelocation()) {
logger.log(
Level.WARNING,
"Disconnecting a non-relocatable session:{0} " +
"that was erroneously prepared to relocate", identity);
handleDisconnect(false, true);
return;
}
((SessionRelocationProtocol) protocol).suspend(
new SuspendCompletionHandler());
} catch (Exception e) {
if (logger.isLoggable(Level.WARNING)) {
logger.logThrow(
Level.WARNING, e,
"suspending messages to client session:{0} throws",
this);
}
}
}
/** {@inheritDoc} <p>
*
* This method is invoked after the session has been prepared for
* relocation and messages have been suspended from the client
* (i.e., invoked from the {@code SuspendCompletionHandler.completed}
* method).
*/
public void completed() {
synchronized (this) {
assert relocationKey != null;
if (isCompleted) {
return;
}
isCompleted = true;
}
if (!supportsRelocation()) {
logger.log(
Level.WARNING,
"Disconnecting a non-relocatable session:{0} " +
"that was erroneously prepared to relocate", identity);
handleDisconnect(false, true);
return;
}
final Set<ProtocolDescriptor> descriptors =
sessionService.getProtocolDescriptors(newNode.getId());
final byte[] key;
synchronized (this) {
key = this.relocationKey;
}
// Add client 'relocate' notification to the task queue to
// ensure that all previous requests sent before the client
// was suspended are processed before relocation.
taskQueue.addTask(
new AbstractKernelRunnable("NotifySessionRelocate") {
public void run() {
try {
((SessionRelocationProtocol) protocol).relocate(
descriptors, ByteBuffer.wrap(key),
new RelocateCompletionHandler());
} catch (Exception e) {
if (logger.isLoggable(Level.WARNING)) {
logger.logThrow(
Level.WARNING, e,
"relocating client session:{0} throws",
this);
}
// If there is a problem with relocation, the
// client session will be cleaned up by one
// of the "monitors" (on the old or new
// node) keeping track of this session's
// relocation, so there is no need to do it
// here.
}
} }, identity);
}
}
/**
* An action to disconnect the client session. This commit action is
* created by the associated session's {@code ClientSessionImpl} when
* processing a request to disconnect the client session.
*/
class DisconnectAction implements Action {
/** {@inheritDoc} */
public boolean flush() {
handleDisconnect(false, true);
return false;
}
}
/**
* A completion handler for notifying the client to suspend messages.
* When messages suspension is completed, this handler notifies {@code
* MoveAction} that suspension relocation preparation is complete so that
* it can send a 'relocate' notification to the client.
*/
private class SuspendCompletionHandler
implements RequestCompletionHandler<Void>
{
private boolean isCompleted = false;
/** {@inheritDoc} */
public synchronized void completed(Future<Void> result) {
synchronized (this) {
isCompleted = true;
}
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE,
"suspend completed, identity:{0} localNodeId:{1}",
identity, sessionService.getLocalNodeId());
}
if (relocatePrepareCompletionHandler != null) {
relocatePrepareCompletionHandler.completed();
}
}
synchronized boolean isCompleted() {
return isCompleted;
}
}
/**
* A completion handler for notifying the client to relocate.
*/
private class RelocateCompletionHandler
implements RequestCompletionHandler<Void>
{
private boolean isCompleted = false;
/** {@inheritDoc} */
public synchronized void completed(Future<Void> result) {
// TBD: need to check result for Exception and disconnect if an
// exception is thrown.
isCompleted = true;
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE,
"relocate completed, identity:{0} localNodeId:{1}",
identity, sessionService.getLocalNodeId());
}
}
synchronized boolean isCompleted() {
return isCompleted;
}
}
}