// // $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.server; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.List; import java.util.Map; import com.google.common.collect.Maps; import com.google.inject.Inject; import com.google.inject.Singleton; import com.samskivert.util.Histogram; import com.samskivert.util.IntMap; import com.samskivert.util.IntMaps; import com.samskivert.util.Interval; import com.samskivert.util.Queue; import com.samskivert.util.StringUtil; import com.samskivert.util.Throttle; import com.threerings.presents.dobj.AccessController; import com.threerings.presents.dobj.CompoundEvent; import com.threerings.presents.dobj.DEvent; import com.threerings.presents.dobj.DObject; import com.threerings.presents.dobj.DObjectManager; import com.threerings.presents.dobj.InvocationRequestEvent; import com.threerings.presents.dobj.NoSuchObjectException; import com.threerings.presents.dobj.ObjectAccessException; import com.threerings.presents.dobj.ObjectAddedEvent; import com.threerings.presents.dobj.ObjectDestroyedEvent; import com.threerings.presents.dobj.ObjectRemovedEvent; import com.threerings.presents.dobj.OidList; import com.threerings.presents.dobj.RootDObjectManager; import com.threerings.presents.dobj.Subscriber; import static com.threerings.presents.Log.log; /** * The presents distributed object manager implements the {@link DObjectManager} interface, * providing an object manager that runs on the server. By virtue of running on the server, it * manages its objects directly rather than managing proxies of objects which is what is done on * the client. Thus it simply queues up events and dispatches them to listeners. * *
The server object manager is meant to run on the main thread of the server application and
* thus provides a method to be invoked by the application main thread which won't return until the
* manager has been requested to shut down.
*/
@Singleton
public class PresentsDObjectMgr
implements RootDObjectManager
{
/** Returned by {@link #getStats}. */
public static class Stats
{
/** The largest size of the distributed object queue during the period. */
public int maxQueueSize;
/** The number of events dispatched during the period. */
public int eventCount;
}
/** Post instances of these if you know you're going to tie up the distributed object thread
* for a long time and don't want a spurious warning. Note: this should only be done
* during server initialization. Tying up the distributed object thread for long periods of
* time during normal operation is a very bad idea. */
public static interface LongRunnable extends Runnable
{
}
/**
* Creates the dobjmgr and prepares it for operation.
*/
@Inject public PresentsDObjectMgr (ReportManager repmgr)
{
// create a dummy object to live as oid zero and use that for some internal event trickery
DObject dummy = new DObject();
dummy.setOid(DUMMY_OID);
dummy.setManager(this);
_objects.put(DUMMY_OID, new DObject());
// register a couple of reports with the report manager
repmgr.registerReporter(ReportManager.DEFAULT_TYPE, new ReportManager.Reporter() {
public void appendReport (StringBuilder report, long now, long elapsed, boolean reset) {
report.append("* presents.PresentsDObjectMgr:\n");
Stats stats = getStats(reset);
int queueSize = _evqueue.size();
report.append("- Queue size: ").append(queueSize).append("\n");
report.append("- Max queue size: ").append(stats.maxQueueSize).append("\n");
report.append("- Units executed: ").append(stats.eventCount);
if (elapsed != 0) {
report.append(" (").append(stats.eventCount/(elapsed/1000)).append("/s)\n");
} else {
report.append(" (inf/s)\n");
}
}
});
repmgr.registerReporter(ReportManager.PROFILE_TYPE, new ReportManager.Reporter() {
public void appendReport (StringBuilder report, long now, long elapsed, boolean reset) {
report.append("* presents.PresentsDObjectMgr:\n");
if (UNIT_PROF_ENABLED) {
report.append("- Unit profiles: ").append(_profiles.size()).append("\n");
for (Map.EntryObjectDestroyedEvent events. It removes the object from
* the object table.
*
* @return true if the event should be dispatched, false if it should be aborted.
*/
public boolean objectDestroyed (DEvent event, DObject target)
{
int oid = target.getOid();
if (oid == DUMMY_OID) {
log.warning("Denying attempt to destroy dummy object!", new Exception());
return false;
}
// log.info("Removing destroyed object from table", "oid", oid);
// remove the object from the table
_objects.remove(oid);
// inactivate the object
target.setManager(null);
// deal with any remaining oid lists that reference this object
Reference[] refs = _refs.remove(oid);
if (refs != null) {
for (Reference ref : refs) {
// skip empty spots
if (ref == null) {
continue;
}
DObject reffer = _objects.get(ref.reffingOid);
// ensure that the referencing object is still around
if (reffer != null) {
// post an object removed event to clear the reference
postEvent(new ObjectRemovedEvent(ref.reffingOid, ref.field, oid));
// log.info("Forcing removal " + ref + ".");
} else {
log.info("Dangling reference from inactive object " + ref + ".");
}
}
}
// if this object has any oid list fields that are still referencing other objects, we need
// to clear out those references
Class> oclass = target.getClass();
Field[] fields = oclass.getFields();
for (Field field : fields) {
// ignore static and non-public fields
int mods = field.getModifiers();
if ((mods & Modifier.STATIC) != 0 || (mods & Modifier.PUBLIC) == 0) {
continue;
}
// ignore non-oidlist fields
if (!OidList.class.isAssignableFrom(field.getType())) {
continue;
}
try {
OidList list = (OidList)field.get(target);
for (int ii = 0; ii < list.size(); ii++) {
clearReference(target, field.getName(), list.get(ii));
}
} catch (Exception e) {
log.warning("Unable to clean up after oid list field", "target", target,
"field", field);
}
}
return true;
}
/**
* Called as a helper for ObjectAddedEvent events. It updates the object/oid list
* tracking structures.
*
* @return true if the event should be dispatched, false if it should be aborted.
*/
public boolean objectAdded (DEvent event, DObject target)
{
ObjectAddedEvent oae = (ObjectAddedEvent)event;
int oid = oae.getOid();
// ensure that the target object exists
if (!_objects.containsKey(oid)) {
log.info("Rejecting object added event of non-existent object",
"refferOid", target.getOid(), "reffedOid", oid);
return false;
}
// get the reference vector for the referenced object. we use bare arrays rather than
// something like an array list to conserve memory. there will be many objects and
// references
Reference[] refs = _refs.get(oid);
if (refs == null) {
refs = new Reference[DEFREFVEC_SIZE];
_refs.put(oid, refs);
}
// determine where to add the reference
Reference ref = new Reference(target.getOid(), oae.getName(), oid);
int rpos = -1;
for (int ii = 0; ii < refs.length; ii++) {
if (ref.equals(refs[ii])) {
log.warning("Ignoring request to track existing reference " + ref + ".");
return true;
} else if (refs[ii] == null && rpos == -1) {
rpos = ii;
}
}
// expand the refvec if necessary
if (rpos == -1) {
Reference[] nrefs = new Reference[refs.length*2];
System.arraycopy(refs, 0, nrefs, 0, refs.length);
rpos = refs.length;
_refs.put(oid, refs = nrefs);
}
// finally add the reference
refs[rpos] = ref;
// log.info("Tracked reference " + ref + ".");
return true;
}
/**
* Called as a helper for ObjectRemovedEvent events. It updates the object/oid
* list tracking structures.
*
* @return true if the event should be dispatched, false if it should be aborted.
*/
public boolean objectRemoved (DEvent event, DObject target)
{
ObjectRemovedEvent ore = (ObjectRemovedEvent)event;
String field = ore.getName();
int toid = target.getOid();
int oid = ore.getOid();
// log.info("Processing object removed", "from", toid, "roid", toid);
// get the reference vector for the referenced object
Reference[] refs = _refs.get(oid);
if (refs == null) {
// this can happen normally when an object is destroyed. it will remove itself from the
// reference system and then generate object removed events for all of its referencees.
// so we opt not to log anything in this case
// log.info("Object removed without reference to track it", "toid", toid,
// "field", field, "oid", oid);
return true;
}
// look for the matching reference
for (int ii = 0; ii < refs.length; ii++) {
Reference ref = refs[ii];
if (ref != null && ref.equals(toid, field)) {
// log.info("Removed reference " + refs[i] + ".");
refs[ii] = null;
return true;
}
}
log.warning("Unable to locate reference for removal", "reffingOid", toid, "field", field,
"reffedOid", oid);
return true;
}
/**
* Should not need to be called except by the invoker during shutdown to ensure that things are
* proceeding smoothly.
*/
public boolean queueIsEmpty ()
{
return !_evqueue.hasElements();
}
/**
* Tests if the event processing thread is still running. This is required by the
* ConnectionManager to ensure messages posted just before or during shutdown are sent.
*/
public synchronized boolean isRunning ()
{
return _running;
}
/**
* Processes a single unit from the queue.
*/
protected void processUnit (Object unit)
{
long start = System.nanoTime();
// keep track of the largest queue size we've seen
int queueSize = _evqueue.size();
if (queueSize > _current.maxQueueSize) {
_current.maxQueueSize = queueSize;
}
try {
if (unit instanceof Runnable) {
// if this is a runnable, it's just an executable unit that should be invoked
((Runnable)unit).run();
} else {
DEvent event = (DEvent)unit;
// if this event is on a proxied object, forward it to the owning manager
ProxyReference proxy = _proxies.get(event.getTargetOid());
if (proxy != null) {
// rewrite the oid into the originating manager's id space
event.setTargetOid(proxy.origObjectId);
// then pass it on to the originating manager to handle
proxy.origManager.postEvent(event);
} else if (event instanceof CompoundEvent) {
processCompoundEvent((CompoundEvent)event);
} else {
processEvent(event);
}
}
} catch (VirtualMachineError e) {
handleFatalError(unit, e);
} catch (Throwable t) {
log.warning("Execution unit failed", "unit", unit, t);
}
// compute the elapsed time in microseconds
long elapsed = (System.nanoTime() - start)/1000;
// report excessively long units
if (elapsed > 500000 && !(unit instanceof LongRunnable)) {
log.warning("Long dobj unit " + StringUtil.shortClassName(unit), "unit", unit,
"time", (elapsed/1000) + "ms");
}
// periodically sample and record the time spent processing a unit
if (UNIT_PROF_ENABLED && _eventCount % UNIT_PROF_INTERVAL == 0) {
String cname;
// do some jiggery pokery to get more fine grained profiling details on certain
// "popular" unit types
if (unit instanceof Interval.RunBuddy) {
Interval ival = ((Interval.RunBuddy)unit).getInterval();
cname = StringUtil.shortClassName(ival);
} else if (unit instanceof InvocationRequestEvent) {
InvocationRequestEvent ire = (InvocationRequestEvent)unit;
Class> c = _invmgr.getDispatcherClass(ire.getInvCode());
cname = (c == null) ? "dobj.InvocationRequestEvent:(no longer registered)" :
StringUtil.shortClassName(c) + ":" + ire.getMethodId();
} else {
cname = StringUtil.shortClassName(unit);
}
UnitProfile uprof = _profiles.get(cname);
if (uprof == null) {
_profiles.put(cname, uprof = new UnitProfile());
}
uprof.record(start, elapsed);
}
}
/**
* Performs the processing associated with a compound event, notifying listeners and the like.
*/
protected void processCompoundEvent (CompoundEvent event)
{
ListobjectDestroyed; clears out the tracking info for a reference by the
* supplied object to the specified oid via the specified field.
*/
protected void clearReference (DObject reffer, String field, int reffedOid)
{
// look up the reference vector for the referenced object
Reference[] refs = _refs.get(reffedOid);
Reference ref = null;
if (refs != null) {
for (int ii = 0; ii < refs.length; ii++) {
if (refs[ii].equals(reffer.getOid(), field)) {
ref = refs[ii];
refs[ii] = null;
break;
}
}
}
// if a referred object and referring object are both destroyed without allowing the
// referred object destruction to process the ObjectRemoved event which is auto-generated,
// the subsequent destruction of the referring object will attempt to clear the reference
// to the referred object which no longer exists; so we don't complain about non- existent
// references if the referree is already destroyed
if (ref == null && _objects.containsKey(reffedOid)) {
log.warning("Requested to clear out non-existent reference",
"refferOid", reffer.getOid(), "field", field, "reffedOid", reffedOid);
// } else {
// log.info("Cleared out reference " + ref + ".");
}
}
protected int getNextOid ()
{
// look for the next unused oid. in theory if we had two billion objects, this would loop
// infinitely, but the world will come to an end long before we have two billion objects
do {
_nextOid = (_nextOid + 1) % Integer.MAX_VALUE;
} while (_objects.containsKey(_nextOid));
return _nextOid;
}
/**
* Registers our event helper methods.
*/
protected void registerEventHelpers ()
{
try {
_helpers.put(ObjectDestroyedEvent.class, new EventHelper () {
public boolean invoke (DEvent event, DObject target) {
return objectDestroyed(event, target);
}
});
_helpers.put(ObjectAddedEvent.class, new EventHelper() {
public boolean invoke (DEvent event, DObject target) {
return objectAdded(event, target);
}
});
_helpers.put(ObjectRemovedEvent.class, new EventHelper() {
public boolean invoke (DEvent event, DObject target) {
return objectRemoved(event, target);
}
});
} catch (Exception e) {
log.warning("Unable to register event helpers", "error", e);
}
}
/**
* Calls {@link Subscriber#objectAvailable} and catches and logs any exception thrown by the
* subscriber during the call.
*/
protected static