// // $Id$ // // Narya library - tools for developing networked games // Copyright (C) 2002-2007 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.server; import java.util.List; import java.util.Map; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.inject.Inject; import com.google.inject.Singleton; import com.samskivert.util.IntMaps; import com.samskivert.util.IntMap; import com.samskivert.util.LRUHashMap; import com.samskivert.util.StringUtil; import com.threerings.io.Streamable; import com.threerings.presents.data.ClientObject; import com.threerings.presents.data.InvocationMarshaller.ListenerMarshaller; import com.threerings.presents.data.InvocationMarshaller; import com.threerings.presents.dobj.DEvent; import com.threerings.presents.dobj.DObject; import com.threerings.presents.dobj.EventListener; import com.threerings.presents.dobj.InvocationRequestEvent; import com.threerings.presents.net.Transport; import static com.threerings.presents.Log.log; /** * The invocation services provide client to server invocations (service requests) and server to * client invocations (responses and notifications). Via this mechanism, the client can make * requests of the server, be notified of its response and the server can asynchronously invoke * code on the client. * *

Invocations are like remote procedure calls in that they are named and take arguments. All * arguments must be {@link Streamable} objects, primitive types, or String objects. All arguments * are passed by value (by serializing and unserializing the arguments); there is no special * facility provided for referencing non-local objects (it is assumed that the distributed object * facility will already be in use for any objects that should be shared). * *

The server invocation manager listens for invocation requests from the client and passes * them on to the invocation provider registered for the requested invocation module. It also * provides a mechanism by which responses and asynchronous notification invocations can be * delivered to the client. */ @Singleton public class InvocationManager implements EventListener { /** * Constructs an invocation manager which will use the supplied distributed object manager to * operate its invocation services. Generally only one invocation manager should be operational * in a particular system. */ @Inject public InvocationManager (PresentsDObjectMgr omgr) { _omgr = omgr; _omgr._invmgr = this; // create the object on which we'll listen for invocation requests DObject invobj = _omgr.registerObject(new DObject()); invobj.addListener(this); _invoid = invobj.getOid(); // log.info("Created invocation service object [oid=" + _invoid + "]."); } /** * Returns the object id of the invocation services object. */ public int getOid () { return _invoid; } /** * Registers the supplied invocation dispatcher, returning a marshaller that can be used to * send requests to the provider for whom the dispatcher is proxying. * * @param dispatcher the dispatcher to be registered. */ public T registerDispatcher ( InvocationDispatcher dispatcher) { return registerDispatcher(dispatcher, null); } /** * @Deprecated use {@link #registerDispatcher(InvocationDispatcher)}. */ public T registerDispatcher ( InvocationDispatcher dispatcher, boolean bootstrap) { return registerDispatcher(dispatcher, null); } /** * Registers the supplied invocation dispatcher, returning a marshaller that can be used to * send requests to the provider for whom the dispatcher is proxying. * * @param dispatcher the dispatcher to be registered. * @param group the bootstrap group in which this marshaller is to be registered, or null if it * is not a bootstrap service. Do not: register a dispatcher with multiple boot * groups. You must collect shared dispatchers into as fine grained a set of groups as * necessary and have different types of clients specify the list of groups they need. */ public T registerDispatcher ( InvocationDispatcher dispatcher, String group) { _omgr.requireEventThread(); // sanity check // get the next invocation code int invCode = nextInvCode(); // create the marshaller and initialize it T marsh = dispatcher.createMarshaller(); marsh.init(_invoid, invCode); // register the dispatcher _dispatchers.put(invCode, dispatcher); // if it's a bootstrap service, slap it in the list if (group != null) { List list = _bootlists.get(group); if (list == null) { _bootlists.put(group, list = Lists.newArrayList()); } list.add(marsh); } _recentRegServices.put(Integer.valueOf(invCode), marsh.getClass().getName()); // log.info("Registered service [marsh=" + marsh + "]."); return marsh; } /** * Clears out a dispatcher registration. This should be called to free up resources when an * invocation service is no longer going to be used. */ public void clearDispatcher (InvocationMarshaller marsh) { _omgr.requireEventThread(); // sanity check if (marsh == null) { log.warning("Refusing to unregister null marshaller."); Thread.dumpStack(); return; } if (_dispatchers.remove(marsh.getInvocationCode()) == null) { log.warning("Requested to remove unregistered marshaller? " + "[marsh=" + marsh + "]."); Thread.dumpStack(); } } /** * Constructs a list of all bootstrap services registered in any of the supplied groups. */ public List getBootstrapServices (String[] bootGroups) { List services = Lists.newArrayList(); for (String group : bootGroups) { List list = _bootlists.get(group); if (list != null) { services.addAll(list); } } return services; } /** * Get the class that is being used to dispatch the specified invocation code, for * informational purposes. * * @return the Class, or null if no dispatcher is registered with * the specified code. */ public Class getDispatcherClass (int invCode) { Object dispatcher = _dispatchers.get(invCode); return (dispatcher == null) ? null : dispatcher.getClass(); } // documentation inherited from interface public void eventReceived (DEvent event) { // log.info("Event received " + event + "."); if (event instanceof InvocationRequestEvent) { InvocationRequestEvent ire = (InvocationRequestEvent)event; dispatchRequest(ire.getSourceOid(), ire.getInvCode(), ire.getMethodId(), ire.getArgs(), ire.getTransport()); } } /** * Called when we receive an invocation request message. Dispatches the request to the * appropriate invocation provider via the registered invocation dispatcher. */ protected void dispatchRequest ( int clientOid, int invCode, int methodId, Object[] args, Transport transport) { // make sure the client is still around ClientObject source = (ClientObject)_omgr.getObject(clientOid); if (source == null) { log.info("Client no longer around for invocation " + "request [clientOid=" + clientOid + ", code=" + invCode + ", methId=" + methodId + ", args=" + StringUtil.toString(args) + "]."); return; } // look up the dispatcher InvocationDispatcher disp = _dispatchers.get(invCode); if (disp == null) { log.info("Received invocation request but dispatcher " + "registration was already cleared [code=" + invCode + ", methId=" + methodId + ", args=" + StringUtil.toString(args) + ", marsh=" + _recentRegServices.get(Integer.valueOf(invCode)) + "]."); return; } // scan the args, initializing any listeners and keeping track of the "primary" listener ListenerMarshaller rlist = null; int acount = args.length; for (int ii = 0; ii < acount; ii++) { Object arg = args[ii]; if (arg instanceof ListenerMarshaller) { ListenerMarshaller list = (ListenerMarshaller)arg; list.omgr = _omgr; list.transport = transport; // keep track of the listener we'll inform if anything // goes horribly awry if (rlist == null) { rlist = list; } } } // log.debug("Dispatching invreq [caller=" + source.who() + // ", disp=" + disp + ", methId=" + methodId + // ", args=" + StringUtil.toString(args) + "]."); // dispatch the request try { if (rlist != null) { rlist.setInvocationId(StringUtil.shortClassName(disp) + ", methodId=" + methodId); } disp.dispatchRequest(source, methodId, args); } catch (InvocationException ie) { if (rlist != null) { rlist.requestFailed(ie.getMessage()); } else { log.warning("Service request failed but we've got no " + "listener to inform of the failure " + "[caller=" + source.who() + ", code=" + invCode + ", dispatcher=" + disp + ", methodId=" + methodId + ", args=" + StringUtil.toString(args) + ", error=" + ie + "]."); } } catch (Throwable t) { log.warning("Dispatcher choked [disp=" + disp + ", caller=" + source.who() + ", methId=" + methodId + ", args=" + StringUtil.toString(args) + "].", t); // avoid logging an error when the listener notices that it's been ignored. if (rlist != null) { rlist.setNoResponse(); } } } /** * Used to generate monotonically increasing provider ids. */ protected synchronized int nextInvCode () { return _invCode++; } /** The object id of the object on which we receive invocation service requests. */ protected int _invoid = -1; /** Used to generate monotonically increasing provider ids. */ protected int _invCode; /** The distributed object manager we're working with. */ protected PresentsDObjectMgr _omgr; /** A table of invocation dispatchers each mapped by a unique code. */ protected IntMap> _dispatchers = IntMaps.newHashIntMap(); /** Maps bootstrap group to lists of services to be provided to clients at boot time. */ protected Map> _bootlists = Maps.newHashMap(); /** Tracks recently registered services so that we can complain informatively if a request * comes in on a service we don't know about. */ protected final Map _recentRegServices = new LRUHashMap(10000); /** The text appended to the procedure name when generating a failure response. */ protected static final String FAILED_SUFFIX = "Failed"; }