All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.threerings.presents.server.ClientManager Maven / Gradle / Ivy

//
// $Id: ClientManager.java 6604 2011-04-05 21:01:02Z charlie $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2011 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.server;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import java.io.IOException;
import java.net.InetAddress;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;

import com.samskivert.util.Lifecycle;
import com.samskivert.util.ObserverList;
import com.samskivert.util.StringUtil;

import com.threerings.util.Name;

import com.threerings.presents.annotation.EventThread;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.net.AuthRequest;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.server.net.AuthingConnection;
import com.threerings.presents.server.net.PresentsConnection;

import com.threerings.nio.conman.Connection;

import static com.threerings.presents.Log.log;

/**
 * The client manager is responsible for managing client sessions which are slightly more than just
 * connections. Clients persist in the absence of connections in case a user goes bye bye
 * unintentionally and wants to reconnect and continue their session.
 *
 * 

The client manager operates with thread safety because it is called both from the conmgr * thread (to notify of connections showing up or going away) and from the dobjmgr thread (when * clients are given the boot for application-defined reasons). */ @Singleton public class ClientManager implements ClientResolutionListener, ReportManager.Reporter, Lifecycle.Component { /** * Used by {@link ClientManager#applyToClient}. */ public static interface ClientOp { /** * Called with the resolved client object. */ void apply (ClientObject clobj); /** * Called if the client resolution fails. */ void resolutionFailed (Exception e); } /** * Used by entities that wish to track when clients initiate and end sessions on this server. */ public static interface ClientObserver { /** * Called when a client has authenticated and been resolved and has started their session. */ void clientSessionDidStart (PresentsSession session); /** * Called when a client has logged off or been forcibly logged off due to inactivity and * has thus ended their session. */ void clientSessionDidEnd (PresentsSession session); } /** * Methods for observing additional events in the session lifecycle. */ public static interface DetailedClientObserver extends ClientObserver { /** * Called prior to the sessions ending. Subclasses and this class tend to nuke a lot of * information in the process of ending. This will allow callers to act on session events * without creating a subclass of a session. For example lightweight management of dobj * fields such as flushing stats or notifying party members of a logout. */ void clientSessionWillEnd (PresentsSession session); } /** * Constructs a client manager that will interact with the supplied connection manager. */ @Inject public ClientManager (ReportManager repmgr, Lifecycle cycle) { repmgr.registerReporter(this); cycle.addComponent(this); } /** * Configures the injector we'll use to resolve dependencies for {@link PresentsSession} * instances. */ public void setInjector (Injector injector) { _injector = injector; } /** * Configures the default factory for creating {@link PresentsSession} and {@link * ClientResolver} classes for authenticated client connections. All factories added via {@link * #addSessionFactory} will be offered a chance to handle sessions before this factory of last * resort. */ public void setDefaultSessionFactory (SessionFactory factory) { _factories.set(_factories.size()-1, factory); } /** * Adds a session factory to the chain. This factory will be offered a chance to resolve * sessions before passing the buck to the next factory in the chain. */ public void addSessionFactory (SessionFactory factory) { _factories.add(0, factory); } /** * Return the number of client resolutions are currently happening. */ public int getOutstandingResolutionCount () { return _penders.size(); } /** * Returns the number of client sessions (some may be disconnected). */ public int getClientCount () { synchronized (_usermap) { return _usermap.size(); } } /** * Returns all sessions logged in from the given IP in the form returned from * {@link InetAddress#getAddress()}. */ public List getSessionsForAddress (byte[] addr) { List sessions = Lists.newArrayListWithExpectedSize(1); synchronized (_usermap) { for (PresentsSession session : _usermap.values()) { InetAddress sessionAddr = session.getInetAddress(); if (sessionAddr == null) { continue; } if (Arrays.equals(addr, sessionAddr.getAddress())) { sessions.add(session); } } } return sessions; } /** * Returns the number of connected clients. */ public int getConnectionCount () { return _conmap.size(); } /** * Returns an iterable over all active client objects. */ public Iterable clientObjects () { return _objmap.values(); } /** * Enumerates all active client objects. */ public Iterator enumerateClientObjects () { return _objmap.values().iterator(); } /** * Registers an observer that will be notified when clients start and end their sessions. */ public void addClientObserver (ClientObserver observer) { _clobservers.add(observer); if (observer instanceof DetailedClientObserver) { _dclobservers.add((DetailedClientObserver)observer); } } /** * Removes an observer previously registered with {@link #addClientObserver}. */ public void removeClientObserver (ClientObserver observer) { _clobservers.remove(observer); if (observer instanceof DetailedClientObserver) { _dclobservers.remove((DetailedClientObserver)observer); } } /** * Returns the client instance that manages the client session for the specified authentication * username or null if that client is not currently connected to the server. */ public PresentsSession getClient (Name authUsername) { synchronized (_usermap) { return _usermap.get(authUsername); } } /** * Returns the client object associated with the specified username. This will return null * unless the client object is resolved for some reason (like they are logged on). */ public ClientObject getClientObject (Name username) { return _objmap.get(username); } /** * Resolves the specified client, applies the supplied client operation to them and releases * the client. */ public void applyToClient (Name username, final ClientOp clop) { resolveClientObject(username, new ClientResolutionListener() { public void clientResolved (Name username, ClientObject clobj) { try { clop.apply(clobj); } catch (Exception e) { log.warning("Client op failed", "username", username, "clop", clop, e); } finally { releaseClientObject(username); } } public void resolutionFailed (Name username, Exception reason) { clop.resolutionFailed(reason); } }); } /** * Requests that the client object for the specified user be resolved. Note: this * must be paired with a call to {@link #releaseClientObject} when the caller is * finished with the client object. */ public synchronized void resolveClientObject ( final Name username, final ClientResolutionListener listener) { // look to see if the client object is already resolved final ClientObject clobj = _objmap.get(username); if (clobj != null) { // report that the client is resolved on the dobjmgr thread to provide equivalent // behavior to the case where we actually have to do the resolution clobj.reference(); _omgr.postRunnable(new Runnable() { public void run () { listener.clientResolved(username, clobj); } }); return; } // look to see if it's currently being resolved ClientResolver clr = _penders.get(username); if (clr != null) { // throw this guy onto the bandwagon clr.addResolutionListener(listener); return; } // figure out our client resolver class Class resolverClass = null; for (SessionFactory factory : _factories) { if ((resolverClass = factory.getClientResolverClass(username)) != null) { break; } } try { // create a client resolver instance which will create our client object, populate it // and notify the listeners clr = _injector.getInstance(resolverClass); clr.init(username); clr.addResolutionListener(this); clr.addResolutionListener(listener); _penders.put(username, clr); // create and register our client object and give it back to the client resolver; we // need to do this on the dobjmgr thread since we're registering an object final ClientResolver fclr = clr; _omgr.postRunnable(new Runnable() { public void run () { ClientObject clobj = fclr.createClientObject(); clobj.setLocal(ClientLocal.class, fclr.createLocalAttribute()); fclr.objectAvailable(_omgr.registerObject(clobj)); } }); } catch (Exception e) { // let the listener know that we're hosed listener.resolutionFailed(username, e); } } /** * Releases a client object that was obtained via a call to {@link #resolveClientObject}. If * this caller is the last reference, the object will be flushed and destroyed. */ public void releaseClientObject (Name username) { ClientObject clobj = _objmap.get(username); if (clobj == null) { log.info("Requested to release unmapped client object", "username", username); return; } // decrement the reference count and stop here if there are remaining references if (clobj.release()) { return; } log.debug("Destroying client " + clobj.who() + "."); // we're all clear to go; remove the mapping _objmap.remove(username); // and destroy the object itself _omgr.destroyObject(clobj.getOid()); } // from interface Lifecycle.Component public void init () { // start up an interval that will check for and flush expired sessions (this will be // canceled when the omgr shuts down) _omgr.newInterval(new Runnable() { public void run () { flushSessions(); } }).schedule(SESSION_FLUSH_INTERVAL, true); } // from interface Lifecycle.Component public void shutdown () { log.info("Client manager shutting down", "ccount", _usermap.size()); // inform all of our clients that they are being shut down synchronized (_usermap) { for (PresentsSession pc : _usermap.values()) { try { pc.shutdown(); } catch (Exception e) { log.warning("Client choked in shutdown()", "client", StringUtil.safeToString(pc), e); } } } } /** * Renames a currently connected client from oldname to newname. * * @return true if the client was found and renamed. */ protected boolean renameClientObject (Name oldname, Name newname) { ClientObject clobj = _objmap.remove(oldname); if (clobj == null) { log.warning("Requested to rename unmapped client object", "username", oldname, new Exception()); return false; } _objmap.put(newname, clobj); return true; } // documentation inherited from interface ClientResolutionListener public synchronized void clientResolved (Name username, ClientObject clobj) { // because we added ourselves as a client resolution listener, the client object reference // count was increased, but we're not a real resolver (who would have to call // releaseClientObject() to release their reference), so we release our reference // immediately clobj.release(); // stuff the object into the mapping table _objmap.put(username, clobj); // and remove the resolution listener _penders.remove(username); } // documentation inherited from interface ClientResolutionListener public synchronized void resolutionFailed (Name username, Exception reason) { // clear out their pending record _penders.remove(username); } /** * Called by the connection manager to let us know when a new connection has been established. */ public synchronized void connectionEstablished ( PresentsConnection conn, Name authname, AuthRequest req, AuthResponse rsp) { String type = authname.getClass().getSimpleName(); // see if a session is already registered with this name PresentsSession session = getClient(authname); if (session != null) { log.info("Resuming session", "type", type, "who", authname, "conn", conn); session.resumeSession(req, conn); } else { log.info("Session initiated", "type", type, "who", authname, "conn", conn); // figure out our session class Class sessionClass = null; for (SessionFactory factory : _factories) { if ((sessionClass = factory.getSessionClass(req)) != null) { break; } } // create a new session and stick'em in the table session = _injector.getInstance(sessionClass); session.startSession(authname, req, conn, rsp.authdata); // map their session instance synchronized (_usermap) { // we refetch the authname from the session for use in the map in case it decides // to do something crazy like rewrite it in startSession() _usermap.put(session.getAuthName(), session); } } // map this connection to this session _conmap.put(conn, session); } /** * Called by the connection manager to let us know when a connection has failed. */ public synchronized void connectionFailed (Connection conn, IOException fault) { // remove the session from the connection map PresentsSession session = _conmap.remove(conn); if (session != null) { log.info("Unmapped failed session", "session", session, "conn", conn, "fault", fault); // let the session know the connection went away session.wasUnmapped(); // and let the session know things went haywire session.connectionFailed(fault); } else if (!(conn instanceof AuthingConnection)) { log.info("Unmapped connection failed?", "conn", conn, "fault", fault, new Exception()); } } /** * Called by the connection manager to let us know when a connection has been closed. */ public synchronized void connectionClosed (Connection conn) { // remove the session from the connection map PresentsSession session = _conmap.remove(conn); if (session != null) { log.debug("Unmapped session", "session", session, "conn", conn); // let the session know the connection went away session.wasUnmapped(); } else { log.info("Closed unmapped connection '" + conn + "'. " + "Session probably not yet authenticated."); } } // documentation inherited from interface ReportManager.Reporter public void appendReport (StringBuilder report, long now, long sinceLast, boolean reset) { report.append("* presents.ClientManager:\n"); report.append("- Sessions: "); synchronized (_usermap) { report.append(_usermap.size()).append(" total, "); } report.append(_conmap.size()).append(" connected, "); report.append(_penders.size()).append(" pending\n"); report.append("- Mapped users: ").append(_objmap.size()).append("\n"); } /** * Called by PresentsSession when it has started its session. */ @EventThread protected void clientSessionDidStart (final PresentsSession session) { // let the observers know _clobservers.apply(new ObserverList.ObserverOp() { public boolean apply (ClientObserver observer) { observer.clientSessionDidStart(session); return true; } }); } /** * Called by PresentsSession when it is about to end its session. */ @EventThread protected void clientSessionWillEnd (final PresentsSession session) { // notify the observers that the session is ended _dclobservers.apply(new ObserverList.ObserverOp() { public boolean apply (DetailedClientObserver observer) { observer.clientSessionWillEnd(session); return true; } }); } /** * Called by PresentsSession when it has ended its session. */ @EventThread protected void clientSessionDidEnd (final PresentsSession session) { // notify the observers that the session is ended _clobservers.apply(new ObserverList.ObserverOp() { public boolean apply (ClientObserver observer) { observer.clientSessionDidEnd(session); return true; } }); } /** * Called by PresentsSession to let us know that we can clear it entirely out of the system. */ protected void clearSession (PresentsSession session) { // remove the session from the username map PresentsSession rc; synchronized (_usermap) { rc = _usermap.remove(session.getAuthName()); } // sanity check just because we can if (rc == null) { log.info("Cleared session: unregistered!", "session", session); } else if (rc != session) { log.info("Cleared session: multiple!", "s1", rc, "s2", session); } else { log.info("Cleared session", "session", session); } } /** * Called once per minute to check for sessions that have been disconnected too long and * forcibly end their sessions. */ protected void flushSessions () { List victims = Lists.newArrayList(); long now = System.currentTimeMillis(); // first build a list of our victims synchronized (_usermap) { for (PresentsSession session : _usermap.values()) { if (session.checkExpired(now)) { victims.add(session); } } } // now end their sessions for (PresentsSession session : victims) { try { log.info("Session expired, ending session", "session", session, "dtime", (now-session.getNetworkStamp()) + "ms]."); session.endSession(); } catch (Exception e) { log.warning("Choke while flushing session", "victim", session, e); } } } /** Used to resolve dependencies in {@link PresentsSession} instances that we create. */ protected Injector _injector; /** A mapping from auth username to session instances. */ protected Map _usermap = Maps.newHashMap(); /** A mapping from connections to session instances. */ protected Map _conmap = Maps.newHashMap(); /** A mapping from usernames to client object instances. */ protected Map _objmap = Maps.newHashMap(); /** A mapping of pending client resolvers. */ protected Map _penders = Maps.newHashMap(); /** Lets us know what sort of session classes to use. */ protected List _factories = Lists.newArrayList(SessionFactory.DEFAULT); /** Tracks registered {@link ClientObserver}s. */ protected ObserverList _clobservers = ObserverList.newSafeInOrder(); /** Tracks registered {@link DetailedClientObserver}s. */ protected ObserverList _dclobservers = ObserverList.newSafeInOrder(); // our injected dependencies @Inject protected PresentsDObjectMgr _omgr; /** The frequency with which we check for expired sessions. */ protected static final long SESSION_FLUSH_INTERVAL = 60 * 1000L; }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy