/* * 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.protocol.simple; import com.sun.sgs.app.Delivery; import com.sun.sgs.impl.sharedutil.HexDumper; import com.sun.sgs.impl.sharedutil.LoggerWrapper; import com.sun.sgs.impl.sharedutil.MessageBuffer; import com.sun.sgs.impl.util.AbstractCompletionFuture; import com.sun.sgs.nio.channels.AsynchronousByteChannel; import com.sun.sgs.protocol.LoginFailureException; import com.sun.sgs.protocol.LoginRedirectException; 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.SessionProtocolHandler; import com.sun.sgs.protocol.SessionRelocationProtocol; import com.sun.sgs.protocol.simple.SimpleSgsProtocol; import java.math.BigInteger; import java.nio.ByteBuffer; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; /** * Implements the protocol specified in {@code SimpleSgsProtocol}. The * implementation uses a wrapper channel, {@link AsynchronousMessageChannel}, * that reads and writes complete messages by framing messages with a 2-byte * message length, and masking (and re-issuing) partial I/O operations. Also * enforces a fixed buffer size when reading. */ public class SimpleSgsRelocationProtocolImpl extends SimpleSgsProtocolImpl implements SessionRelocationProtocol { /** The logger for this class. */ private static final LoggerWrapper staticLogger = new LoggerWrapper( Logger.getLogger(SimpleSgsRelocationProtocolImpl.class.getName())); /** The default reason string returned for relocation failure. */ private static final String DEFAULT_RELOCATE_FAILED_REASON = "relocation refused"; /** A lock for {@suspendCompletionFuture}, and {@code relocationInfo} * fields. */ private final Object lock = new Object(); /** The completion future if suspending messages is in progress, or * null. */ private SuspendMessagesCompletionFuture suspendCompletionFuture = null; /** The session's relocation information, if this session is relocating to * another node. */ private RelocationInfo relocationInfo = null; /** * Creates a new instance of this class. * * @param listener a protocol listener * @param acceptor the {@code SimpleSgsProtocol} acceptor * @param byteChannel a byte channel for the underlying connection * @param readBufferSize the read buffer size */ SimpleSgsRelocationProtocolImpl(ProtocolListener listener, SimpleSgsProtocolAcceptor acceptor, AsynchronousByteChannel byteChannel, int readBufferSize) { this(listener, acceptor, byteChannel, readBufferSize, staticLogger); /* * TBD: It might be a good idea to implement high- and low-water marks * for the buffers, so they don't go into hysteresis when they get * full. -JM */ scheduleRead(); } /** * Constructs a new instance of this class. The subclass should invoke * {@link SimpleSgsProtocolImpl#scheduleRead} after constructing the * instance to commence reading. * * @param listener a protocol listener * @param acceptor the {@code SimpleSgsProtocol} acceptor * @param byteChannel a byte channel for the underlying connection * @param readBufferSize the read buffer size * @param logger a logger for this instance */ protected SimpleSgsRelocationProtocolImpl(ProtocolListener listener, SimpleSgsProtocolAcceptor acceptor, AsynchronousByteChannel byteChannel, int readBufferSize, LoggerWrapper logger) { super(listener, acceptor, byteChannel, readBufferSize, logger); } /** * Returns the {@code SimpleSgsProtocol} version supported by this * implementation. * * This implementation returns the latest {@code SimpleSgsProtocol} * version. * * @return the {@code SimpleSgsProtocol} version supported by this * implementation */ @Override protected byte getProtocolVersion() { return SimpleSgsProtocol.VERSION; } /* -- Implement SessionProtocol -- */ /** {@inheritDoc} */ @Override public void sessionMessage(ByteBuffer message, Delivery delivery) { checkSuspend(); super.sessionMessage(message, delivery); } /** {@inheritDoc} */ @Override public void channelJoin( String name, BigInteger channelId, Delivery delivery) { checkSuspend(); super.channelJoin(name, channelId, delivery); } /** {@inheritDoc} */ @Override public void channelLeave(BigInteger channelId) { checkSuspend(); super.channelLeave(channelId); } /** {@inheritDoc} */ @Override public void channelMessage( BigInteger channelId, ByteBuffer message, Delivery delivery) { checkSuspend(); super.channelMessage(channelId, message, delivery); } /* -- Implement SessionRelocationProtocol -- */ /** {@inheritDoc} */ public void suspend(RequestCompletionHandler<Void> completionHandler) { synchronized (lock) { if (suspendCompletionFuture != null) { throw new IllegalStateException( "already suspending messages"); } suspendCompletionFuture = new SuspendMessagesCompletionFuture(completionHandler); } ByteBuffer buf = ByteBuffer.allocate(1); buf.put(SimpleSgsProtocol.SUSPEND_MESSAGES). flip(); writeNow(buf, true); } /** {@inheritDoc} */ public void resume() { synchronized (lock) { if (suspendCompletionFuture != null) { suspendCompletionFuture = null; } } ByteBuffer buf = ByteBuffer.allocate(1); buf.put(SimpleSgsProtocol.RESUME_MESSAGES). flip(); writeNow(buf, true); } /** {@inheritDoc} */ public void relocate(Set<ProtocolDescriptor> descriptors, ByteBuffer relocationKey, RequestCompletionHandler<Void> completionHandler) { synchronized (lock) { if (relocationInfo != null) { throw new IllegalStateException("session already relocating"); } else if (suspendCompletionFuture == null || !suspendCompletionFuture.isDone()) { throw new IllegalStateException("session is not suspended"); } if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "relocating, identity:{0} key:{1}", getIdentity(), HexDumper.toHexString(relocationKey.array())); } relocationInfo = new RelocationInfo(descriptors, relocationKey); } relocationInfo.sendRelocateNotification(); } /* -- Private methods for sending protocol messages -- */ /** * Notifies the associated client that the previous relocation attempt * was successful. */ private void relocateSuccess() { MessageBuffer buf = new MessageBuffer(1 + reconnectKey.length); buf.putByte(SimpleSgsProtocol.RELOCATE_SUCCESS). putBytes(reconnectKey); writeNow(ByteBuffer.wrap(buf.getBuffer()), true); } /** * Notifies the associated client that the previous relocation attempt * was unsuccessful for the specified {@code reason}. The specified * {@code throwable}, if non-{@code null} is an exception that * occurred while processing the relocation request. The message * channel should be careful not to reveal to the associated client * sensitive data that may be present in the specified {@code * throwable}. * * @param reason a reason why the relocation was unsuccessful * @param throwable an exception that occurred while processing the * relocation request, or {@code null} */ private void relocateFailure(String reason, Throwable ignore) { // the reason argument is overridden for security reasons reason = DEFAULT_RELOCATE_FAILED_REASON; MessageBuffer buf = new MessageBuffer(1 + MessageBuffer.getSize(reason)); buf.putByte(SimpleSgsProtocol.RELOCATE_FAILURE). putString(reason); writeNow((ByteBuffer.wrap(buf.getBuffer())), true); monitorDisconnection(); } /** * Throws {@link IllegalStateException} if the client session is * relocating or has suspended messages. * * @throws IllegalStateException if the client session is relocating */ private void checkSuspend() { synchronized (lock) { if (relocationInfo != null) { throw new IllegalStateException("session relocating"); } else if (suspendCompletionFuture != null) { throw new IllegalStateException("messages suspended"); } } } /** * Handles v5 protocol messages (relocate and suspend), and delegates * to the super class to handle the v4 protocol messages. * * @param opcode the message opcode * @param msg a message buffer containing the entire message, but * with the position advanced to the payload (just after the * opcode) */ @Override protected void handleMessageReceived(byte opcode, MessageBuffer msg) { switch (opcode) { case SimpleSgsProtocol.RELOCATE_REQUEST: byte version = msg.getByte(); if (version != getProtocolVersion()) { if (logger.isLoggable(Level.SEVERE)) { logger.log(Level.SEVERE, "got protocol version:{0}, " + "expected {1}", version, getProtocolVersion()); } close(); break; } byte[] keyBytes = msg.getBytes(msg.limit() - msg.position()); BigInteger relocationKey = new BigInteger(1, keyBytes); listener.relocatedSession( relocationKey, SimpleSgsRelocationProtocolImpl.this, new RelocateHandler()); // Resume reading immediately readNow(); break; case SimpleSgsProtocol.SUSPEND_MESSAGES_COMPLETE: synchronized (lock) { if (suspendCompletionFuture != null) { suspendCompletionFuture.done(); } else { if (logger.isLoggable(Level.WARNING)) { logger.log( Level.WARNING, "{0} received unexpected " + "SUSPEND_MESSAGES_COMPLETE"); } } } break; default: super.handleMessageReceived(opcode, msg); break; } } /** * A completion handler that is notified when the associated relocate * request has completed processing. */ private class RelocateHandler implements RequestCompletionHandler<SessionProtocolHandler> { /** {@inheritDoc} * * <p>This implementation invokes the {@code get} method on the * specified {@code future} to obtain the session's protocol * handler. * * <p>If the relocate request completed successfully (without * throwing an exception), it sends a relocate success message to * the client. * * <p>Otherwise, if the {@code get} invocation throws an {@code * ExecutionException} and the exception's cause is a {@link * LoginRedirectException} or {@link RelocateFailureException}, it * sends a relocate failure message to the client. * * <p>If the {@code get} method throws an exception other than * {@code ExecutionException}, or the {@code ExecutionException}'s * cause is not either a {@code RelocateFailureException} or a {@code * LoginRedirectException}, then a relocate failed message is sent * to the client. */ public void completed(Future<SessionProtocolHandler> future) { try { protocolHandler = future.get(); relocateSuccess(); } catch (ExecutionException e) { // relocate failed Throwable cause = e.getCause(); if (cause instanceof LoginRedirectException || cause instanceof RelocateFailureException) { relocateFailure(cause.getMessage(), cause.getCause()); } else { relocateFailure(e.getMessage(), e.getCause()); } } catch (Exception e) { relocateFailure(e.getMessage(), e.getCause()); } } } /** * Relocation information. */ private class RelocationInfo { private final Set<ProtocolDescriptor> descriptors; private final ByteBuffer relocationKey; RelocationInfo(Set<ProtocolDescriptor> descriptors, ByteBuffer relocationKey) { this.descriptors = descriptors; this.relocationKey = relocationKey; } /** * Sends a relocate notification to the underlying session, and * monitors the underlying connection for timely disconnection. */ void sendRelocateNotification() { for (ProtocolDescriptor descriptor : descriptors) { if (acceptor.getDescriptor().supportsProtocol(descriptor)) { byte[] redirectionData = ((SimpleSgsProtocolDescriptor) descriptor). getConnectionData(); ByteBuffer buf = ByteBuffer.allocate(1 + redirectionData.length + relocationKey.remaining()); buf.put(SimpleSgsProtocol.RELOCATE_NOTIFICATION). put(redirectionData). put(relocationKey). flip(); writeNow(buf, true); monitorDisconnection(); return; } } } } /** * A completion future for suspending messages. */ private static class SuspendMessagesCompletionFuture extends AbstractCompletionFuture<Void> { SuspendMessagesCompletionFuture(RequestCompletionHandler<Void> completionFuture) { super(completionFuture); } /** {@inheritDoc} */ protected Void getValue() { return null; } /** {@inheritDoc} */ public void done() { super.done(); } } }