/* * 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.channel; import com.sun.sgs.app.AppContext; import com.sun.sgs.app.Channel; import com.sun.sgs.app.ChannelListener; import com.sun.sgs.app.ClientSession; 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.TaskManager; import com.sun.sgs.app.util.ManagedSerializable; import com.sun.sgs.impl.service.channel.ChannelServer.MembershipStatus; import com.sun.sgs.impl.service.channel.ChannelServiceImpl.MembershipEventType; import com.sun.sgs.impl.service.session.ClientSessionImpl; import com.sun.sgs.impl.service.session.ClientSessionWrapper; 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.BindingKeyedCollections; import com.sun.sgs.impl.util.BindingKeyedMap; import com.sun.sgs.impl.util.IoRunnable; import com.sun.sgs.impl.util.KernelCallable; import com.sun.sgs.impl.util.ManagedQueue; import com.sun.sgs.kernel.KernelRunnable; import com.sun.sgs.service.DataService; import com.sun.sgs.service.Node; import com.sun.sgs.service.TaskService; import com.sun.sgs.service.Transaction; import com.sun.sgs.service.WatchdogService; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.math.BigInteger; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import static com.sun.sgs.impl.service.channel.ChannelServiceImpl. getObjectForId; /** * Channel implementation for use within a single transaction. * * <p>This implementation uses several {@link BindingKeyedCollections} * as follows: * * <dl style="margin-left: 1em"> * * <dt> <i>Map:</i> <b><code>channelsMap</code></b> <br> * <i>Prefix:</i> <code>{@value #CHANNELS_MAP_PREFIX}</code> <br> * <i>Key:</i> <i>{@code name}</i><br> * <i>Value:</i> <b>{@link ChannelImpl}</b> * * <dd style="padding-top: .5em">Map for accessing a {@code ChannelImpl} * by name. <p> * * <dt> <i>Map:</i> <b><code>eventQueuesMap</code></b> <br> * <i>Prefix:</i> <code>{@value * #EVENT_QUEUE_MAP_PREFIX}<i>coordinatorNodeId.</i></code> <br> * <i>Key:</i> <i>{@code channelId}</i> (as string form of * {@code BigInteger})<br> * <i>Value:</i> <b>{@code EventQueue}</b> * * <dd style="padding-top: .5em">Map for accessing an event queue for a * channel on a given node. The map is also used during recovery to * determine which channels are coordinated on a failed node so that * each channel can be reassigned a new coordinator.<p> * * <dt> <i>Map:</i> <b><code>savedMessagesMap</code></b> <br> * <i>Prefix:</i> <code>{@value * #SAVED_MESSAGES_MAP_PREFIX}<i>channelId.</i></code> <br> * <i>Key:</i> <i>{@code timestamp}</i> (as string form of * {@code Long})<br> * <i>Value:</i> <b>{@code ChannelMessageInfo}</b> * * <dd style="padding-top: .5em">Map for accessing saved messages for a * given channel by timestamp. The map is used when a client * session relocates to a new node and the channel service discovers * that the session missed one or more channel messages during * relocation.<p> * </dl> <p> */ final class ChannelImpl implements ManagedObject, Serializable { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** The logger for this class. */ private static final LoggerWrapper logger = new LoggerWrapper( Logger.getLogger(ChannelImpl.class.getName())); /** The package name. */ private static final String PKG_NAME = "com.sun.sgs.impl.service.channel."; /** The channels map prefix. */ static final String CHANNELS_MAP_PREFIX = PKG_NAME + "name."; /** An event queue map prefix. */ static final String EVENT_QUEUE_MAP_PREFIX = PKG_NAME + "eventQueue."; /** The saved messages map prefix. */ static final String SAVED_MESSAGES_MAP_PREFIX = PKG_NAME + "message."; /** The empty channel membership set. */ static final Set<BigInteger> EMPTY_CHANNEL_MEMBERSHIP = Collections.emptySet(); /** The random number generator for choosing a new coordinator. */ private static final Random random = new Random(); /** The map of channels, keyed by name. */ private static BindingKeyedMap<ChannelImpl> channelsMap = null; /** The channel name. */ private final String name; /** The ID from a managed reference to this instance. */ final BigInteger channelRefId; /** The wrapped channel instance. */ private final ManagedReference<ChannelWrapper> wrappedChannelRef; /** The reference to this channel's listener. */ private final ManagedReference<ChannelListener> listenerRef; /** The delivery requirement for messages sent on this channel. */ private final Delivery delivery; /** The node IDs of ChannelServers that have locally connected * members of this channel. */ private final Set<Long> servers = new HashSet<Long>(); /** * The node ID of the coordinator for this channel. At first, it * is the node that the channel was created on. If the * coordinator node fails, then the coordinator becomes one of the * member's nodes or the node performing recovery if there are no * members. */ private long coordNodeId; /** Indicates whether this coordinator is reassigned so that some * actions can be performed by the new coordinator. */ private boolean isCoordinatorReassigned; /** The transaction. */ private transient Transaction txn; /** Flag that is 'true' if this channel is closed. */ private boolean isClosed = false; /** * The maximum channel message length supported by sessions joined to this * channel. */ private int maxMessageLength = Integer.MAX_VALUE; /** * The maximum number of message bytes that can be queued for delivery on * this channel. */ private final int writeBufferCapacity; /** The event queue reference. */ private final ManagedReference<EventQueue> eventQueueRef; /** * Constructs an instance of this class with the specified * {@code name}, {@code listener}, {@code delivery} guarantee, * write buffer capacity, and channel wrapper. * * @param name a channel name * @param listener a channel listener * @param delivery a delivery guarantee * @param writeBufferCapacity the capacity of the write buffer, in bytes * @param channelWrapper the previous channel wrapper, or {@code null} if * the channel is being created for the first time */ private ChannelImpl(String name, ChannelListener listener, Delivery delivery, int writeBufferCapacity, ChannelWrapper channelWrapper) { if (name == null) { throw new NullPointerException("null name"); } this.name = name; DataService dataService = getDataService(); if (listener != null) { if (!(listener instanceof Serializable)) { throw new IllegalArgumentException("non-serializable listener"); } else if (!(listener instanceof ManagedObject)) { listener = new ManagedSerializableChannelListener(listener); } this.listenerRef = dataService.createReference(listener); } else { this.listenerRef = null; } this.delivery = delivery; this.writeBufferCapacity = writeBufferCapacity; this.txn = ChannelServiceImpl.getTransaction(); ManagedReference<ChannelImpl> ref = dataService.createReference(this); if (channelWrapper == null) { channelWrapper = new ChannelWrapper(ref); } else { channelWrapper.setChannelRef(ref); } this.wrappedChannelRef = dataService.createReference(channelWrapper); this.channelRefId = ref.getId(); this.coordNodeId = getLocalNodeId(); if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "Created ChannelImpl:{0}", HexDumper.toHexString(channelRefId)); } getChannelsMap().putOverride(name, this); EventQueue eventQueue = new EventQueue(this); eventQueueRef = dataService.createReference(eventQueue); getEventQueuesMap(coordNodeId). put(channelRefId.toString(), eventQueue); } /** Returns the data service. */ private static DataService getDataService() { return ChannelServiceImpl.getInstance().getDataService(); } /** * Returns the channels map, keyed by channel name. */ private static synchronized BindingKeyedMap<ChannelImpl> getChannelsMap() { if (channelsMap == null) { channelsMap = newMap(CHANNELS_MAP_PREFIX); } return channelsMap; } /** * Returns the event queues map for the specified coordinator {@code * nodeId}, keyed by channel ID. In the returned map, each key is a * channel ID (as a BigInteger converted to string form) and is mapped * to its corresponding {@code EventQueue}. */ private static BindingKeyedMap<EventQueue> getEventQueuesMap(long nodeId) { return newMap(EVENT_QUEUE_MAP_PREFIX + Long.toString(nodeId) + "."); } /* -- Factory methods -- */ /** * Constructs a new {@code Channel} with the given {@code name}, {@code * listener}, {@code delivery} guarantee and write-buffer capacity. */ static Channel newInstance(String name, ChannelListener listener, Delivery delivery, int writeBufferCapacity) { return newInstance(name, listener, delivery, writeBufferCapacity, null); } /** * Constructs a new {@code Channel} with the given {@code name}, {@code * listener}, {@code delivery} guarantee, write-buffer capacity and * {@code channelWrapper}. If the {@code channelWrapper} is null, * then this is a newly-created channel. Otherwise, the channel is * being "recreated" due to a "leaveAll" event. */ private static Channel newInstance(String name, ChannelListener listener, Delivery delivery, int writeBufferCapacity, ChannelWrapper channelWrapper) { ChannelImpl channel = new ChannelImpl(name, listener, delivery, writeBufferCapacity, channelWrapper); return channel.getWrappedChannel(); } /** * Returns a channel with the given {@code name}. */ static Channel getInstance(String name) { ChannelImpl channelImpl = getChannelsMap().get(name); if (channelImpl != null) { return channelImpl.getWrappedChannel(); } else { throw new NameNotBoundException("channel not found: " + name); } } /* -- Implement Channel -- */ /** Implements {@link Channel#getName}. */ String getName() { checkContext(); if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "getName returns {0}", name); } return name; } /** Implements {@link Channel#getDelivery}. */ Delivery getDelivery() { checkContext(); if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "getDelivery returns {0}", delivery); } return delivery; } /** Implements {@link Channel#hasSessions}. */ boolean hasSessions() { checkClosed(); return !servers.isEmpty(); } /** Implements {@link Channel#getSessions}. */ Iterator<ClientSession> getSessions() { checkClosed(); Set<BigInteger> channelMembers = null; if (!servers.isEmpty()) { channelMembers = ChannelServiceImpl.getInstance().collectChannelMembership( txn, channelRefId, servers); } return new ClientSessionIterator( channelMembers != null ? channelMembers : EMPTY_CHANNEL_MEMBERSHIP); } /** Implements {@link Channel#join(ClientSession)}. */ void join(final ClientSession session) { try { checkClosed(); if (session == null) { throw new NullPointerException("null session"); } checkDelivery(session); /* * Enqueue join request with underlying (unwrapped) client * session object. */ updateMaxMessageLength(session); addEvent( new JoinEvent(unwrapSession(session), eventQueueRef.get())); logger.log(Level.FINEST, "join session:{0} returns", session); } catch (RuntimeException e) { logger.logThrow(Level.FINE, e, "join throws"); throw e; } } /** Implements {@link Channel#join(Set)}. * * Enqueues a join event to this channel's event queue and notifies * this channel's coordinator to service the event. */ void join(final Set<? extends ClientSession> sessions) { try { checkClosed(); if (sessions == null) { throw new NullPointerException("null sessions"); } /* * Check for null elements, and check that sessions support * this channel's delivery guarantee. */ for (ClientSession session : sessions) { if (session == null) { throw new NullPointerException( "sessions contains a null element"); } checkDelivery(session); } /* * Enqueue join requests, each with underlying (unwrapped) * client session object. */ EventQueue eventQueue = eventQueueRef.get(); for (ClientSession session : sessions) { updateMaxMessageLength(session); addEvent(new JoinEvent(unwrapSession(session), eventQueue)); } logger.log(Level.FINEST, "join sessions:{0} returns", sessions); } catch (RuntimeException e) { logger.logThrow(Level.FINE, e, "join throws"); throw e; } } /** * Throws {@code DeliveryNotSupportedException} if the specified {@code * session} does not support this channel's delivery guarantee. * * @param session a client session * @throws DeliveryNotSupportedException if the specified {@code session} * does not support this channel's delivery guarantee */ private void checkDelivery(ClientSession session) { for (Delivery d : session.supportedDeliveries()) { if (d.supportsDelivery(delivery)) { return; } } throw new DeliveryNotSupportedException( "client session:" + session + " does not support delivery guarantee", delivery); } /** * Sets the channel's maximum message length to the client session's * maximum message length if the session's maximum is lower than the * existing maximum for the channel. * * @param session a client session joining the channel */ private void updateMaxMessageLength(ClientSession session) { int sessionMaxMessageLength = session.getMaxMessageLength(); if (maxMessageLength > sessionMaxMessageLength) { getDataService().markForUpdate(this); maxMessageLength = sessionMaxMessageLength; } } /** * Returns the message timestamp of the last message processed by * this channel. * * @return the current message timestamp processed by this * channel's event queue */ long getCurrentMessageTimestamp() { return eventQueueRef.get().currentTimestamp; } /** * Returns the underlying {@code ClientSession} for the specified * {@code session}. Note: The client session service wraps each client * session object that it hands out to the application. The channel * service implementation relies on the assumption that a client * session's {@code ManagedObject} ID is the client session's ID (used * for identifying the client session, e.g. for sending messages to * the client session). This method is invoked by the {@code join} and * {@code leave} methods in order to access the underlying {@code * ClientSession} so that the correct client session ID can be obtained. */ private ClientSession unwrapSession(ClientSession session) { assert session instanceof ClientSessionWrapper; return ((ClientSessionWrapper) session).getClientSession(); } /** * Adds the specified channel {@code event} to this channel's event queue * and notifies the coordinator that there is an event to service. As * an optimization, if the local node is the coordinator for this channel * and the event queue is empty, then service the event immediately. */ private void addEvent(ChannelEvent event) { EventQueue eventQueue = eventQueueRef.get(); if (!eventQueue.offer(event, this)) { throw new ResourceUnavailableException( "not enough resources to add channel event"); } } /** * If the coordinator is the local node, services events locally; * otherwise, schedules a task to send a request to this channel's * coordinator to service the event queue. * * @param eventQueue this channel's event queue */ private void notifyServiceEventQueue(EventQueue eventQueue) { if (isCoordinator()) { eventQueue.serviceEventQueue(); } else { final ChannelServer coordinator = getChannelServer(coordNodeId); if (coordinator == null) { /* * If the ChannelServer for the coordinator's node has been * removed, then the coordinator's node has failed and will * be reassigned during recovery. When recovery for the * failed node completes, the newly chosen coordinator will * restart the processing of channel events. */ return; } final long coord = coordNodeId; final ChannelServiceImpl channelService = ChannelServiceImpl.getInstance(); channelService.getTaskService().scheduleNonDurableTask( new AbstractKernelRunnable("SendServiceEventQueue") { public void run() { channelService.runIoTask( new IoRunnable() { public void run() throws IOException { coordinator.serviceEventQueue(channelRefId); } }, coord); } }, false); } } /** Implements {@link Channel#leave(ClientSession)}. * * Enqueues a leave event to this channel's event queue and notifies * this channel's coordinator to service the event. */ void leave(final ClientSession session) { try { checkClosed(); if (session == null) { throw new NullPointerException("null client session"); } /* * Enqueue leave request with underlying (unwrapped) client * session object. */ addEvent( new LeaveEvent(unwrapSession(session), eventQueueRef.get())); logger.log(Level.FINEST, "leave session:{0} returns", session); } catch (RuntimeException e) { logger.logThrow(Level.FINE, e, "leave throws"); throw e; } } /** Implements {@link Channel#leave(Set)}. * * Enqueues leave event(s) to this channel's event queue and notifies * this channel's coordinator to service the event(s). */ void leave(final Set<? extends ClientSession> sessions) { try { checkClosed(); if (sessions == null) { throw new NullPointerException("null sessions"); } /* * Enqueue leave requests, each with underlying (unwrapped) * client session object. */ for (ClientSession session : sessions) { addEvent(new LeaveEvent(unwrapSession(session), eventQueueRef.get())); } logger.log(Level.FINEST, "leave sessions:{0} returns", sessions); } catch (RuntimeException e) { logger.logThrow(Level.FINE, e, "leave throws"); throw e; } } /** Implements {@link Channel#leaveAll}. * * Closes the current channel, and creates a new channel (with a new * channel ID) that uses the existing channel's wrapper. Since the * application still uses the same channel wrapper for this channel, * the application is not effected. */ void leaveAll() { try { checkClosed(); close(false); ChannelListener listener = listenerRef != null ? listenerRef.get() : null; newInstance(name, listener, delivery, writeBufferCapacity, getWrappedChannel()); logger.log(Level.FINEST, "leaveAll returns"); } catch (RuntimeException e) { logger.logThrow(Level.FINE, e, "leaveAll throws"); throw e; } } /** Implements {@link Channel#send(ClientSession,ByteBuffer)}. * * Enqueues a send event to this channel's event queue and notifies * this channel's coordinator to service the event. */ void send(ClientSession sender, ByteBuffer message) { try { checkClosed(); if (message == null) { throw new NullPointerException("null message"); } message = message.asReadOnlyBuffer(); if (message.remaining() > maxMessageLength) { throw new IllegalArgumentException( "message too long: " + message.remaining() + " > " + maxMessageLength); } /* * Enqueue send request. */ byte[] msgBytes = new byte[message.remaining()]; message.get(msgBytes); BigInteger senderRefId = sender != null ? getSessionRefId(unwrapSession(sender)) : null; boolean isChannelMember = senderRefId != null ? ChannelServiceImpl.getInstance(). isLocalChannelMember(channelRefId, senderRefId) : true; addEvent( new SendEvent(senderRefId, msgBytes, eventQueueRef.get(), isChannelMember)); if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "send channel:{0} message:{1} returns", this, HexDumper.format(msgBytes, 0x50)); } } catch (RuntimeException e) { if (logger.isLoggable(Level.FINE)) { logger.logThrow( Level.FINE, e, "send channel:{0} message:{1} throws", this, HexDumper.format(message, 0x50)); } throw e; } } /** * If this channel has a null {@code ChannelListener}, forwards the * specified {@code message} to the channel by invoking this channel's * {@code send} method with the specified {@code sender} and {@code * message}; otherwise, invokes the {@code ChannelListener}'s {@code * receivedMessage} method with this channel, the specified {@code sender}, * and {@code message}. */ private void receivedMessage(ClientSession sender, ByteBuffer message) { assert sender instanceof ClientSessionWrapper; if (listenerRef == null) { send(sender, message); } else { listenerRef.get().receivedMessage( getWrappedChannel(), sender, message.asReadOnlyBuffer()); } } /** * Enqueues a close event to this channel's event queue and notifies * this channel's coordinator to service the event. This method is * invoked with {@code true} by this channel's {@code ChannelWrapper} * when the application removes the wrapper object, and is invoked with * {@code false} by this channel when the application invokes the * {@link #leaveAll} method. * * @param removeName if {@code true}, the channel's name binding * is removed when the channel's persistent structures * are cleaned up, otherwise, the channel's name binding is * not removed */ void close(boolean removeName) { checkContext(); getDataService().markForUpdate(this); if (!isClosed) { /* * Enqueue close event. */ addEvent(new CloseEvent(removeName, eventQueueRef.get())); isClosed = true; } } /* -- Implement Object -- */ /** {@inheritDoc} */ @Override public boolean equals(Object obj) { // TBD: Because this is a managed object, does an "==" check // suffice here? return (this == obj) || (obj != null && obj.getClass() == this.getClass() && channelRefId.equals(((ChannelImpl) obj).channelRefId)); } /** {@inheritDoc} */ @Override public int hashCode() { return channelRefId.hashCode(); } /** {@inheritDoc} */ @Override public String toString() { return getClass().getName() + "[" + HexDumper.toHexString(channelRefId) + "]"; } /* -- Serialization methods -- */ private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); txn = ChannelServiceImpl.getTransaction(); } /* -- Other methods -- */ /** * Returns the write buffer capacity for this channel. * * @return the write buffer capacity */ private int getWriteBufferCapacity() { return writeBufferCapacity; } /** * Returns the ID for the specified {@code session}. */ private static BigInteger getSessionRefId(ClientSession session) { return getDataService().createReference(session).getId(); } /** * Returns the node ID for the specified {@code session}. If the * session is relocating, this method returns the node ID that * the session is relocating to. */ private static long getNodeId(ClientSessionImpl session) { return session.isRelocating() ? session.getRelocatingToNodeId() : session.getNodeId(); } /** * Returns the wrapped channel for this instance. */ private ChannelWrapper getWrappedChannel() { return wrappedChannelRef.get(); } /** * Checks that this channel's transaction is currently active, * throwing TransactionNotActiveException if it isn't. */ private void checkContext() { ChannelServiceImpl.checkTransaction(txn); } /** * Returns {@code true} if the channel is closed, and {@code false} * otherwise. */ boolean isClosed() { return isClosed; } /** * Checks the context, and then checks that this channel is not * closed, throwing an IllegalStateException if the channel is * closed. */ private void checkClosed() { checkContext(); if (isClosed) { throw new IllegalStateException("channel is closed"); } } /** * Returns {@code true} if this node is the coordinator for this * channel, otherwise returns {@code false}. */ boolean isCoordinator() { return coordNodeId == getLocalNodeId(); } /** * Returns {@code true} if the local node is the coordinator for this * channel, and returns {@code false} otherwise. If the coordinator * was just reassigned to this node (i.e., the {@code * isCoordinatorReassigned} field is {@code true}) and this channel * supports reliable message delivery, then schedule a new task to reap * saved messages. */ private boolean checkCoordinator() { if (!isCoordinator()) { return false; } else { if (isCoordinatorReassigned) { getDataService().markForUpdate(this); if (isReliable()) { SavedMessageReaper.scheduleNewTask(channelRefId, false); } isCoordinatorReassigned = false; } return true; } } /** * Returns a new {@code BindingKeyedMap} with the specified {@code * keyPrefix}. * * @param <V> the value type * @param keyPrefix a key prefix for the map's service bindings * @return a new {@code BindingKeyedMap} */ private static <V> BindingKeyedMap<V> newMap(String keyPrefix) { return ChannelServiceImpl.getCollectionsFactory(). newMap(keyPrefix); } /** * Adds the specified {@code nodeId} to the set of server nodes for * this channel. * * @param nodeId a server node's ID */ void addServerNodeId(long nodeId) { checkClosed(); if (servers.add(nodeId)) { getDataService().markForUpdate(this); } } /** * Removes the specified {@code nodeId} from the set of server nodes * for this channel. * * @param nodeId a server node's ID */ void removeServerNodeId(long nodeId) { if (servers.remove(nodeId)) { getDataService().markForUpdate(this); } } /** * Returns the map of saved messages for this channel, for looking up * messages by timestamp. */ private static BindingKeyedMap<ChannelMessageInfo> getSavedMessagesMap(BigInteger channelRefId) { return newMap(SAVED_MESSAGES_MAP_PREFIX + channelRefId + "."); } /** * Saves the specified channel {@code message} with the specified * {@code timestamp}. Reliable messages are saved for a period of time * (the length of time it takes to move a client session) so that * channel messages missed during relocation can be obtained and * delivered to a relocated client session. */ private void saveMessage(byte[] message, long timestamp) { assert isReliable(); BindingKeyedMap<ChannelMessageInfo> savedMessagesMap = getSavedMessagesMap(channelRefId); ChannelMessageInfo messageInfo = new ChannelMessageInfo(message, timestamp); savedMessagesMap.put(getTimestampEncoding(timestamp), messageInfo); } /** * Returns a list containing saved channel messages (if any) with * timestamps between {@code fromTimestamp} and {@code toTimestamp} * inclusive. If {@code fromTimestamp} is greater than {@code * toTimestamp} this method returns {@code null}. */ List<ChannelMessageInfo> getChannelMessages( long fromTimestamp, long toTimestamp) { assert isReliable(); List<ChannelMessageInfo> messages = null; if (fromTimestamp <= toTimestamp) { messages = new ArrayList<ChannelMessageInfo>( (int) (toTimestamp - fromTimestamp + 1)); BindingKeyedMap<ChannelMessageInfo> savedMessagesMap = getSavedMessagesMap(channelRefId); for (long ts = fromTimestamp; ts <= toTimestamp; ts++) { ChannelMessageInfo messageInfo = savedMessagesMap.get(getTimestampEncoding(ts)); if (messageInfo != null) { messages.add(messageInfo); } } } return messages; } /** * Returns an encoding for the specified {@code timestamp} * that preserves ascending timestamp order with * lexicographically-ordered keys. The encoding consists * of the following: * * <ul> * <li> a single hex digit whose value is one less than the number of * hex digits (leading zero digits are not included), * <li> a hyphen, * <li> the hex encoding of the timestamp with leading zero digits * stripped. * </ul> */ private static String getTimestampEncoding(long timestamp) { String hexString = Long.toHexString(timestamp); int numHexits = hexString.length(); StringBuilder builder = new StringBuilder(2 + numHexits); builder. append(Character.forDigit(numHexits - 1, 16)). append('-'). append(hexString); return builder.toString(); } /** * Contains a saved channel message with its associated timestamp * and expiration time. */ static class ChannelMessageInfo implements ManagedObject, Serializable { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** The channel message. */ final byte[] message; /** The message timestamp. */ final long timestamp; /** The message's expiration time. */ private final long expiration; /** * Constructs an instance with the specified {@code message} * and {@code timestamp}. */ ChannelMessageInfo(byte[] message, long timestamp) { this.message = message; this.timestamp = timestamp; this.expiration = System.currentTimeMillis() + ChannelServiceImpl.getInstance().sessionRelocationTimeout; } /** * Returns {@code true} if the channel message has expired * (that is, its expiration time has passed). */ boolean isExpired() { return expiration <= System.currentTimeMillis(); } } /** * A (periodic) task to reap messages saved past their expiration time. */ private static final class SavedMessageReaper implements KernelRunnable, Task, Serializable { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** The channel's ID. */ private final BigInteger channelRefId; /** Indicates whether this task is durable. */ private final boolean isDurable; /** * Constructs an instance with the specified {@code channelRefId}. * Use the {@code scheduleNewTask} method to construct an instance and * schedule the instance as a periodic task. */ private SavedMessageReaper(BigInteger channelRefId, boolean isDurable) { this.channelRefId = channelRefId; this.isDurable = isDurable; } /** {@inheritDoc} */ public String getBaseTaskType() { return getClass().getName(); } /** * Iterates through the saved message map, removing messages * saved past their expiration time. Iteration over a {@code * BindingKeyedMap} returns bindings in lexicographical order. * Timestamps are encoded to preserve ascending timestamp order * with lexicographically-ordered keys, so the messages will be * returned in ascending timestamp order. */ public void run() { BindingKeyedMap<ChannelMessageInfo> savedMessages = getSavedMessagesMap(channelRefId); if (savedMessages.isEmpty()) { // Saved messages no longer exist, so "cancel" periodic task. return; } else { // Remove messages saved past their expiration time. DataService dataService = getDataService(); TaskManager taskManager = AppContext.getTaskManager(); Iterator<ChannelMessageInfo> iter = savedMessages.values().iterator(); while (taskManager.shouldContinue() && iter.hasNext()) { ChannelMessageInfo messageInfo = iter.next(); if (messageInfo.isExpired()) { if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, "Removing saved message, channel:{0} " + "timestamp:{1}", HexDumper.toHexString(channelRefId), messageInfo.timestamp); } iter.remove(); dataService.removeObject(messageInfo); } else { break; } } if (!savedMessages.isEmpty()) { scheduleTask(); } } } private void scheduleTask() { TaskService taskService = ChannelServiceImpl.getTaskService(); if (isDurable) { taskService.scheduleTask(this, 1000L); } else { taskService.scheduleNonDurableTask(this, 1000L, true); } } /** * Creates a new message reaper and schedules it to run. * * @param channelRefId a channel ID * @param isDurable if {@code true} the task should be * scheduled as a durable task, otherwise it should be * scheduled as a non-durable task */ static void scheduleNewTask( BigInteger channelRefId, boolean isDurable) { (new SavedMessageReaper(channelRefId, isDurable)).scheduleTask(); } } /** * Reassigns the channel coordinator as follows: * * 1) Reassigns the channel's coordinator from the node specified by * the {@code failedCoordNodeId} to another server node (if there are * channel members), or the local node (if there are no channel * members) and rebinds the event queue to the new coordinator's key. * * 2) Sends out a 'serviceEventQueue' request to the new * coordinator to restart this channel's event processing. */ private void reassignCoordinator(long failedCoordNodeId) { DataService dataService = getDataService(); if (coordNodeId != failedCoordNodeId) { logger.log( Level.SEVERE, "attempt to reassign coordinator:{0} for channel:{1} " + "that is not the failed node:{2}", coordNodeId, failedCoordNodeId, this); return; } /* * Assign a new coordinator, and store event queue in new * coordinator's event queue map. */ dataService.markForUpdate(this); coordNodeId = chooseCoordinatorNode(); isCoordinatorReassigned = true; if (logger.isLoggable(Level.FINER)) { logger.log( Level.FINER, "channel:{0} reassigning coordinator from:{1} to:{2}", HexDumper.toHexString(channelRefId), failedCoordNodeId, coordNodeId); } EventQueue eventQueue = eventQueueRef.get(); getEventQueuesMap(coordNodeId). put(channelRefId.toString(), eventQueue); eventQueue.coordinatorReassigned(); /* * Send a 'serviceEventQueue' notification to the new coordinator. */ notifyServiceEventQueue(eventQueue); } /** * Chooses a node to be the new coordinator for this channel, and * returns the ID for the chosen node. If there is one or more * channel server(s) for this channel that are currently alive, this * method chooses one of those server nodes at random to be the new * coordinator. If there are no live channel servers for this channel, * then the local node is chosen to be the coordinator. * * This method should be called within a transaction. */ private long chooseCoordinatorNode() { if (!servers.isEmpty()) { int numServers = servers.size(); Long[] serverIds = servers.toArray(new Long[numServers]); int startIndex = random.nextInt(numServers); WatchdogService watchdogService = ChannelServiceImpl.getWatchdogService(); for (int i = 0; i < numServers; i++) { int tryIndex = (startIndex + i) % numServers; long candidateId = serverIds[tryIndex]; Node coordCandidate = watchdogService.getNode(candidateId); if (coordCandidate != null && coordCandidate.isAlive()) { return candidateId; } } } return getLocalNodeId(); } /** * Returns the local node's ID. */ private static long getLocalNodeId() { return ChannelServiceImpl.getLocalNodeId(); } /** * Returns the channel server for the specified {@code nodeId}. */ private static ChannelServer getChannelServer(long nodeId) { return ChannelServiceImpl.getInstance().getChannelServer(nodeId); } /** * Returns a set containing the node IDs of the channel servers for * this channel. */ private Set<Long> getServerNodeIds() { return new HashSet<Long>(servers); } /** * Removes the channel object, the channel listener wrapper (if we * created a wrapper for it), and the event queue and associated * binding from the data store. If {@code removeName} is {@code * true}, the channel's name binding is also removed. This method * is called when {@code leaveAll} is invoked on the channel (in * which case the channel's name binding is not removed) and is * called when the channel is closed (in which case the channel's * name binding is removed). */ private void removeChannel(boolean removeName) { DataService dataService = getDataService(); if (removeName) { getChannelsMap().removeOverride(name); } dataService.removeObject(this); if (listenerRef != null) { ChannelListener maybeWrappedListener = listenerRef.get(); if (maybeWrappedListener instanceof ManagedSerializable) { dataService.removeObject(maybeWrappedListener); } } BindingKeyedMap<EventQueue> eventQueuesMap = getEventQueuesMap(coordNodeId); eventQueuesMap.removeOverride(channelRefId.toString()); EventQueue eventQueue = eventQueueRef.get(); dataService.removeObject(eventQueue); if (isReliable()) { SavedMessageReaper.scheduleNewTask(channelRefId, true); } } /** * Returns {@code true} if this channel supports reliable message * delivery, otherwise returns {@code false}. */ private boolean isReliable() { return delivery.equals(Delivery.RELIABLE) || delivery.equals(Delivery.UNORDERED_RELIABLE); } /* -- Other classes -- */ /** * An iterator for {@code ClientSession}s of a given channel. */ private static class ClientSessionIterator implements Iterator<ClientSession> { /** The iterator for sessions. */ private Iterator<BigInteger> iterator; /** The client session to be returned by {@code next}. */ private ClientSession nextSession = null; /** * Constructs an instance of this class with the specified * {@code channelRefId}. */ ClientSessionIterator(Set<BigInteger> channelMembers) { iterator = channelMembers.iterator(); } /** {@inheritDoc} */ public boolean hasNext() { if (!iterator.hasNext()) { return false; } if (nextSession != null) { return true; } ClientSessionImpl session = (ClientSessionImpl) getObjectForId(iterator.next()); if (session == null) { return hasNext(); } else { nextSession = session.getWrappedClientSession(); return true; } } /** {@inheritDoc} */ public ClientSession next() { try { if (nextSession == null && !hasNext()) { throw new NoSuchElementException(); } return nextSession; } finally { nextSession = null; } } /** {@inheritDoc} */ public void remove() { throw new UnsupportedOperationException("remove is not supported"); } } /** * A wrapper for a {@code ChannelListener} that is serializable, * but not managed. */ private static class ManagedSerializableChannelListener extends ManagedSerializable<ChannelListener> implements ChannelListener { private static final long serialVersionUID = 1L; /** Constructs an instance with the specified {@code listener}. */ ManagedSerializableChannelListener(ChannelListener listener) { super(listener); } /** {@inheritDoc} */ public void receivedMessage( Channel channel, ClientSession sender, ByteBuffer message) { assert sender instanceof ClientSessionWrapper; get().receivedMessage(channel, sender, message); } } /** * The channel's event queue.<p> * * Each channel event (join, leave, send, close) is assigned a * <i>timestamp</i> when the event is added to the channel's event * queue. The current timestamp records the timestamp of the latest * send event that the queue has started to process. The initial event * timestamp is <code>1</code>. A send event increments the next * timestamp, so all future join and leave events will have a later * timestamp. Any join and leave events that are immediately prior to * the latest send event will share the send event's timestamp. <p> * * In order to implement reliable join, leave, send, and close events, * the event queue doesn't process one event until it has completed the * processing of all previous events, which means sucessfully * delivering those events to all nodes that need to be notified (for * example, in the case of a send event, that would be notifying all * nodes with sessions joined to the channel). When the event queue * assigns a timestamp to the next send event, the event queue has * already processed all previous join and leave events, up to and * including ones with that send event's timestamp. */ 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 channel. */ private final ManagedReference<ChannelImpl> channelRef; /** The managed reference to the managed queue. */ private final ManagedReference<ManagedQueue<ChannelEvent>> queueRef; /** The next timestamp to assign to an event. */ private long nextTimestamp = 1; /** The timestamp beyond which membership does not have to be * verified with the node to which a session is connected. */ private long coordinatorAssignmentTimestamp = 0; /** The timestamp for the last event processed. */ private long currentTimestamp = 0; /** * The number of bytes of the write buffer that are currently * available. */ private int writeBufferAvailable; /** * Constructs an event queue for the specified {@code channel}. */ EventQueue(ChannelImpl channel) { DataService dataService = getDataService(); channelRef = dataService.createReference(channel); queueRef = dataService.createReference( new ManagedQueue<ChannelEvent>()); writeBufferAvailable = channel.getWriteBufferCapacity(); } /** * Notifies this event queue that its channel's coordinator has * been reassigned and is considered to be recovering. This event * queue notes the "next timestamp" (to be assigned to an event) so * that the event queue can perform specific recovery actions for * events until it processes all events (if any) with timestamps * less than the timestamp at the moment the coordinator was * reassigned. */ void coordinatorReassigned() { getDataService().markForUpdate(this); coordinatorAssignmentTimestamp = isEmpty() ? 0 : nextTimestamp; } /** * Returns {@code true} if the coordinator is considered recovering * for the specified {@code timestamp}, and returns {@code false} * otherwise. * * @param timestamp an event timestamp * @return {@code true} if the coordinator is considered recovering * for the specified {@code timestamp} */ boolean isCoordinatorRecovering(long timestamp) { return timestamp < coordinatorAssignmentTimestamp; } /** * Attempts to enqueue the specified {@code event}, and returns * {@code true} if successful, and {@code false} otherwise. If * this node coordinates the channel and the channel's event queue * is empty, then start processing the event immediately. If the * event is successfully added to the queue, then sent a * notification to the channel's coordinator to service the event * queue. * * @param event the event * @param channel the channel instance * @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(ChannelEvent event, ChannelImpl channel) { boolean notifyServiceEventQueue = true; int cost = event.getCost(); if (cost > writeBufferAvailable) { throw new MessageRejectedException( "Not enough queue space: " + writeBufferAvailable + " bytes available, " + cost + " requested"); } if (channel.isCoordinator() && isEmpty()) { // The event queue is empty, and this node is the // coordinator for the channel's event queue, so process // the event now. if (startProcessingEvent(channel, event)) { // Event completed processing, so return success. return true; } // The event needs to be added to the head of the queue // (below) because it is not yet complete. There is no // need to notify the event queue because it will be // notified when the event has completed processing. notifyServiceEventQueue = false; } boolean success = getQueue().offer(event); if (success && (cost > 0)) { getDataService().markForUpdate(this); writeBufferAvailable -= cost; if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "{0} reserved {1,number,#} leaving {2,number,#}", this, cost, writeBufferAvailable); } } if (notifyServiceEventQueue) { channel.notifyServiceEventQueue(this); } return success; } /** * Returns the channel for this queue. */ ChannelImpl getChannel() { return channelRef.get(); } /** * Returns the channel ID for this queue. */ BigInteger getChannelRefId() { return channelRef.getId(); } /** * Returns the managed queue object. */ ManagedQueue<ChannelEvent> getQueue() { return queueRef.get(); } /** * Returns {@code true} if the event queue is empty. */ boolean isEmpty() { return getQueue().isEmpty(); } /** Returns the timestamp to assign to the next event. */ long getNextTimestamp() { return nextTimestamp; } /** * Returns the timestamp to assign to the next event, * then increments it. */ long getNextTimestampAndIncrement() { getDataService().markForUpdate(this); return nextTimestamp++; } long getCurrentTimestamp() { return currentTimestamp; } /** * Services the channel event queue. The coordinator processes * events as follows: <ul> * * <li> If the event at the head of the queue has not started * processing, then it marks the event's state as 'processing' by * invoking the event's {@code processing} method, and then * initiates processing by invoking the event's {@code * serviceEvent} method passing the channel instance. * * <li> If the event at the head of the queue has completed * processing (its {@code serviceEvent} method or {@code * isCompleted} method returns {@code true}), the event is removed * from the queue, and the next event can be serviced. * </ul> * * An event remains in the queue until it has completed processing. * Reliable events (such as join, leave, and reliable send events) * require a delivery acknowledgment in order to be reliable in the * face of coordinator crash. Therefore the event at the head of * the queue starts processing inside a transaction, performing any * necessary persistent updates, then performs non-transactional * actions after the transaction commits (such as delivering a * notification), and then marks the event itself as 'completed' * once the non-transactional actions have completed (such as a * notification being acknowledged). At this point, the event can * be removed from the queue, and the next event can be * serviced. <p> * * If the channel coordinator's node crashes while the event at the * head of the queue is being processed, the channel's new * coordinator restarts event processing. */ void serviceEventQueue() { ChannelImpl channel = getChannel(); if (!channel.checkCoordinator()) { logger.log( Level.WARNING, "Attempt at node:{0} channel:{1} to service events; " + "instead of current coordinator:{2}", getLocalNodeId(), HexDumper.toHexString(channel.channelRefId), channel.coordNodeId); return; } if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "coordinator:{0} channelId:{1}", getLocalNodeId(), HexDumper.toHexString(channel.channelRefId)); } ChannelServiceImpl channelService = ChannelServiceImpl.getInstance(); DataService dataService = getDataService(); /* * Process channel events */ int eventsPerTxn = channelService.eventsPerTxn; ManagedQueue<ChannelEvent> eventQueue = getQueue(); boolean completed = false; do { ChannelEvent event = eventQueue.peek(); if (event == null) { // No more events to process, so return. if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "coordinator:{0} channelId:{1} " + "no more events", getLocalNodeId(), HexDumper.toHexString(channel.channelRefId)); } return; } else if (event.isCompleted()) { // Remove completed event and get next event to // process. Return if there are no more events. if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "coordinator:{0} channelId:{1} " + "removing completed event:{2}", getLocalNodeId(), HexDumper.toHexString(channel.channelRefId), event); } eventQueue.poll(); event = eventQueue.peek(); if (event == null) { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "coordinator:{0} channelId:{1} " + "no more events", getLocalNodeId(), HexDumper.toHexString( channel.channelRefId)); } return; } } else if (event.isProcessing()) { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "coordinator:{0} channelId:{1} " + "event:{2} is already processing", getLocalNodeId(), HexDumper.toHexString(channel.channelRefId), event); } return; } int cost = event.getCost(); if (cost > 0) { 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); } } // Mark event as "processing", and then service event. if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "coordinator:{0} channelId:{1} " + "service event:{2}", getLocalNodeId(), HexDumper.toHexString(channel.channelRefId), event); } completed = startProcessingEvent(getChannel(), event); if (completed) { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "coordinator:{0} channelId:{1} " + "remove completed event:{2}", getLocalNodeId(), HexDumper.toHexString(channel.channelRefId), event); } eventQueue.poll(); } } while (completed && --eventsPerTxn > 0); if (eventQueue.peek() != null) { channelService. addServiceEventQueueTaskOnCommit(channel.channelRefId); } } /** * Starts processing the specified {@code event}, updating the * current message timestamp if the event is a channel "send" event. */ private boolean startProcessingEvent(ChannelImpl channel, ChannelEvent event) { event.processing(); if (event instanceof SendEvent) { getDataService().markForUpdate(this); currentTimestamp = event.timestamp; } return event.serviceEvent(channel); } /* -- Implement ManagedObjectRemoval -- */ /** {@inheritDoc} */ public void removingObject() { try { getDataService().removeObject(queueRef.get()); } catch (ObjectNotFoundException e) { // already removed. } } } /** * Represents an event on a channel. */ abstract static class ChannelEvent implements ManagedObject, Serializable { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** This event's timestamp. */ protected final long timestamp; /** * The event's completed status, indicating whether this event has * been completely processed and it is safe to service the next * event in the queue. */ private boolean completed = false; /** The ID of the coordinator node on which this event is being * processed. -1 indicates that event hasn't ever started * processing. */ protected long processingOnNodeId = -1; /** Constructs an instance with the specified {@code timestamp} */ ChannelEvent(long timestamp) { this.timestamp = timestamp; } /** * Services this event (taken from the head of the event queue) for * the specified {@code channel}, and returns {@code true} if the * event has completed processing and can be removed from the event * queue. */ abstract boolean serviceEvent(ChannelImpl channel); /** * Returns the cost of this event, which the {@code EventQueue} * may use to reject events when the total cost is too large. * The default implementation returns a cost of zero. * * @return the cost of this event */ int getCost() { return 0; } /** * Marks this event as completed. */ boolean completed() { logger.log(Level.FINEST, "completed event:{0}", this); try { getDataService().markForUpdate(this); } catch (ObjectNotFoundException e) { // markForUpdate can throw ONFE if this event has been // removed. if (logger.isLoggable(Level.WARNING)) { logger.logThrow( Level.WARNING, e, "Marking event:{0} completed throws", this); } } completed = true; return completed; } /** * Marks this event as being processed on the local node. */ void processing() { logger.log(Level.FINEST, "processing event:{0}", this); getDataService().markForUpdate(this); processingOnNodeId = getLocalNodeId(); } /** * Returns {@code true} if this event is completed and it is safe * to service the next event in the queue, otherwise returns {@code * false}. */ boolean isCompleted() { return completed; } /** * Returns {@code true} if this event is being processed on this * node, and {@code false} otherwise. Events are marked as * processing on a given node. Therefore, if this event is marked * as processing on a given coordinator node, and the channel's * coordinator is reassigned to another node and the coordinator * subsequently invokes this method on this event, the method will * return {@code false}. This indicates that the event has not yet * started processing on the new coordinator's node, and the * coordinator should (re)start this event's processing on the new * node. */ boolean isProcessing() { return processingOnNodeId == getLocalNodeId(); } } /** * A channel join event. */ private static class JoinEvent extends ChannelEvent { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** The session ID for the session to join the channel. */ private final BigInteger sessionRefId; /** * Constructs a join event with the specified {@code session}. */ JoinEvent(ClientSession session, EventQueue eventQueue) { super(eventQueue.getNextTimestamp()); sessionRefId = getSessionRefId(session); } /** {@inheritDoc} */ public boolean serviceEvent(final ChannelImpl channel) { assert isProcessing() && !isCompleted(); ClientSessionImpl session = (ClientSessionImpl) getObjectForId(sessionRefId); if (session == null) { logger.log( Level.FINE, "unable to obtain client session for ID:{0}", this); return completed(); } channel.addServerNodeId(getNodeId(session)); JoinNotifyTask task = new JoinNotifyTask(channel, this, session, sessionRefId); ChannelServiceImpl.getInstance().addChannelTaskOnCommit( channel.channelRefId, task); return isCompleted(); } /** {@inheritDoc} */ @Override public String toString() { return getClass().getName() + ": " + HexDumper.toHexString(sessionRefId); } } private abstract static class NotifyTask extends AbstractKernelRunnable { protected final ChannelServiceImpl channelService; protected final BigInteger channelRefId; protected final long timestamp; private final BigInteger eventRefId; /** * Constructs an instance. This constructor must be called within a * transaction. */ NotifyTask(ChannelImpl channel, ChannelEvent channelEvent) { super(null); this.channelService = ChannelServiceImpl.getInstance(); this.channelRefId = channel.channelRefId; this.eventRefId = getDataService().createReference(channelEvent).getId(); this.timestamp = channelEvent.timestamp; } /** * Returns the channel associated with this task, or null if the channel * no longer exists. This method must be invoked within a transaction. */ protected ChannelImpl getChannel() { return (ChannelImpl) getObjectForId(channelRefId); } /** * Marks the associated event complete and adds a task to * resume servicing the channel's event queue. This must be * called outside of a transaction. */ protected void completed() { try { channelService.runTransactionalTask( new AbstractKernelRunnable("MarkChannelEventCompleted") { public void run() { ChannelEvent event = (ChannelEvent) getObjectForId(eventRefId); if (event != null) { event.completed(); } else { // This shouldn't happen logger.log( Level.SEVERE, "channel:{0}: event removed before completed", HexDumper.toHexString(channelRefId)); } } }); } catch (Exception e) { // Transaction schedule will print out warning. } finally { channelService.addServiceEventQueueTask(channelRefId); } } /** * Removes the specified {@code nodeId} from the associated * channel. * * @param nodeId a node ID */ protected void removeNodeIdFromChannel(final long nodeId) { try { channelService.runTransactionalTask( new AbstractKernelRunnable("removeNodeIdFromChannel") { public void run() { ChannelImpl channel = getChannel(); if (channel != null) { channel.removeServerNodeId(nodeId); } } }); } catch (Exception e) { // Transaction scheduler will print out warning. } } } /** * A non-transactional task to send a notification to a session's * node, allowing for the possibility of the session relocating while * the notification is in transit. */ private abstract static class SessionNotifyTask extends NotifyTask { protected final String name; protected final Delivery delivery; protected final BigInteger sessionRefId; private final BigInteger eventQueueRefId; /** The session's node ID. Initialized during construction * and modified by calls to {@code removeMembershipIfNodeUnchanged} * and {@code remapMembershipIfRelocating} methods. */ protected volatile long sessionNodeId; /** * Constructs an instance. This constructor must be called within a * transaction. */ SessionNotifyTask(ChannelImpl channel, ChannelEvent channelEvent, ClientSessionImpl session, BigInteger sessionRefId) { super(channel, channelEvent); this.name = channel.name; this.delivery = channel.delivery; this.eventQueueRefId = channel.eventQueueRef.getId(); this.sessionRefId = sessionRefId; this.sessionNodeId = getNodeId(session); } /** * This must be called outside of a transaction. */ public void run() { channelService.checkNonTransactionalContext(); while (!channelService.shuttingDown()) { ChannelServer server = getChannelServer(sessionNodeId); if (server == null) { // If session's node hasn't changed, then it is // disconnected because its channel server is gone. if (updateSessionNodeId()) { continue; // relocating } else { break; // disconnected } } try { if (sendNotification(server)) { // Notification was successful break; } else { // If session's node hasn't changed, then it is // disconnected because it wasn't connected to the // node. if (updateSessionNodeId()) { continue; // relocating } else { break; // disconnected } } } catch (IOException e) { if (!channelService.isAlive(sessionNodeId)) { // If the session's node hasn't changed, then it is // disconnected because its node crashed. removeNodeIdFromChannel(sessionNodeId); if (updateSessionNodeId()) { continue; // relocating } else { break; // disconnected } } // Wait for transient situation to resolve. try { // TBD: make sleep time configurable? Thread.sleep(200); } catch (InterruptedException ie) { } } } // Mark event as completed and add task to resume // servicing the event queue. completed(); } /** * Sends a notification message to the specified channel {@code * server} and returns the result of the notification. This method is * invoked outside of a transaction. * * @return the result of the notification * @throws IOException if a communication problem occurs while sending * the notification */ protected abstract boolean sendNotification(ChannelServer server) throws IOException; /** * Returns {@code true} if when the session's node ID changes, it * should be added to the channel's set of node IDs, and returns * {@code false} otherwise. This method is invoked inside of a * transaction. * * @return {@code true} if when the session's node ID changes, it * should be added to the channel's set of node IDs, and * {@code false} otherwise */ protected abstract boolean addChangedSessionNodeId(); /** * Returns the client session associated with this task, or null if * the session no longer exists. This method must be invoked * within a transaction. */ protected ClientSessionImpl getSession() { return (ClientSessionImpl) getObjectForId(sessionRefId); } /** * Updates this task's {@code sessionNodeId} and returns {@code true} * if the session's node ID has changed, and returns {@code false} if * the session's node ID is unchanged or the session no longer * exists. If the session's node ID has changed, this method invokes * the {@code addChangedSessionNodeId} method, and if that method * returns {@code true}, the session's new node ID is added to the * channel's set of node IDs. This method may be invoked outside of a * transaction. * * @return {@code true} if the session's node ID is updated, otherwise * {@code false} */ protected final boolean updateSessionNodeId() { final long oldSessionNodeId = sessionNodeId; try { return channelService.runTransactionalCallable( new KernelCallable<Boolean>("updateSessionNodeId") { public Boolean call() { ClientSessionImpl session = getSession(); if (session != null) { sessionNodeId = getNodeId(session); } boolean updated = sessionNodeId != oldSessionNodeId; if (updated && addChangedSessionNodeId()) { ChannelImpl channel = getChannel(); if (channel != null) { channel.addServerNodeId(sessionNodeId); } else { updated = false; } } return updated; } }); } catch (Exception e) { // Transaction scheduler will print out warning. return false; } } /** * Returns the event queue's latest timestamp. This method may be * invoked outside of a transaction. */ protected long getEventQueueTimestamp() { try { return channelService.runTransactionalCallable( new KernelCallable<Long>("getEventQueueTimestamp") { public Long call() { EventQueue eventQueue = (EventQueue) getObjectForId(eventQueueRefId); if (eventQueue != null) { return eventQueue.getNextTimestamp(); } else { return -1L; } } }); } catch (Exception e) { return -1L; } } } /** * A non-transactional task to send a join notification to a session's * node, allowing for the possibility of the session relocating while * the join message is in transit. */ private static class JoinNotifyTask extends SessionNotifyTask { /** * Constructs an instance. This constructor must be called within a * transaction. */ JoinNotifyTask(ChannelImpl channel, JoinEvent joinEvent, ClientSessionImpl session, BigInteger sessionRefId) { super(channel, joinEvent, session, sessionRefId); } /** {@inheritDoc} <p> Sends a join notification. */ protected boolean sendNotification(ChannelServer server) throws IOException { /* * Send "join" notification to session's server with a * timestamp of the last message sent on the channel (which may * be zero if no messages have been sent on the channel). The * timestamp of the last message sent on the channel is one * less than the event's timestamp. */ boolean success = server.join(name, channelRefId, (byte) delivery.ordinal(), timestamp - 1, sessionRefId); if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, "Sent join, name:{0} channel:{1} session:{2} " + "coordinator:{3} returned {4}", name, HexDumper.toHexString(channelRefId), HexDumper.toHexString(sessionRefId), getLocalNodeId(), success); } return success; } /** {@inheritDoc} <p> * * A join event requires that a changed session's node ID be added to * the channel's set of server node IDs. */ protected boolean addChangedSessionNodeId() { return true; } /** {@inheritDoc} */ protected void completed() { long eventQueueTimestamp = getEventQueueTimestamp(); if (eventQueueTimestamp > timestamp) { channelService.cacheMembershipEvent( MembershipEventType.JOIN, channelRefId, sessionRefId, timestamp, eventQueueTimestamp); } super.completed(); } } /** * A channel leave event. */ private static class LeaveEvent extends ChannelEvent { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; private final BigInteger sessionRefId; /** * Constructs a leave event with the specified {@code session}. */ LeaveEvent(ClientSession session, EventQueue eventQueue) { super(eventQueue.getNextTimestamp()); sessionRefId = getSessionRefId(session); } /** {@inheritDoc} */ public boolean serviceEvent(ChannelImpl channel) { assert isProcessing() && !isCompleted(); ClientSessionImpl session = (ClientSessionImpl) getObjectForId(sessionRefId); if (session == null) { logger.log( Level.FINE, "unable to obtain client session for ID:{0}", this); return completed(); } LeaveNotifyTask task = new LeaveNotifyTask(channel, this, session, sessionRefId); ChannelServiceImpl.getInstance().addChannelTaskOnCommit( channel.channelRefId, task); return isCompleted(); } /** {@inheritDoc} */ @Override public String toString() { return getClass().getName() + ": " + HexDumper.toHexString(sessionRefId); } } /** * A non-transactional task to send a join notification to a session's * node, allowing for the possibility of the session relocating while * the join message is in transit. */ private static class LeaveNotifyTask extends SessionNotifyTask { /** * Constructs an instance. This constructor must be called within a * transaction. */ LeaveNotifyTask(ChannelImpl channel, LeaveEvent leaveEvent, ClientSessionImpl session, BigInteger sessionRefId) { super(channel, leaveEvent, session, sessionRefId); } /** {@inheritDoc} <p> Sends a leave notification. */ protected boolean sendNotification(ChannelServer server) throws IOException { boolean success = server.leave(channelRefId, timestamp, sessionRefId); if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, "Sent leave, channel:{0} session:{1} " + "coordinator:{2} returned {3}", HexDumper.toHexString(channelRefId), HexDumper.toHexString(sessionRefId), getLocalNodeId(), success); } return success; } /** {@inheritDoc} <p> * * A leave event should not have a changed session's node ID * added to the channel's set of server node IDs. */ protected boolean addChangedSessionNodeId() { return false; } /** {@inheritDoc} */ protected void completed() { long eventQueueTimestamp = getEventQueueTimestamp(); if (eventQueueTimestamp > timestamp) { channelService.cacheMembershipEvent( MembershipEventType.LEAVE, channelRefId, sessionRefId, timestamp, eventQueueTimestamp); } super.completed(); } } /** * A channel send event. */ static class SendEvent extends ChannelEvent { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** The channel message. */ private final byte[] message; /** The sender's session ID, or null. */ private final BigInteger senderRefId; /** Indicates whether the sender is known to be a channel member * when this event was constructed. */ private boolean isChannelMember; /** * Constructs a send event with the given {@code senderRefId} and * {@code message}. * * @param senderRefId a sender's session ID, or {@code null} * @param message a message * @param eventQueue the channel's event queue * @param isChannelMember {@code true} if the sender is currently * known to be a member of the associated channel */ SendEvent( BigInteger senderRefId, byte[] message, EventQueue eventQueue, boolean isChannelMember) { super(eventQueue.getNextTimestampAndIncrement()); this.senderRefId = senderRefId; this.message = message; this.isChannelMember = isChannelMember; } /** {@inheritDoc} */ public boolean serviceEvent(ChannelImpl channel) { assert isProcessing() && !isCompleted(); ChannelServiceImpl channelService = ChannelServiceImpl.getInstance(); if (senderRefId != null) { /* * Sender is a client, so verify that the sending session * is a member of the channel. */ ClientSessionImpl sender = (ClientSessionImpl) getObjectForId(senderRefId); if (sender == null) { if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, "send attempt by disconnected session:{0} " + "to channel:{1}", HexDumper.toHexString(senderRefId), channel); } return completed(); } else { EventQueue queue = channel.eventQueueRef.get(); if (queue.isCoordinatorRecovering(timestamp)) { getDataService().markForUpdate(this); isChannelMember = isChannelMemberRemoteCheck( channel.channelRefId, senderRefId, getNodeId(sender), timestamp); } if (!channelService.isChannelMember( channel.channelRefId, senderRefId, isChannelMember, timestamp)) { if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, "send attempt by non-member session:{0} " + "to channel:{1}", HexDumper.toHexString( senderRefId), channel); } return completed(); } } } /* * Enqueue a channel task to forward the message to the * channel's servers for delivery. */ SendNotifyTask task = new SendNotifyTask(channel, this); ChannelServiceImpl.getInstance().addChannelTaskOnCommit( channel.channelRefId, task); /* * If the message is reliable, store message for a period of * time so that relocating client sessions belonging to this * channel can obtain messages sent while those sessions are * relocating, otherwise, mark this event as completed. */ if (channel.isReliable()) { channel.saveMessage(message, timestamp); } else { completed(); } return isCompleted(); } /** Use the message length as the cost for sending messages. * * Only return a non-zero cost if the message hasn't started * processing yet so that the cost calculation can be * idempotent. */ @Override int getCost() { return processingOnNodeId == -1 ? message.length : 0; } /** {@inheritDoc} */ @Override public String toString() { return getClass().getName(); } } /** * A non-transactional task to transmit a "send" notification to * each of the channel's server nodes so that each server node * can deliver a channel message to the channel's respective * members. When all the appropriate channel servers have been * notified, this task marks the associated ChannelEvent complete * (within a transaction). */ private static class SendNotifyTask extends NotifyTask { private final Set<Long> serverNodeIds; private final byte[] message; private final boolean isReliable; /** * Constructs an instance with the specified {@code channel} * and {@code sendEvent}. */ SendNotifyTask(ChannelImpl channel, SendEvent sendEvent) { super(channel, sendEvent); this.serverNodeIds = channel.servers; this.message = sendEvent.message; this.isReliable = channel.isReliable(); } /** {@inheritDoc} */ public void run() { try { /* * Send "send" notification to channel's servers. */ for (final long nodeId : serverNodeIds) { boolean success = channelService.runIoTask( new IoRunnable() { public void run() throws IOException { ChannelServer server = getChannelServer(nodeId); if (server != null) { server.send(channelRefId, message, timestamp); } } }, nodeId); if (!success) { // Server node has failed, so remove it from // channel's server list. removeNodeIdFromChannel(nodeId); } } } finally { if (isReliable) { // Only a reliable send event need to be marked // completed. An unreliable send is marked completed // when it is processed by the event queue (before its // corresponding SendNotifyEvent is run). completed(); } } } } /** * A channel close event, used for closing a channel or removing * all members from the channel (as a result of a "leaveAll" * request). If a channel is being closed permanently, its name * binding is removed from the data service. If the channel is * being cleared of its membership (as a result of "leaveAll"), * then its name binding is being used to refer to a * ChannelWrapper with a new channel instance, so the name * binding is retained. */ private static class CloseEvent extends ChannelEvent { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** A flag to indicate whether the channel's name binding * should be removed. */ private final boolean removeName; /** * Constructs a close event. If {@code removeName} is {@code true}, * the channel is truly being closed, and its channel name * binding will be removed. If {@code removeName} is {@code * false}, then {@code leaveAll} was invoked on the channel, so * the channel's old persistent structures will be removed, but the * channel's name binding will remain, still referring to the * original {@code ChannelWrapper} whose underlying reference * was modified to refer to a newly created channel. * * @param removeName {@code true} if the channel's name binding * should be removed when the channel is closed */ CloseEvent(boolean removeName, EventQueue eventQueue) { super(eventQueue.getNextTimestamp()); this.removeName = removeName; } /** {@inheritDoc} */ public boolean serviceEvent(ChannelImpl channel) { assert isProcessing() && !isCompleted(); CloseNotifyTask task = new CloseNotifyTask(channel, removeName); ChannelServiceImpl.getInstance().addChannelTaskOnCommit( channel.channelRefId, task); return false; } /** {@inheritDoc} */ @Override public String toString() { return getClass().getName(); } } /** * A non-transactional task (with transactional components) to send a * "close" notification to each of the channel's server nodes so that * each server node can send the channel's respective members a * leave notification. This task also removes the channel's * persistent data when the notifications have been delivered. If * {@code removeName}, specified during construction, is {@code true} * the channel's name binding is removed along with the channel's * persistent data, otherwise the channel's name binding is not * removed. */ private static class CloseNotifyTask extends AbstractKernelRunnable { private final BigInteger channelRefId; private final Set<Long> serverNodeIds; private final boolean removeName; // FIXME: This is a kludge for now. private static final long timestamp = Long.MAX_VALUE; /** * Constructs an instance with the specified {@code channel}. If * {@code removeName} is {@code true}, the channel's name binding * is removed along with its persistent data. */ CloseNotifyTask(ChannelImpl channel, boolean removeName) { super(null); this.channelRefId = channel.channelRefId; this.serverNodeIds = channel.servers; this.removeName = removeName; } public void run() { final ChannelServiceImpl channelService = ChannelServiceImpl.getInstance(); /* * Send "close" notification to channel's servers. */ for (final long nodeId : serverNodeIds) { channelService.runIoTask( new IoRunnable() { public void run() throws IOException { ChannelServer server = getChannelServer(nodeId); if (server != null) { server.close(channelRefId, timestamp); } } }, nodeId); } /* * Notify local channel service to clean up this channel's * coordinator's transient data. */ channelService.closedChannel(channelRefId); /* * Remove channel's persistent data and, optionally, the * channel's name binding. */ try { channelService.runTransactionalTask( new AbstractKernelRunnable("RemoveClosedChannel") { public void run() { ChannelImpl channel = (ChannelImpl) getObjectForId(channelRefId); if (channel != null) { channel.removeChannel(removeName); } else { // This shouldn't happen logger.log( Level.SEVERE, "channel:{0} removed before closed", HexDumper.toHexString(channelRefId)); } } }); } catch (Exception e) { // Transaction scheduler will print out warning. } } } /** * Returns the event queue for the channel that has the specified * {@code channelRefId} and coordinator {@code nodeId}. */ private static EventQueue getEventQueue( long nodeId, BigInteger channelRefId) { EventQueue eventQueue = getEventQueuesMap(nodeId).get(channelRefId.toString()); if (eventQueue == null) { logger.log( Level.WARNING, "Event queue for channel:{0} does not exist", HexDumper.toHexString(channelRefId)); } return eventQueue; } /* -- Static method invoked by ChannelServiceImpl -- */ /** * Handles a channel {@code message} that the specified {@code sender} * is sending on the channel with the specified {@code channelRefId}. * * @param channelRefId the channel ID, as a {@code BigInteger} * @param sender the client session sending the channel message * @param message the channel message */ static void handleChannelMessage( BigInteger channelRefId, ClientSession sender, ByteBuffer message) { assert sender instanceof ClientSessionWrapper; ChannelImpl channel = (ChannelImpl) getObjectForId(channelRefId); if (channel != null) { channel.receivedMessage(sender, message); } else { // Ignore message received for unknown channel. if (logger.isLoggable(Level.FINE)) { logger.log( Level.FINE, "Dropping message:{0}: from:{1} for unknown channel: {2}", HexDumper.format(message), sender, HexDumper.toHexString(channelRefId)); } } } /** * Services the event queue for the channel with the specified {@code * channelRefId}. */ static void serviceEventQueue(BigInteger channelRefId) { EventQueue eventQueue = getEventQueue(getLocalNodeId(), channelRefId); if (eventQueue != null) { eventQueue.serviceEventQueue(); } } /** * Returns an iterator for channels that are coordinated on the node with * the specified {@code nodeId}. */ private static Iterator<String> getChannelsIterator(long nodeId) { return getEventQueuesMap(nodeId).keySet().iterator(); } /** * A persistent task to reassign channel coordinators on a failed node * to another node. In a single task, only one failed coordinator is * reassigned. A task for one coordinator schedules a task for the * next reassignment, if there are coordinators on the failed node left * to be reassigned. */ static class ReassignCoordinatorsTask implements Task, Serializable { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** * The node ID of the failed node. * @serial */ private final long failedNodeId; /** * The iterator for channels on the failed node. * @serial */ private final Iterator<String> channelIter; /** * Constructs an instance of this class with the specified * {@code failedNodeId}. */ ReassignCoordinatorsTask(long failedNodeId) { this.failedNodeId = failedNodeId; this.channelIter = getChannelsIterator(failedNodeId); } /** * Reassigns the next coordinator for the {@code failedNodeId} to * another node with member sessions (or the local node if there * are no member sessions), and then reschedules this task to * reassign the next coordinator. If there are no more * coordinators for the specified {@code failedNodeId}, then no * action is taken. */ public void run() { if (!channelIter.hasNext()) { return; } WatchdogService watchdogService = ChannelServiceImpl.getWatchdogService(); TaskService taskService = ChannelServiceImpl.getTaskService(); BigInteger channelRefId = new BigInteger(channelIter.next()); channelIter.remove(); ChannelImpl channel = (ChannelImpl) getObjectForId(channelRefId); if (channel != null) { channel.reassignCoordinator(failedNodeId); /* * If other channel servers have failed, remove the failed * server node ID from the channel too. This covers the * case where a channel coordinator (informed of a node * failure) fails before it has a chance to schedule a task * to remove the server node ID for another failed node * (cascading failure during recovery). */ for (long serverNodeId : channel.getServerNodeIds()) { Node serverNode = watchdogService.getNode(serverNodeId); if (serverNode == null || !serverNode.isAlive()) { channel.removeServerNodeId(serverNodeId); } } } /* * Schedule a task to reassign the next channel coordinator, or * if done with coordinator reassignment, remove the recovered * node's mapping from the node-to-event-queues map. */ if (channelIter.hasNext()) { taskService.scheduleTask(this); } } } /** * A persistent task to remove a failed node, from locally coordinated * channels. This task handles a single locally-coordinated channel, * and then schedules another task to handle the next one. */ static class RemoveFailedNodeFromLocalChannelsTask implements Task, Serializable { /** The serialVersionUID for this class. */ private static final long serialVersionUID = 1L; /** The failed node. */ private final long failedNodeId; /** The iterator for locally-coordinated channels.*/ private final Iterator<String> iter; /** * Constructs an instance with the specified {@code localNodeId} * and {@code failedNodeId}. */ RemoveFailedNodeFromLocalChannelsTask( long localNodeId, long failedNodeId) { this.failedNodeId = failedNodeId; this.iter = getChannelsIterator(localNodeId); } /** * Gets the next locally-coordinated channel, removes the failed * node from that channel, and reschedules this task to handle the * next locally-coordinated channel. If there are no more * locally-coordinated channels, then this task takes no action. */ public void run() { if (iter == null || !iter.hasNext()) { return; } BigInteger channelRefId = new BigInteger(iter.next()); ChannelImpl channel = (ChannelImpl) getObjectForId(channelRefId); if (channel != null) { channel.removeServerNodeId(failedNodeId); } // Schedule a task to remove failed node from next // locally coordinated channel. if (iter.hasNext()) { ChannelServiceImpl.getTaskService().scheduleTask(this); } } } /** * Checks with the {@code ChannelServer} on the node with the specified * {@code nodeId} whether the session with the specified {@code * sessionRefId} is a member of the channel with the specified {@code * channelRefId}, and returns {@code true} if the session is a member * and returns {@code false} otherwise. * * @param channelRefId a channel ID * @param sessionRefId a session ID * @param nodeId the session's node ID * @param timestamp the requesting event's timestamp */ private static boolean isChannelMemberRemoteCheck( BigInteger channelRefId, BigInteger sessionRefId, long nodeId, long timestamp) { try { ChannelServer server = getChannelServer(nodeId); MembershipStatus status = server.isMember(channelRefId, sessionRefId); switch (status) { case MEMBER: return true; case NON_MEMBER: case UNKNOWN: // The return value for UNKNOWN is correct only if the // session is obtained in a transaction while this // method is invoked (which would mean the session's // node can't change due to relocation). Otherwise, we // would need to resample the session's node ID to see // if it changed. If it hasn't changed, then it is // disconnected, otherwise, need to check membership // with the new session's new node. return false; default: throw new AssertionError(); } } catch (IOException e) { // The session's node can't be contacted, so the session // must be disconnected. } return false; } }