Files
narya/src/java/com/threerings/presents/server/ClientManager.java
T
Michael Bayne fa206b76ed Metrics! Added support for a "state of the server" report, in which
managers can participate, reporting lies, damned lies and useful
statistics. Initial participants include the client manager, the
connection manager and the distributed object manager.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@1894 542714f4-19e9-0310-aa3c-eee0fc999fb1
2002-11-05 02:17:56 +00:00

432 lines
15 KiB
Java

//
// $Id: ClientManager.java,v 1.26 2002/11/05 02:17:56 mdb Exp $
package com.threerings.presents.server;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import com.samskivert.util.IntervalManager;
import com.threerings.presents.Log;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.net.AuthRequest;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.net.Credentials;
import com.threerings.presents.server.net.*;
import com.threerings.presents.server.util.SafeInterval;
/**
* The client manager is responsible for managing the clients (surprise,
* surprise) 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.
*
* <p> 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).
*/
public class ClientManager
implements ConnectionObserver, PresentsServer.Reporter
{
/**
* Constructs a client manager that will interact with the supplied
* connection manager.
*/
public ClientManager (ConnectionManager conmgr)
{
// register ourselves as a connection observer
conmgr.addConnectionObserver(this);
// start up an interval that will check for expired clients and
// flush them from the bowels of the server
IntervalManager.register(new SafeInterval(PresentsServer.omgr) {
public void run () {
flushClients();
}
}, CLIENT_FLUSH_INTERVAL, null, true);
// register as a "state of server" reporter
PresentsServer.registerReporter(this);
}
/**
* Instructs the client manager to construct instances of this derived
* class of {@link PresentsClient} to managed newly accepted client
* connections.
*/
public void setClientClass (Class clientClass)
{
// sanity check
if (!PresentsClient.class.isAssignableFrom(clientClass)) {
Log.warning("Requested to use client class that does not " +
"derive from PresentsClient " +
"[class=" + clientClass.getName() + "].");
return;
}
// make a note of it
_clientClass = clientClass;
}
/**
* Instructs the client to use instances of this {@link
* ClientResolver} derived class when resolving clients in preparation
* for starting a client session.
*/
public void setClientResolverClass (Class clrClass)
{
// sanity check
if (!ClientResolver.class.isAssignableFrom(clrClass)) {
Log.warning("Requested to use client resolver class that does " +
"not derive from ClientResolver " +
"[class=" + clrClass.getName() + "].");
} else {
// make a note of it
_clrClass = clrClass;
}
}
/**
* Enumerates all active client objects.
*/
public Iterator enumerateClientObjects ()
{
return _objmap.values().iterator();
}
/**
* Returns the client instance that manages the client session for the
* specified credentials or null if that client is not currently
* connected to the server.
*/
public PresentsClient getClient (Credentials creds)
{
return (PresentsClient)_usermap.get(creds);
}
/**
* 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 (String username)
{
return (ClientObject)_objmap.get(toKey(username));
}
/**
* We convert usernames to lower case in the username to client object
* mapping so that we can pass arbitrarily cased usernames (like those
* that might be typed in by a "user") straight on through.
*/
protected final String toKey (String username)
{
return username.toLowerCase();
}
/**
* Requests that the client object for the specified user be resolved.
* If the client object is already resolved, the request will be
* processed immediately, otherwise the appropriate client object will
* be instantiated and populated by the registered client resolver
* (which may involve talking to databases).
*/
public synchronized void resolveClientObject (
String username, ClientResolutionListener listener)
{
// look to see if the client object is already resolved
String key = toKey(username);
ClientObject clobj = (ClientObject)_objmap.get(key);
if (clobj != null) {
listener.clientResolved(username, clobj);
return;
}
// look to see if it's currently being resolved
ClientResolver clr = (ClientResolver)_penders.get(key);
if (clr != null) {
// throw this guy onto the bandwagon
clr.addResolutionListener(listener);
return;
}
try {
// create a client resolver instance which will create our
// client object, populate it and notify the listeners
clr = (ClientResolver)_clrClass.newInstance();
clr.init(username);
clr.addResolutionListener(listener);
// request that the appropriate client object be created by
// the dobject manager which starts the whole business off
PresentsServer.omgr.createObject(clr.getClientObjectClass(), clr);
} catch (Exception e) {
// let the listener know that we're hosed
listener.resolutionFailed(username, e);
}
}
/**
* Called by the {@link ClientResolver} once a client object has been
* resolved.
*/
protected synchronized void mapClientObject (
String username, ClientObject clobj)
{
// stuff the object into the mapping table
String key = toKey(username);
_objmap.put(key, clobj);
// and remove the resolution listener
_penders.remove(key);
}
/**
* If an entity resolves a client object outside the scope of a normal
* client session, it should call this to unmap the client object when
* it's finished. This won't actually unmap the client if someone came
* along and started a session for that client in the meanwhile, but
* if it does unmap the client it will also destroy the client object
* (finishing the job, as it were).
*/
public synchronized void unmapClientObject (String username)
{
// we only remove the mapping if there's not a session in progress
// (which is indicated by an entry in the locks table)
String key = toKey(username);
if (_locks.contains(key)) {
return;
}
ClientObject clobj = (ClientObject)_objmap.remove(key);
if (clobj != null) {
PresentsServer.omgr.destroyObject(clobj.getOid());
} else {
Log.warning("Requested to unmap non-existent client object " +
"[username=" + username + "].");
}
}
/**
* When a client object becomes part of an active session, this method
* should be called to ensure that it is not unloaded by any entities
* that temporarily resolve and release the object. This is called
* automatically when a real user starts a session by establishing a
* network connection with the server. If a client session is managed
* via some other mechanism (bots managed by the server, for example),
* this method and its corresponding {@link #releaseClientObject}
* should be called at the beginning and end of the faked client
* session respectively.
*/
public synchronized void lockClientObject (String username)
{
String key = toKey(username);
if (_locks.contains(key)) {
Log.warning("Requested to lock already locked user " +
"[username=" + username + "].");
Thread.dumpStack();
} else {
_locks.add(key);
}
}
/**
* Releases a client object when their session has ended.
*
* @see #lockClientObject
*/
public synchronized void releaseClientObject (String username)
{
if (!_locks.remove(toKey(username))) {
Log.warning("Requested to unlock a user that was not locked " +
"[username=" + username + "].");
Thread.dumpStack();
}
}
// documentation inherited
public synchronized void connectionEstablished (
Connection conn, AuthRequest req, AuthResponse rsp)
{
Credentials creds = req.getCredentials();
String username = creds.getUsername();
// see if a client is already registered with these credentials
PresentsClient client = (PresentsClient)_usermap.get(creds);
if (client != null) {
Log.info("Resuming session [username=" + username +
", conn=" + conn + "].");
client.resumeSession(conn);
} else {
Log.info("Session initiated [username=" + username +
", conn=" + conn + "].");
// create a new client and stick'em in the table
try {
// create a client and start up its session
client = (PresentsClient)_clientClass.newInstance();
client.startSession(this, creds, conn);
// map their client instance
_usermap.put(creds, client);
} catch (Exception e) {
Log.warning("Failed to instantiate client instance to " +
"manage new client connection '" + conn + "'.");
Log.logStackTrace(e);
}
}
// map this connection to this client
_conmap.put(conn, client);
}
// documentation inherited
public synchronized void connectionFailed (
Connection conn, IOException fault)
{
// remove the client from the connection map
PresentsClient client = (PresentsClient)_conmap.remove(conn);
if (client != null) {
Log.info("Unmapped failed client [client=" + client +
", conn=" + conn + ", fault=" + fault + "].");
// let the client know the connection went away
client.wasUnmapped();
// and let the client know things went haywire
client.connectionFailed(fault);
} else {
Log.info("Unmapped connection failed? [conn=" + conn +
", fault=" + fault + "].");
Thread.dumpStack();
}
}
// documentation inherited
public synchronized void connectionClosed (Connection conn)
{
// remove the client from the connection map
PresentsClient client = (PresentsClient)_conmap.remove(conn);
if (client != null) {
Log.debug("Unmapped client [client=" + client +
", conn=" + conn + "].");
// let the client know the connection went away
client.wasUnmapped();
} else {
Log.info("Closed unmapped connection '" + conn + "'. " +
"Client probably not yet authenticated.");
}
}
// documentation inherited from interface PresentsServer.Reporter
public void appendReport (StringBuffer report, long now, long sinceLast)
{
report.append("* presents.ClientManager:\n");
report.append("- Sessions: ");
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 the client instance when the client requests a logoff.
* This is called from the conmgr thread.
*/
synchronized void clientDidEndSession (PresentsClient client)
{
Credentials creds = client.getCredentials();
String username = client.getUsername();
// remove the client from the username map
PresentsClient rc = (PresentsClient)_usermap.remove(creds);
// sanity check just because we can
if (rc == null) {
Log.warning("Unregistered client ended session " + client + ".");
Thread.dumpStack();
// if they weren't in the username mapping, bail out now
// because the subsequent unmappings would just fail if we
// tried to do them for an unmapped client
return;
} else if (rc != client) {
Log.warning("Different clients with same username!? " +
"[c1=" + rc + ", c2=" + client + "].");
} else {
Log.info("Ending session " + client + ".");
}
// unmap (and destroy) the client object
unmapClientObject(username);
}
/**
* Called once per minute to check for clients that have been
* disconnected too long and forcibly end their sessions.
*/
protected void flushClients ()
{
ArrayList victims = null;
long now = System.currentTimeMillis();
// first build a list of our victims (we can't flush clients
// directly while iterating due to risk of a
// ConcurrentModificationException)
Iterator iter = _usermap.values().iterator();
while (iter.hasNext()) {
PresentsClient client = (PresentsClient)iter.next();
if (client.checkExpired(now)) {
if (victims == null) {
victims = new ArrayList();
}
victims.add(client);
}
}
if (victims != null) {
for (int ii = 0; ii < victims.size(); ii++) {
PresentsClient client = (PresentsClient)victims.get(ii);
Log.info("Client expired, ending session [client=" + client +
", dtime=" + (now-client.getNetworkStamp()) + "ms].");
client.endSession();
}
}
}
/** A mapping from usernames to client instances. */
protected HashMap _usermap = new HashMap();
/** A mapping from connections to client instances. */
protected HashMap _conmap = new HashMap();
/** A mapping from usernames to client object instances. */
protected HashMap _objmap = new HashMap();
/** A mapping of pending client resolvers. */
protected HashMap _penders = new HashMap();
/** A set containing the usernames of all locked clients. */
protected HashSet _locks = new HashSet();
/** The client class in use. */
protected Class _clientClass = PresentsClient.class;
/** The client resolver class in use. */
protected Class _clrClass = ClientResolver.class;
/** The frequency with which we check for expired clients. */
protected static final long CLIENT_FLUSH_INTERVAL = 60 * 1000L;
}