// // $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.lang.reflect.*; 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.AuditLogger; import com.samskivert.util.Histogram; import com.samskivert.util.IntMap; import com.samskivert.util.IntMaps; import com.samskivert.util.Interval; import com.samskivert.util.Invoker; import com.samskivert.util.Queue; import com.samskivert.util.RunQueue; import com.samskivert.util.StringUtil; import com.samskivert.util.Throttle; import com.threerings.presents.dobj.*; 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, RunQueue, ReportManager.Reporter
{
/** Contains operational statistics that are tracked by the distributed object manager between
* {@link ReportManager#Reporter} intervals. The snapshot for the most recently completed
* period can be requested via {@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(0);
dummy.setManager(this);
_objects.put(0, new DObject());
// register ourselves as a state of server reporter and also tell the report manager that
// we're providing the runqueue for it to do its business
repmgr.registerReporter(this);
repmgr.init(this);
// register our event helpers
registerEventHelpers();
}
/**
* Returns the id to be assigned to the next event posted to the event queue.
*
* @param increment if true, the event id will be incremented so that the caller can "claim"
* the returned event id.
*/
public synchronized long getNextEventId (boolean increment)
{
return increment ? _nextEventId++ : _nextEventId;
}
/**
* Sets up an access controller that will be provided to any distributed objects created on the
* server. The controllers can subsequently be overridden if desired, but a default controller
* is useful for implementing basic access control policies.
*/
public void setDefaultAccessController (AccessController controller)
{
AccessController oldDefault = _defaultController;
_defaultController = controller;
// switch all objects from the old default (null, usually) to the new default.
for (DObject obj : _objects.values()) {
if (oldDefault == obj.getAccessController()) {
obj.setAccessController(controller);
}
}
}
/**
* Registers an object managed by another distributed object manager (probably on another
* server). The local server will assign the object a proxy oid, and any events that come in on
* this object will be rewritten from their proxy oid to their original id before forwarding on
* to the originating object manager.
*/
public void registerProxyObject (DObject object, DObjectManager omgr)
{
int origObjectId = object.getOid();
// register the object locally which will reassign its oid and set us as its manager
registerObject(object);
// and note a proxy reference for the object which we'll use to forward events back to its
// originating manager after converting them back to the original oid
_proxies.put(object.getOid(), new ProxyReference(origObjectId, omgr));
// TEMP: report what we're doing as we're seeing funny business
log.info("Registered proxy object [type=" + object.getClass().getName() +
", remoid=" + origObjectId + ", locoid=" + object.getOid() + "].");
}
/**
* Clears a proxy object reference from our local distributed object space. This merely removes
* it from our internal tables, the caller is responsible for coordinating the deregistration
* of the object with the proxying client.
*/
public void clearProxyObject (int origObjectId, DObject object)
{
if (_proxies.remove(object.getOid()) == null) {
log.warning("Missing proxy mapping for cleared proxy [ooid=" + origObjectId + "].");
}
_objects.remove(object.getOid());
// TEMP: report what we're doing as we're seeing funny business
log.info("Clearing proxy object [type=" + object.getClass().getName() +
", remoid=" + origObjectId + ", locoid=" + object.getOid() + "].");
}
// from interface DObjectManager
public boolean isManager (DObject object)
{
// we are always authoritative in the present implementation
return true;
}
// from interface DObjectManager
public ObjectDestroyedEvent 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();
// 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 (int i = 0; i < refs.length; i++) {
// skip empty spots
if (refs[i] == null) {
continue;
}
Reference ref = refs[i];
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 (int f = 0; f < fields.length; f++) {
Field field = fields[f];
// 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 i = 0; i < list.size(); i++) {
clearReference(target, field.getName(), list.get(i));
}
} 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 i = 0; i < refs.length; i++) {
if (ref.equals(refs[i])) {
log.warning("Ignoring request to track existing reference " + ref + ".");
return true;
} else if (refs[i] == null && rpos == -1) {
rpos = i;
}
}
// 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 i = 0; i < refs.length; i++) {
Reference ref = refs[i];
if (ref != null && ref.equals(toid, field)) {
// log.info("Removed reference " + refs[i] + ".");
refs[i] = 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();
}
// from interface ReportManager.Reporter
public void appendReport (StringBuilder report, long now, long sinceLast, boolean reset)
{
report.append("* presents.PresentsDObjectMgr:\n");
int queueSize = _evqueue.size();
report.append("- Queue size: ").append(queueSize).append("\n");
report.append("- Max queue size: ").append(_current.maxQueueSize).append("\n");
report.append("- Units executed: ").append(_current.eventCount).append("\n");
if (UNIT_PROF_ENABLED) {
report.append("- Unit profiles: ").append(_profiles.size()).append("\n");
for (Map.EntryobjectDestroyed; 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 i = 0; i < refs.length; i++) {
if (refs[i].equals(reffer.getOid(), field)) {
ref = refs[i];
refs[i] = 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 synchronized boolean isRunning ()
{
return _running;
}
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 ()
{
Class[] ptypes = new Class[] { DEvent.class, DObject.class };
Method method;
try {
method = PresentsDObjectMgr.class.getMethod("objectDestroyed", ptypes);
_helpers.put(ObjectDestroyedEvent.class, method);
method = PresentsDObjectMgr.class.getMethod("objectAdded", ptypes);
_helpers.put(ObjectAddedEvent.class, method);
method = PresentsDObjectMgr.class.getMethod("objectRemoved", ptypes);
_helpers.put(ObjectRemovedEvent.class, method);
} 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