// // $Id$ // // Narya library - tools for developing networked games // Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved // http://www.threerings.net/code/narya/ // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // This library 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 // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package com.threerings.presents.peer.server; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import com.google.common.base.Function; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.inject.Inject; import com.google.inject.Injector; import com.samskivert.util.ArrayIntSet; import com.samskivert.util.ChainedResultListener; import com.samskivert.util.Interval; import com.samskivert.util.Invoker; import com.samskivert.util.Lifecycle; import com.samskivert.util.ObserverList; import com.samskivert.util.ResultListener; import com.samskivert.util.ResultListenerList; import com.samskivert.util.Tuple; import com.samskivert.jdbc.RepositoryUnit; import com.samskivert.jdbc.WriteOnlyUnit; import com.threerings.io.ObjectInputStream; import com.threerings.io.ObjectOutputStream; import com.threerings.io.Streamable; import com.threerings.util.Name; import com.threerings.presents.dobj.DObject; import com.threerings.presents.dobj.EntryAddedEvent; import com.threerings.presents.dobj.EntryRemovedEvent; import com.threerings.presents.dobj.EntryUpdatedEvent; import com.threerings.presents.dobj.ObjectAccessException; import com.threerings.presents.dobj.SetListener; import com.threerings.presents.dobj.Subscriber; import com.threerings.presents.annotation.MainInvoker; import com.threerings.presents.client.Client; import com.threerings.presents.client.InvocationService; import com.threerings.presents.data.ClientObject; import com.threerings.presents.server.ClientManager; import com.threerings.presents.server.InvocationException; import com.threerings.presents.server.InvocationManager; import com.threerings.presents.server.PresentsDObjectMgr; import com.threerings.presents.server.PresentsSession; import com.threerings.presents.server.ReportManager; import com.threerings.presents.server.ServiceAuthenticator; import com.threerings.presents.server.SessionFactory; import com.threerings.presents.server.net.ConnectionManager; import com.threerings.presents.peer.client.PeerService; import com.threerings.presents.peer.data.ClientInfo; import com.threerings.presents.peer.data.NodeObject; import com.threerings.presents.peer.data.PeerAuthName; import com.threerings.presents.peer.net.PeerCreds; import com.threerings.presents.peer.server.persist.NodeRecord; import com.threerings.presents.peer.server.persist.NodeRepository; import static com.threerings.presents.Log.log; /** * Manages connections to the other nodes in a Presents server cluster. Each server maintains a * client connection to the other servers and subscribes to the {@link NodeObject} of all peer * servers and uses those objects to communicate cross-node information. */ public abstract class PeerManager implements PeerProvider, ClientManager.ClientObserver, Lifecycle.ShutdownComponent { /** * Used by entities that wish to know when cached data has become stale due to a change on * one of our peer servers. */ public static interface StaleCacheObserver { /** * Called when some possibly cached data has changed on one of our peer servers. */ void changedCacheData (Streamable data); } /** * Used by entities that wish to know when this peer has been forced into immediately releasing * a lock. */ public static interface DroppedLockObserver { /** * Called when this node has been forced to drop a lock. */ void droppedLock (NodeObject.Lock lock); } /** * Wraps an operation that needs a shared resource lock to be acquired before it can be * performed, and released after it completes. Used by {@link PeerManager#performWithLock}. */ public static interface LockedOperation { /** * Called when the resource lock was acquired successfully. The lock will be released * immediately after this function call finishes. */ void run (); /** * Called when the resource lock was not acquired successfully, with the name of the peer * who is holding the lock (or null in case of a generic failure). */ void fail (String peerName); } /** * Encapsulates code that is meant to be executed one or more servers. * *

Note well: the action you provide is serialized and sent to the server to which * the member is currently connection. This means you MUST NOT instantiate a NodeAction * anonymously because that will maintain an implicit non-transient reference to its containing * class which will then also be serialized (assuming it is even serializable). */ public static abstract class NodeAction implements Streamable { /** Returns true if this action should be executed on the specified node. This will be * called on the originating server to decide whether or not to deliver the action to the * server in question. */ public abstract boolean isApplicable (NodeObject nodeobj); /** Invokes the action on the target server. */ public void invoke () { try { execute(); } catch (Throwable t) { log.warning(getClass().getName() + " failed.", t); } } protected abstract void execute (); } /** * Encapsulates code that is meant to be executed one or more servers and return a result. * *

Note well: the request you provide is serialized and sent to the server to which * the member is currently connection. This means you MUST NOT instantiate a NodeRequest * anonymously because that will maintain an implicit non-transient reference to its containing * class which will then also be serialized (assuming it is even serializable). */ public static abstract class NodeRequest implements Streamable { /** Returns true if this request should be executed on the specified node. This will be * called on the originating server to decide whether or not to deliver the request to the * server in question. */ public abstract boolean isApplicable (NodeObject nodeobj); /** Invokes the action on the target server. */ public void invoke (final InvocationService.ResultListener listener) { try { execute(listener); } catch (Throwable t) { log.warning(getClass().getName() + " failed.", t); } } protected abstract void execute (InvocationService.ResultListener listener); } /** Returned by {@link #getStats}. */ public static class Stats implements Cloneable { /** The number of locks this node has acquired. */ public long locksAcquired; /** The number of milliseconds spent waiting to acquire locks. */ public long lockAcquireWait; /** The number of locks this node has released. */ public long locksReleased; /** The number of locks this node has had hijacked. */ public long locksHijacked; /** The number of lock requests that have timed out. */ public long lockTimeouts; /** The number of node actions we've invoked. */ public long nodeActionsInvoked; /** The total number of messages received from all of our peers. This is updated on the * conmgr thread which is why it's atomic. */ public AtomicLong peerMessagesIn = new AtomicLong(0); /** The total number of messages sent to all of our peers. */ public long peerMessagesOut; @Override public Stats clone () { try { return (Stats)super.clone(); } catch (Exception e) { throw new RuntimeException(e); } } } /** * Creates an uninitialized peer manager. */ @Inject public PeerManager (Lifecycle cycle) { cycle.addComponent(this); } /** * Returns the distributed object that represents this node to its peers. */ public NodeObject getNodeObject () { return _nodeobj; } /** * Returns an iterable over our node object and that of our peers. */ public Iterable getNodeObjects () { return Iterables.filter( Iterables.concat(Collections.singleton(_nodeobj), Iterables.transform(_peers.values(), GET_NODE_OBJECT)), Predicates.notNull()); } /** * Initializes this peer manager to connect to all other nodes in the NODES table. See * {@link #init(String, String, String, String, int, String)} for the behavior of the method * and the meaning of its parameters. */ public void init (String nodeName, String sharedSecret, String hostName, String publicHostName, int port) { init(nodeName, sharedSecret, hostName, publicHostName, port, ""); } /** * Initializes this peer manager and initiates the process of connecting to its peer nodes. * This will also reconfigure the ConnectionManager and ClientManager with peer related bits, * so this should not be called until after the main server has set up its client * factory and authenticator. * * @param nodeName this node's unique name. * @param sharedSecret a shared secret used to allow the peers to authenticate with one * another. * @param hostName the DNS name of the server running this node. * @param publicHostName if non-null, a separate public DNS hostname by which the node is to * be known to normal clients (we may want inter-peer communication to take place over a * different network than the communication between real clients and the various peer * servers). * @param port the port on which other nodes should connect to us. * @param nodeNamespace The namespace for nodes to peer with. This node will connect to other * nodes with the same prefix from the NODES table. */ public void init (String nodeName, String sharedSecret, String hostName, String publicHostName, int port, String nodeNamespace) { _nodeNamespace = nodeNamespace; _nodeName = nodeName; _sharedSecret = sharedSecret; // wire ourselves into the server _conmgr.addChainedAuthenticator( new ServiceAuthenticator(PeerCreds.class, PeerAuthName.class) { @Override protected boolean areValid (PeerCreds creds) { return creds.areValid(_sharedSecret); } }); _clmgr.addSessionFactory( SessionFactory.newSessionFactory(PeerCreds.class, PeerSession.class, PeerAuthName.class, PeerClientResolver.class)); // create our node object _nodeobj = _omgr.registerObject(createNodeObject()); _nodeobj.setNodeName(nodeName); _nodeobj.setBootStamp(System.currentTimeMillis()); // register ourselves with the node table _self = new NodeRecord( _nodeName, hostName, (publicHostName == null) ? hostName : publicHostName, port); _invoker.postUnit(new WriteOnlyUnit("registerNode(" + _self + ")") { @Override public void invokePersist () throws Exception { _noderepo.updateNode(_self); } }); // set the invocation service _nodeobj.setPeerService(_invmgr.registerDispatcher(new PeerDispatcher(this))); // register ourselves as a client observer _clmgr.addClientObserver(this); // and start our peer refresh interval (this lives for the lifetime of the server) _omgr.newInterval(new Runnable() { public void run () { refreshPeers(); } }).schedule(5000L, 60*1000L); // give derived classes an easy way to get in on the init action didInit(); } /** * Returns true if the supplied peer credentials match our shared secret. */ public boolean isAuthenticPeer (PeerCreds creds) { return creds.areValid(_sharedSecret); } /** * Locates the client with the specified name. Returns null if the client is not logged onto * any peer. */ public ClientInfo locateClient (final Name key) { return lookupNodeDatum(new Function() { public ClientInfo apply (NodeObject nodeobj) { return nodeobj.clients.get(key); } }); } /** * Locates a datum from among the set of peer {@link NodeObject}s. Objects are searched in * arbitrary order and the first non-null value returned by the supplied lookup operation is * returned to the caller. Null if all lookup operations returned null. */ public T lookupNodeDatum (Function op) { for (T value : Iterables.filter(Iterables.transform(getNodeObjects(), op), Predicates.notNull())) { return value; } return null; } /** * Invokes the supplied function on all node objects (except the local node). A caller * that needs to call an invocation service method on a remote node should use this mechanism * to locate the appropriate node (or nodes) and call the desired method. * * @return the number of times the invoked function returned true. */ public int invokeOnNodes (Function,Boolean> func) { int invoked = 0; for (PeerNode peer : _peers.values()) { if (peer.nodeobj != null) { if (func.apply(Tuple.newTuple(peer.getClient(), peer.nodeobj))) { invoked++; } } } return invoked; } /** * Invokes the supplied action on this and any other server that it indicates is appropriate. * The action will be executed on the distributed object thread, but this method does not need * to be called from the distributed object thread. */ public void invokeNodeAction (final NodeAction action) { invokeNodeAction(action, null); } /** * Invokes the supplied action on this and any other server that it indicates is appropriate. * The action will be executed on the distributed object thread, but this method does not need * to be called from the distributed object thread. * * @param onDropped a runnable to be executed if the action was not invoked on the local server * or any peer node due to failing to match any of the nodes. The runnable will be executed on * the dobj event thread. */ public void invokeNodeAction (final NodeAction action, final Runnable onDropped) { // if we're not on the dobjmgr thread, get there if (!_omgr.isDispatchThread()) { _omgr.postRunnable(new Runnable() { public void run () { invokeNodeAction(action, onDropped); } }); return; } // first serialize the action to make sure we can byte[] actionBytes = flattenAction(action); // invoke the action on our local server if appropriate boolean invoked = false; if (action.isApplicable(_nodeobj)) { invokeAction(null, actionBytes); invoked = true; } // now send it to any remote node that is also appropriate for (PeerNode peer : _peers.values()) { if (peer.nodeobj != null && action.isApplicable(peer.nodeobj)) { peer.nodeobj.peerService.invokeAction(peer.getClient(), actionBytes); invoked = true; } } // if we did not invoke the action on any node, call the onDropped handler if (!invoked && onDropped != null) { onDropped.run(); } if (invoked) { _stats.nodeActionsInvoked++; // stats! } } /** * Invokes a node action on a specific node without executing {@link * NodeAction#isApplicable} to determine whether the action is applicable. */ public void invokeNodeAction (String nodeName, NodeAction action) { PeerNode peer = _peers.get(nodeName); if (peer != null) { peer.nodeobj.peerService.invokeAction(peer.getClient(), flattenAction(action)); } else if (nodeName.equals(_nodeName)) { invokeAction(null, flattenAction(action)); } } /** * Invokes the supplied request on all servers in parallel. The request will execute on the * distributed object thread, but this method does not need to be called from there. * * If any one node reports failure, this function reports failure. If all nodes report success, * this function will report success. */ public void invokeNodeRequest ( final NodeRequest request, final NodeRequestsListener listener) { // if we're not on the dobjmgr thread, get there if (!_omgr.isDispatchThread()) { _omgr.postRunnable(new Runnable() { public void run () { invokeNodeRequest(request, listener); } }); return; } // serialize the action to make sure we can byte[] requestBytes = flattenRequest(request); // build a set of node names (including the local node) to which to send the request final Set nodes = Sets.newHashSet(); if (request.isApplicable(_nodeobj)) { nodes.add(_nodeobj.nodeName); } for (PeerNode peer : _peers.values()) { if (request.isApplicable(peer.nodeobj)) { nodes.add(peer.getNodeName()); } } if (nodes.isEmpty()) { listener.requestsProcessed(new NodeRequestsResultImpl()); return; } final Map results = Maps.newHashMap(); final Map failures = Maps.newHashMap(); final AtomicInteger completedNodes = new AtomicInteger(); for (final String node : nodes) { invokeNodeRequest(node, requestBytes, new InvocationService.ResultListener() { public void requestProcessed (Object result) { // check off this node's successful response @SuppressWarnings("unchecked") T castResult = (T) result; results.put(node, castResult); nodeDone(node); } public void requestFailed (String cause) { failures.put(node, cause); nodeDone(node); } protected void nodeDone (String node) { if (completedNodes.incrementAndGet() == nodes.size()) { // if all nodes have responded, let caller know listener.requestsProcessed(new NodeRequestsResultImpl(results, failures)); } } }); } } /** * Invokes a node request on a specific node and returns the result through the listener. */ public void invokeNodeRequest (String nodeName, NodeRequest request, InvocationService.ResultListener listener) { invokeNodeRequest(nodeName, flattenRequest(request), listener); } /** * Initiates a proxy on an object that is managed by the specified peer. The object will be * proxied into this server's distributed object space and its local oid reported back to the * supplied result listener. * *

Note that proxy requests do not stack like subscription requests. Only one * entity must issue a request to proxy an object and that entity must be responsible for * releasing the proxy when it knows that there are no longer any local subscribers to the * object. */ public void proxyRemoteObject ( String nodeName, int remoteOid, final ResultListener listener) { final Client peer = getPeerClient(nodeName); if (peer == null) { String errmsg = "Have no connection to peer [node=" + nodeName + "]."; listener.requestFailed(new ObjectAccessException(errmsg)); return; } final Tuple key = Tuple.newTuple(nodeName, remoteOid); if (_proxies.containsKey(key)) { String errmsg = "Cannot proxy already proxied object [key=" + key + "]."; listener.requestFailed(new ObjectAccessException(errmsg)); return; } // issue a request to subscribe to the remote object peer.getDObjectManager().subscribeToObject(remoteOid, new Subscriber() { public void objectAvailable (T object) { // make a note of this proxy mapping _proxies.put(key, new Tuple, DObject>(this, object)); // map the object into our local oid space _omgr.registerProxyObject(object, peer.getDObjectManager()); // then tell the caller about the (now remapped) oid listener.requestCompleted(object.getOid()); } public void requestFailed (int oid, ObjectAccessException cause) { listener.requestFailed(cause); } }); } /** * Unsubscribes from and clears a proxied object. The caller must be sure that there are no * remaining subscribers to the object on this local server. */ public void unproxyRemoteObject (String nodeName, int remoteOid) { Tuple key = Tuple.newTuple(nodeName, remoteOid); Tuple, DObject> bits = _proxies.remove(key); if (bits == null) { log.warning("Requested to clear unknown proxy", "key", key); return; } // clear out the local object manager's proxy mapping _omgr.clearProxyObject(remoteOid, bits.right); final Client peer = getPeerClient(nodeName); if (peer == null) { log.warning("Unable to unsubscribe from proxy, missing peer", "key", key); return; } // restore the object's omgr reference to our ClientDObjectMgr and its oid back to the // remote oid so that it can properly finish the unsubscription process bits.right.setOid(remoteOid); bits.right.setManager(peer.getDObjectManager()); // finally unsubscribe from the object on our peer peer.getDObjectManager().unsubscribeFromObject(remoteOid, bits.left); } /** * Returns the client object representing the connection to the named peer, or * null if we are not currently connected to it. */ public Client getPeerClient (String nodeName) { PeerNode peer = _peers.get(nodeName); return (peer == null) ? null : peer.getClient(); } /** * Returns the public hostname to use when connecting to the specified peer or null if the peer * is not currently connected to this server. */ public String getPeerPublicHostName (String nodeName) { if (_nodeName.equals(nodeName)) { return _self.publicHostName; } PeerNode peer = _peers.get(nodeName); return (peer == null) ? null : peer.getPublicHostName(); } /** * Returns the internal hostname to use when connecting to the specified peer or null if the * peer is not currently connected to this server. Peers connect to one another via their * internal hostname. Do not publish this data to clients out on the Internets. */ public String getPeerInternalHostName (String nodeName) { if (_nodeName.equals(nodeName)) { return _self.hostName; } PeerNode peer = _peers.get(nodeName); return (peer == null) ? null : peer.getInternalHostName(); } /** * Returns the port on which to connect to the specified peer or -1 if the peer is not * currently connected to this server. */ public int getPeerPort (String nodeName) { if (_nodeName.equals(nodeName)) { return _self.port; } PeerNode peer = _peers.get(nodeName); return (peer == null) ? -1 : peer.getPort(); } /** * Acquires a lock on a resource shared amongst this node's peers. If the lock is successfully * acquired, the supplied listener will receive this node's name. If another node acquires the * lock first, then the listener will receive the name of that node. */ public void acquireLock (final NodeObject.Lock lock, final ResultListener listener) { // wait until any pending resolution is complete queryLock(lock, new ChainedResultListener(listener) { public void requestCompleted (String result) { if (result == null) { if (_suboids.isEmpty()) { lockAcquired(lock, 0L, listener); } else { _locks.put(lock, new LockHandler(lock, true, listener)); } } else { listener.requestCompleted(result); } } }); } /** * Releases a lock. This can be cancelled using {@link #reacquireLock}, in which case the * passed listener will receive this node's name as opposed to null, which * signifies that the lock has been successfully released. */ public void releaseLock (final NodeObject.Lock lock, final ResultListener listener) { // wait until any pending resolution is complete queryLock(lock, new ChainedResultListener(listener) { public void requestCompleted (String result) { if (_nodeName.equals(result)) { if (_suboids.isEmpty()) { lockReleased(lock, listener); } else { _locks.put(lock, new LockHandler(lock, false, listener)); } } else { if (result != null) { log.warning("Tried to release lock held by another peer", "lock", lock, "owner", result); } listener.requestCompleted(result); } } }); } /** * Reacquires a lock after a call to {@link #releaseLock} but before the result listener * supplied to that method has been notified with the result of the action. The result * listener will receive the name of this node to indicate that the lock is still held. If a * node requests to release a lock, then receives a lock-related request from another peer, it * can use this method to cancel the release reliably, since the lock-related request will have * been sent before the peer's ratification of the release. */ public void reacquireLock (NodeObject.Lock lock) { // make sure we're releasing it LockHandler handler = _locks.get(lock); if (handler == null || !handler.getNodeName().equals(_nodeName) || handler.isAcquiring()) { log.warning("Tried to reacquire lock not being released", "lock", lock, "handler", handler); return; } // perform an update to let other nodes know that we're reacquiring _nodeobj.updateLocks(lock); // cancel the handler and report to any listeners _locks.remove(lock); handler.cancel(); handler.listeners.requestCompleted(_nodeName); } /** * Determines the owner of the specified lock, waiting for any resolution to complete before * notifying the supplied listener. */ public void queryLock (NodeObject.Lock lock, ResultListener listener) { // if it's being resolved, add the listener to the list LockHandler handler = _locks.get(lock); if (handler != null) { handler.listeners.add(listener); return; } // otherwise, return its present value listener.requestCompleted(queryLock(lock)); } /** * Finds the owner of the specified lock (if any) among this node and its peers. This answer * is not definitive, as the lock may be in the process of resolving. */ public String queryLock (NodeObject.Lock lock) { for (NodeObject nodeobj : getNodeObjects()) { if (nodeobj.locks.contains(lock)) { return nodeobj.nodeName; } } return null; } /** * Tries to acquire the resource lock and, if successful, performs the operation and releases * the lock; if unsuccessful, calls the operation's failure handler. Please note: the lock will * be released immediately after the operation. */ public void performWithLock (final NodeObject.Lock lock, final LockedOperation operation) { acquireLock(lock, new ResultListener() { public void requestCompleted (String nodeName) { if (getNodeObject().nodeName.equals(nodeName)) { // lock acquired successfully - perform the operation, and release the lock. try { operation.run(); } finally { releaseLock(lock, new ResultListener.NOOP()); } } else { // some other peer beat us to it operation.fail(nodeName); if (nodeName == null) { log.warning("Lock acquired by null?", "lock", lock); } } } public void requestFailed (Exception cause) { log.warning("Lock acquisition failed", "lock", lock, cause); operation.fail(null); } }); } /** * Adds an observer to notify when this peer has been forced to drop a lock immediately. */ public void addDroppedLockObserver (DroppedLockObserver observer) { _dropobs.add(observer); } /** * Removes a dropped lock observer from the list. */ public void removeDroppedLockObserver (DroppedLockObserver observer) { _dropobs.remove(observer); } /** * Called by {@link PeerSession}s when clients subscribe to the {@link NodeObject}. */ public void clientSubscribedToNode (int cloid) { _suboids.add(cloid); } /** * Called by {@link PeerSession}s when clients unsubscribe from the {@link NodeObject}. */ public void clientUnsubscribedFromNode (int cloid) { _suboids.remove(cloid); for (LockHandler handler : _locks.values().toArray(new LockHandler[_locks.size()])) { if (handler.getNodeName().equals(_nodeName)) { handler.clientUnsubscribed(cloid); } } } /** * Registers a stale cache observer. */ public void addStaleCacheObserver (String cache, StaleCacheObserver observer) { ObserverList list = _cacheobs.get(cache); if (list == null) { list = new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY); _cacheobs.put(cache, list); } list.add(observer); } /** * Removes a stale cache observer registration. */ public void removeStaleCacheObserver (String cache, StaleCacheObserver observer) { ObserverList list = _cacheobs.get(cache); if (list == null) { return; } list.remove(observer); if (list.isEmpty()) { _cacheobs.remove(cache); } } /** * Called when cached data has changed on the local server and needs to inform our peers. */ public void broadcastStaleCacheData (String cache, Streamable data) { _nodeobj.setCacheData(new NodeObject.CacheData(cache, data)); } /** * Returns a snapshot of runtime statistics tracked by the peer manager. */ public Stats getStats () { return _stats.clone(); } // from interface Lifecycle.ShutdownComponent public void shutdown () { if (_nodeName == null) { // never initialized return; } // clear out our invocation service if (_nodeobj != null) { _invmgr.clearDispatcher(_nodeobj.peerService); } // clear out our client observer registration _clmgr.removeClientObserver(this); // clear our record from the node table _invoker.postUnit(new WriteOnlyUnit("deleteNode(" + _nodeName + ")") { @Override public void invokePersist () throws Exception { _noderepo.deleteNode(_nodeName); } }); // shut down the peers for (PeerNode peer : _peers.values()) { peer.shutdown(); } } // from interface PeerProvider public void ratifyLockAction (ClientObject caller, NodeObject.Lock lock, boolean acquire) { LockHandler handler = _locks.get(lock); if (handler != null && handler.getNodeName().equals(_nodeName)) { handler.ratify(caller, acquire); } else { // this is not an error condition, as we may have cancelled the handler or // allowed another to take priority } } // from interface PeerProvider public void invokeAction (ClientObject caller, byte[] serializedAction) { NodeAction action = null; try { ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(serializedAction)); action = (NodeAction)oin.readObject(); _injector.injectMembers(action); action.invoke(); } catch (Exception e) { log.warning("Failed to execute node action", "from", (caller == null) ? "self" : caller.who(), "action", action, "serializedSize", serializedAction.length, e); } } // from interface PeerProvider public void invokeRequest (ClientObject caller, byte[] serializedAction, InvocationService.ResultListener listener) { NodeRequest request = null; try { ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(serializedAction)); request = (NodeRequest)oin.readObject(); _injector.injectMembers(request); request.invoke(listener); } catch (Exception e) { log.warning("Failed to execute node request", "from", (caller == null) ? "self" : caller.who(), "request", request, "serializedSize", serializedAction.length, e); listener.requestFailed("Failed to execute node request"); } } // from interface PeerProvider public void generateReport (ClientObject caller, String type, PeerService.ResultListener listener) throws InvocationException { listener.requestProcessed(_repmgr.generateReport(type)); } // from interface ClientManager.ClientObserver public void clientSessionDidStart (PresentsSession client) { if (ignoreClient(client)) { return; } // create and publish a ClientInfo record for this client ClientInfo clinfo = createClientInfo(); initClientInfo(client, clinfo); // sanity check if (_nodeobj.clients.contains(clinfo)) { log.warning("Received clientSessionDidStart() for already registered client!?", "old", _nodeobj.clients.get(clinfo.getKey()), "new", clinfo); // go ahead and update the record _nodeobj.updateClients(clinfo); } else { _nodeobj.addToClients(clinfo); } } // from interface ClientManager.ClientObserver public void clientSessionDidEnd (PresentsSession client) { if (ignoreClient(client)) { return; } // we scan through the list instead of relying on ClientInfo.getKey() because we want // derived classes to be able to override that for lookups that happen way more frequently // than logging off Name username = client.getAuthName(); for (ClientInfo clinfo : _nodeobj.clients) { if (clinfo.username.equals(username)) { _nodeobj.startTransaction(); try { // we clear our client info in a transaction so that derived classes can remove // other things from the NodeObject and we'll send that out to all of our peers // in a single compound event clearClientInfo(client, clinfo); } finally { _nodeobj.commitTransaction(); } return; } } log.warning("Session ended for unregistered client", "who", username); } /** * Called after we have finished our initialization. */ protected void didInit () { } /** * Reloads the list of peer nodes from our table and refreshes each with a call to {@link * #refreshPeer}. */ protected void refreshPeers () { // load up information on our nodes _invoker.postUnit(new RepositoryUnit("refreshPeers") { @Override public void invokePersist () throws Exception { // let the world know that we're alive _noderepo.heartbeatNode(_nodeName); // then load up all the peer records _nodes = _noderepo.loadNodes(_nodeNamespace); } @Override public void handleSuccess () { for (NodeRecord record : _nodes) { if (record.nodeName.equals(_nodeName)) { continue; } try { refreshPeer(record); } catch (Exception e) { log.warning("Failure refreshing peer " + record + ".", e); } } } @Override public long getLongThreshold () { return 700L; } protected List _nodes; }); } /** * Ensures that we have a connection to the specified node if it has checked in since we last * failed to connect. */ protected void refreshPeer (NodeRecord record) { PeerNode peer = _peers.get(record.nodeName); if (peer == null) { peer = _injector.getInstance(getPeerNodeClass()); _peers.put(record.nodeName, peer); peer.init(record); } peer.refresh(record); } /** * Returns true if we should ignore the supplied client, false if we should let our other peers * know that this client is authenticated with this server. Note: this is called at * the beginning and end of the client session, so this method should return the same value * both times. */ protected boolean ignoreClient (PresentsSession client) { // if this is another peer, don't publish their info return (client instanceof PeerSession); } /** * Creates the appropriate derived class of {@link NodeObject} which will be registered with * the distributed object system. */ protected NodeObject createNodeObject () { return new NodeObject(); } /** * Creates a {@link ClientInfo} record which will subsequently be initialized by a call to * {@link #initClientInfo}. */ protected ClientInfo createClientInfo () { return new ClientInfo(); } /** * Returns the lock handler for the specified lock. */ protected LockHandler getLockHandler (NodeObject.Lock lock) { return _locks.get(lock); } protected LockHandler createLockHandler (PeerNode peer, NodeObject.Lock lock, boolean acquire) { LockHandler handler = new LockHandler(peer, lock, acquire); _locks.put(lock, handler); return handler; } /** * Called when possibly cached data has changed on one of our peer servers. */ protected void changedCacheData (String cache, final Streamable data) { // see if we have any observers ObserverList list = _cacheobs.get(cache); if (list == null) { return; } // if so, notify them list.apply(new ObserverList.ObserverOp() { public boolean apply (StaleCacheObserver observer) { observer.changedCacheData(data); return true; } }); } /** * Called when we have been forced to drop a lock. */ protected void droppedLock (final NodeObject.Lock lock) { _nodeobj.removeFromLocks(lock); _stats.locksHijacked++; _dropobs.apply(new ObserverList.ObserverOp() { public boolean apply (DroppedLockObserver observer) { observer.droppedLock(lock); return true; } }); } /** * Initializes the supplied client info for the supplied client. */ protected void initClientInfo (PresentsSession client, ClientInfo info) { info.username = client.getAuthName(); } /** * Called when a client ends their session to clear their information from our node object. */ protected void clearClientInfo (PresentsSession client, ClientInfo info) { _nodeobj.removeFromClients(info.getKey()); } /** * Creates a {@link PeerNode} to manage our connection to the specified peer. */ protected Class getPeerNodeClass () { return PeerNode.class; } /** * Creates credentials that a {@link PeerNode} can use to authenticate with another node. */ protected PeerCreds createCreds () { return new PeerCreds(_nodeName, _sharedSecret); } /** * Called when we hear about a client logging on to another node. */ protected void clientLoggedOn (String nodeName, ClientInfo clinfo) { PresentsSession session = _clmgr.getClient(clinfo.username); if (session != null) { log.info("Booting user that has connected on another node", "username", clinfo.username, "otherNode", nodeName); session.endSession(); } } /** * Called when we hear about a client logging off of another node. */ protected void clientLoggedOff (String nodeName, ClientInfo clinfo) { // nothing to do by default } /** * Called when a peer connects to this server. */ protected void peerStartedSession (PeerSession session) { // this may be the first we've heard of this guy, so let's refresh our peers and // potentially connect right back to him refreshPeers(); // pass our stats record in so that it can count up messages in/out session.setStats(_stats); } /** * Called when a peer's session with this server ends. */ protected void peerEndedSession (PeerSession session) { // nada } /** * Called when we have established a connection to the supplied peer. */ protected void connectedToPeer (PeerNode peer) { // check for lock conflicts String nodeName = peer.getNodeName(); for (NodeObject.Lock lock : peer.nodeobj.locks) { PeerManager.LockHandler handler = _locks.get(lock); if (handler != null) { log.warning("Client hijacked lock in process of resolution", "handler", handler, "node", nodeName); handler.clientHijackedLock(nodeName); } else if (_nodeobj.locks.contains(lock)) { log.warning("Client hijacked lock owned by this node", "lock", lock, "node", nodeName); droppedLock(lock); } } } /** * Called when a ended our session with the supplied peer. */ protected void disconnectedFromPeer (PeerNode peer) { // clear any locks held by that peer String nodeName = peer.getNodeName(); for (LockHandler handler : _locks.values().toArray(new LockHandler[_locks.size()])) { if (handler.getNodeName().equals(nodeName)) { handler.clientDidLogoff(); } } } /** * Flattens the supplied node action into bytes. */ protected byte[] flattenAction (NodeAction action) { try { ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream oout = new ObjectOutputStream(bout); oout.writeObject(action); return bout.toByteArray(); } catch (Exception e) { throw new IllegalArgumentException( "Failed to serialize node action [action=" + action + "].", e); } } /** * Flattens the supplied node request into bytes. */ protected byte[] flattenRequest (NodeRequest request) { try { ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream oout = new ObjectOutputStream(bout); oout.writeObject(request); return bout.toByteArray(); } catch (Exception e) { throw new IllegalArgumentException( "Failed to serialize node request [request=" + request + "].", e); } } protected void invokeNodeRequest (String nodeName, byte[] requestBytes, InvocationService.ResultListener listener) { PeerNode peer = _peers.get(nodeName); if (peer != null) { peer.nodeobj.peerService.invokeRequest(peer.getClient(), requestBytes, listener); } else if (nodeName.equals(_nodeName)) { invokeRequest(null, requestBytes, listener); } } protected void lockAcquired (NodeObject.Lock lock, long wait, ResultListener listener) { _nodeobj.addToLocks(lock); _stats.locksAcquired++; _stats.lockAcquireWait += wait; listener.requestCompleted(_nodeName); } protected void lockReleased (NodeObject.Lock lock, ResultListener listener) { _nodeobj.removeFromLocks(lock); _stats.locksReleased++; listener.requestCompleted(null); } /** * Handles a lock in a state of resolution. */ protected class LockHandler implements SetListener { /** Listeners waiting for resolution. */ public ResultListenerList listeners = new ResultListenerList(); /** * Creates a handler to acquire or release a lock for this node. */ public LockHandler (NodeObject.Lock lock, boolean acquire, ResultListener listener) { _lock = lock; _acquire = acquire; listeners.add(listener); // signal our desire to acquire or release the lock if (acquire) { _nodeobj.setAcquiringLock(lock); } else { _nodeobj.setReleasingLock(lock); } // take a snapshot of the set of subscriber client oids; we will act when all of them // ratify _remoids = _suboids.clone(); // schedule a timeout to act if something goes wrong (_timeout = _omgr.newInterval(new Runnable () { public void run () { log.warning("Lock handler timed out, acting anyway", "lock", _lock, "acquire", _acquire); _stats.lockTimeouts++; activate(); } })).schedule(LOCK_TIMEOUT); } /** * Creates a handle that tracks another node's acquisition or release of a lock. */ public LockHandler (PeerNode peer, NodeObject.Lock lock, boolean acquire) { _peer = peer; _lock = lock; _acquire = acquire; // ratify the action peer.nodeobj.peerService.ratifyLockAction(peer.getClient(), lock, acquire); // listen for the act to take place peer.nodeobj.addListener(this); } /** * Returns the name of the node waiting to perform the action. */ public String getNodeName () { return (_peer == null) ? _nodeName : _peer.getNodeName(); } /** * Checks whether we are acquiring as opposed to releasing a lock. */ public boolean isAcquiring () { return _acquire; } /** * Signals that one of the remote nodes has ratified the pending action. */ public void ratify (ClientObject caller, boolean acquire) { if (acquire != _acquire) { return; } if (!_remoids.remove(caller.getOid())) { log.warning("Received unexpected ratification", "handler", this, "who", caller.who()); } maybeActivate(); } /** * Called when a client has unsubscribed from this node (which is waiting for * ratification). */ public void clientUnsubscribed (int cloid) { // unsubscription is implicit ratification if (_remoids.remove(cloid)) { maybeActivate(); } } /** * Called when the connection to the controlling node has been broken. */ public void clientDidLogoff () { _locks.remove(_lock); listeners.requestCompleted(null); } /** * Called when a client hijacks the lock by having it in its node object when it connects. */ public void clientHijackedLock (String nodeName) { cancel(); _locks.remove(_lock); _stats.locksHijacked++; listeners.requestCompleted(nodeName); } /** * Cancels this handler, as another one will be taking its place. */ public void cancel () { if (_peer != null) { _peer.nodeobj.removeListener(this); } else { _timeout.cancel(); } } // documentation inherited from interface SetListener public void entryAdded (EntryAddedEvent event) { if (_acquire && event.getName().equals(NodeObject.LOCKS) && event.getEntry().equals(_lock)) { wasActivated(_peer.getNodeName()); } } // documentation inherited from interface SetListener public void entryRemoved (EntryRemovedEvent event) { if (!_acquire && event.getName().equals(NodeObject.LOCKS) && event.getOldEntry().equals(_lock)) { wasActivated(null); } } // documentation inherited from interface SetListener public void entryUpdated (EntryUpdatedEvent event) { if (!_acquire && event.getName().equals(NodeObject.LOCKS) && event.getEntry().equals(_lock)) { wasActivated(_peer.getNodeName()); } } @Override public String toString () { return "[node=" + getNodeName() + ", lock=" + _lock + ", acquire=" + _acquire + "]"; } /** * Performs the action if all remote nodes have ratified. */ protected void maybeActivate () { if (_remoids.isEmpty()) { _timeout.cancel(); activate(); } } /** * Performs the configured action. */ protected void activate () { _locks.remove(_lock); if (_acquire) { lockAcquired(_lock, System.currentTimeMillis() - _startStamp, listeners); } else { lockReleased(_lock, listeners); } } /** * Called when the remote node has performed its action. */ protected void wasActivated (String owner) { _peer.nodeobj.removeListener(this); _locks.remove(_lock); listeners.requestCompleted(owner); } protected PeerNode _peer; protected NodeObject.Lock _lock; protected boolean _acquire; protected ArrayIntSet _remoids; protected Interval _timeout; protected long _startStamp = System.currentTimeMillis(); } protected static class NodeRequestsResultImpl implements NodeRequestsListener.NodeRequestsResult { public NodeRequestsResultImpl (Map results, Map errors) { _results = Maps.newHashMap(results); _errors = Maps.newHashMap(errors); } public NodeRequestsResultImpl () { _results = Maps.newHashMap(); _errors = Maps.newHashMap(); } public Map getNodeResults () { return _results; } public Map getNodeErrors () { return _errors; } public boolean wasDropped () { return _results.isEmpty() && _errors.isEmpty(); } protected Map _results; protected Map _errors; } /** Extracts the node object from the supplied peer. */ protected static final Function GET_NODE_OBJECT = new Function() { public NodeObject apply (PeerNode peer) { return peer.nodeobj; } }; protected String _nodeName, _sharedSecret; protected NodeRecord _self; protected NodeObject _nodeobj; protected String _nodeNamespace; protected Map _peers = Maps.newHashMap(); /** Used to resolve dependencies in unserialized {@link NodeAction} instances. */ /** The client oids of all peers subscribed to the node object. */ protected ArrayIntSet _suboids = new ArrayIntSet(); /** Contains a mapping of proxied objects to subscriber instances. */ protected Map,Tuple,DObject>> _proxies = Maps.newHashMap(); /** Our stale cache observers. */ protected Map> _cacheobs = Maps.newHashMap(); /** Listeners for dropped locks. */ protected ObserverList _dropobs = ObserverList.newFastUnsafe(); /** Locks in the process of resolution. */ protected Map _locks = Maps.newHashMap(); /** Used to track runtime statistics. */ protected Stats _stats = new Stats(); // our service dependencies @Inject protected @MainInvoker Invoker _invoker; @Inject protected ClientManager _clmgr; @Inject protected ConnectionManager _conmgr; @Inject protected Injector _injector; @Inject protected InvocationManager _invmgr; @Inject protected NodeRepository _noderepo; @Inject protected PresentsDObjectMgr _omgr; @Inject protected ReportManager _repmgr; /** We wait this long for peer ratification to complete before acquiring/releasing the lock. */ protected static final long LOCK_TIMEOUT = 5000L; }