
com.threerings.presents.server.PresentsDObjectMgr Maven / Gradle / Ivy
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2012 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.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);
_objects.put(DUMMY_OID, dummy);
// 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.Entry entry : _profiles.entrySet()) {
report.append(" ").append(entry.getKey());
report.append(" ").append(entry.getValue()).append("\n");
}
} else {
report.append("- Unit profiles disabled.\n");
}
}
});
// 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 void subscribeToObject (int oid, Subscriber target)
{
// queue up an access object event
postEvent(new AccessObjectEvent(oid, target, AccessObjectEvent.SUBSCRIBE));
}
// from interface DObjectManager
public void unsubscribeFromObject (int oid, Subscriber target)
{
// queue up an access object event
postEvent(new AccessObjectEvent(oid, target, AccessObjectEvent.UNSUBSCRIBE));
}
// from interface DObjectManager
public void postEvent (DEvent event)
{
if (!_running) {
log.warning("Posting message to inactive object manager", "event", event,
new Exception());
}
// assign the event's id and append it to the queue
event.eventId = getNextEventId(true);
_evqueue.append(event);
}
// from interface DObjectManager
public void removedLastSubscriber (DObject obj, boolean deathWish)
{
// destroy the object if it so desires
if (deathWish) {
destroyObject(obj.getOid());
}
}
// from interface RootDObjectManager
public T registerObject (T object)
{
if (_dobjThread != null && !isDispatchThread()) {
log.warning("Registering DObject on non-dobject thread",
"class", object.getClass().getName(), new Exception());
}
int oid = getNextOid();
// initialize this object
object.setOid(oid);
object.setManager(this);
// set the default access controller if a controller hasn't already been specified
if (object.getAccessController() == null) {
object.setAccessController(_defaultController);
}
// insert it into the table
_objects.put(oid, object);
// log.info("Registered object", "obj", object);
return object;
}
// from interface RootDObjectManager
public void destroyObject (int oid)
{
if (oid == DUMMY_OID) {
log.warning("Denying request to destroy the dummy object!", new Exception());
return;
}
// queue up an object destroyed event
postEvent(new ObjectDestroyedEvent(oid));
}
// from interface RootDObjectManager
public Interval newInterval (final Runnable action)
{
return new Interval(this) {
@Override public void expired () {
action.run();
}
@Override public String toString () {
return "DObjectManagerInterval(" + action + ")";
}
@Override protected void noteRejected () {
// don't complain
}
};
}
/**
* Returns the object in the object table with the specified oid or null if no object has that
* oid. Be sure only to call this function from the dobjmgr thread and not to do anything funny
* with the object. If subscription is desired, use {@link #subscribeToObject}.
*/
public DObject getObject (int oid)
{
return _objects.get(oid);
}
/**
* Returns a recent snapshot of runtime statistics tracked by the distributed object manager.
*
* @param snapshot if true, the current stats will be snapshotted and reset and the new
* snapshot will be returned. If false, the previous snapshot will be returned. If no snapshot
* has ever been taken, the current stats that have been accumulting since the JVM start will
* be returned.
*/
public Stats getStats (boolean snapshot)
{
if (snapshot) {
_recent = _current;
_current = new Stats();
_current.maxQueueSize = _evqueue.size();
}
return _recent;
}
// from Executor
public void execute (Runnable command)
{
postRunnable(command);
}
/**
* Posts a self-contained unit of code that should be run on the distributed object manager
* thread at the next available opportunity. The code will be queued up with the rest of the
* events and invoked in turn. Like event processing code, the code should not take long to
* complete and should definitely not block.
*
* From interface RunQueue
*/
public void postRunnable (Runnable unit)
{
if (!_running) {
log.warning("Posting runnable to inactive object manager", "unit", unit,
new Exception());
}
// just append it to the queue
_evqueue.append(unit);
}
/**
* Returns true if the thread invoking this method is the same thread that is doing distributed
* object event dispatch. Code that wishes to enforce that it is either always or never called
* on the event dispatch thread will want to make use of this method.
*
* From interface RunQueue
*/
public synchronized boolean isDispatchThread ()
{
return Thread.currentThread() == _dobjThread;
}
/**
* Ensures that the calling thread is the distributed object event dispatch thread, throwing an
* {@link IllegalStateException} if it is not. Note: before the manager is started up,
* all calls to this method will succeed, as things that take place during server
* initialization are considered safe and only after the distributed object manager is started
* (and the event thread is established) will we require it.
*/
public synchronized void requireEventThread ()
{
if (_dobjThread != null && !isDispatchThread()) {
throw new IllegalStateException("This method must be called on the dobj event thread.");
}
}
/**
* Ensures that the calling thread is not the distributed object event dispatch
* thread, throwing an {@link IllegalStateException} if it is.
*/
public void refuseEventThread ()
{
if (isDispatchThread()) {
throw new IllegalStateException(
"This method must not be called on the dobj event thread.");
}
}
/**
* Runs the dobjmgr event loop until it is requested to exit. This should be called from the
* main application thread.
*/
public void run ()
{
log.info("DOMGR running.");
// make a note of the thread that's processing events
synchronized (this) {
_dobjThread = Thread.currentThread();
}
while (isRunning()) {
// pop the next unit off the queue and process it
processUnit(_evqueue.get());
}
log.info("DOMGR exited.");
}
/**
* Requests that the dobjmgr shut itself down directly. You may want to try using {@link
* PresentsInvoker#shutdown} which will make sure that both the Invoker and DObjectMgr are
* empty and then shut them both down.
*/
public void harshShutdown ()
{
postRunnable(new Runnable() {
public void run () {
_running = false;
}
});
}
/**
* Sets the frequency at which we take profiling samples.
*/
public void setUnitProfInterval (int interval)
{
_unitProfInterval = interval;
}
/**
* Returns the profiling sample frequency.
*/
public int getUnitProfInterval ()
{
return _unitProfInterval;
}
/**
* Dumps collected profiling information to the system log.
*/
public void dumpUnitProfiles ()
{
for (Map.Entry entry : _profiles.entrySet()) {
log.info("P: " + entry.getKey() + " => " + entry.getValue());
}
}
/**
* Clears the current set of unit profiles.
*/
public void clearUnitProfiles ()
{
_profiles.clear();
}
/**
* Called as a helper for 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();
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);
// deactivate the object
target.setManager(null);
// if it's using the default access controller, clear it out so it'll get the current
// default controller if it's reactivated.
if (target.getAccessController() == _defaultController) {
target.setAccessController(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 % _unitProfInterval == 0) {
String cname;
// do some jiggery pokery to get more fine grained profiling details on certain
// "popular" unit types
if (unit instanceof Interval.RunBuddy) {
cname = StringUtil.shortClassName(
((Interval.RunBuddy)unit).getIntervalClassName());
} 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(elapsed);
}
}
/**
* Performs the processing associated with a compound event, notifying listeners and the like.
*/
protected void processCompoundEvent (CompoundEvent event)
{
List events = event.getEvents();
int ecount = events.size();
// look up the target object
DObject target = _objects.get(event.getTargetOid());
if (target == null) {
log.debug("Compound event target no longer exists", "event", event);
return;
}
// check the permissions on all of the events
for (int ii = 0; ii < ecount; ii++) {
DEvent sevent = events.get(ii);
if (!target.checkPermissions(sevent)) {
log.warning("Event failed permissions check", "event", sevent, "target", target);
return;
}
}
// dispatch the events
for (int ii = 0; ii < ecount; ii++) {
dispatchEvent(events.get(ii), target);
}
// always notify proxies of compound events
target.notifyProxies(event);
}
/**
* Performs the processing associated with an event, notifying listeners and the like.
*/
protected void processEvent (DEvent event)
{
// look up the target object
DObject target = _objects.get(event.getTargetOid());
if (target == null) {
log.debug("Event target no longer exists", "event", event);
return;
}
// check the event's permissions
if (!target.checkPermissions(event)) {
log.warning("Event failed permissions check", "event", event, "target", target);
return;
}
if (dispatchEvent(event, target)) {
// unless requested not to, notify any proxies
target.notifyProxies(event);
}
}
/**
* Dispatches an event after the target object has been resolved and the permissions have been
* checked. This is used by {@link #processEvent} and {@link #processCompoundEvent}.
*
* @return the value returned by {@link DEvent#applyToObject}.
*/
protected boolean dispatchEvent (DEvent event, DObject target)
{
boolean notify = true; // assume always notify
try {
// do any internal management necessary based on this event
EventHelper helper = _helpers.get(event.getClass());
if (helper != null) {
// if helper returns false, we abort event processing
if (!helper.invoke(event, target)) {
return false;
}
}
// everything's good so far, apply the event to the object
notify = event.applyToObject(target);
// if the event returns false from applyToObject, this means it's a silent event and we
// shouldn't notify the listeners
if (notify) {
target.notifyListeners(event);
}
} catch (VirtualMachineError e) {
handleFatalError(event, e);
} catch (Throwable t) {
log.warning("Failure processing event", "event", event, "target", target, t);
}
// track the number of events dispatched
++_eventCount;
++_current.eventCount;
return true;
}
/**
* Attempts to recover from fatal errors but rethrows if things are freaking out too
* frequently.
*/
protected void handleFatalError (Object causer, Error error)
{
if (_fatalThrottle.throttleOp()) {
throw error;
}
log.warning("Fatal error caused by '" + causer + "': " + error, error);
}
/**
* Called by objectDestroyed
; 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 void informObjectAvailable (Subscriber sub, T obj)
{
try {
sub.objectAvailable(obj);
} catch (Exception e) {
log.warning("Subscriber choked during object available",
"obj", StringUtil.safeToString(obj), "sub", sub, e);
}
}
/**
* Used to make an object available to a subscriber (with or without the associated
* subscription).
*/
protected class AccessObjectEvent extends DEvent
{
public static final int SUBSCRIBE = 0;
public static final int UNSUBSCRIBE = 1;
public AccessObjectEvent (int oid, Subscriber target, int action)
{
super(DUMMY_OID); // target the bogus object
_oid = oid;
_target = target;
_action = action;
}
@Override
public boolean isPrivate ()
{
return true;
}
@Override
public boolean applyToObject (DObject target)
throws ObjectAccessException
{
// sanity check; we do this check here rather than in subscribeToObject() to ensure
// that we always dispatch our response on the dobjmgr thread
if (_oid <= 0) {
_target.requestFailed(_oid, new ObjectAccessException("Invalid oid " + _oid + "."));
return false;
}
// look up the target object
@SuppressWarnings("unchecked") T obj = (T)_objects.get(_oid);
// if we're unsubscribing, take care of that and get on out
if (_action == UNSUBSCRIBE) {
if (obj != null) {
obj.removeSubscriber(_target);
}
return false;
}
// if it don't exist, let them know
if (obj == null) {
_target.requestFailed(_oid, new NoSuchObjectException(_oid));
return false;
}
// check permissions
if (!obj.checkPermissions(_target)) {
String errmsg = "m.access_denied\t" + _oid;
_target.requestFailed(_oid, new ObjectAccessException(errmsg));
return false;
}
// subscribe 'em
obj.addSubscriber(_target);
// let them know that things are groovy
informObjectAvailable(_target, obj);
// return false to ensure that this event is not dispatched to the fake object's
// subscriber list (even though it's empty)
return false;
}
protected int _oid;
protected Subscriber _target;
protected int _action;
}
/**
* Used to track references of objects in oid lists.
*/
protected static class Reference
{
public int reffingOid;
public String field;
public int reffedOid;
public Reference (int reffingOid, String field, int reffedOid) {
this.reffingOid = reffingOid;
this.field = field;
this.reffedOid = reffedOid;
}
public boolean equals (Reference other) {
if (other == null) {
return false;
} else {
return (reffingOid == other.reffingOid && field.equals(other.field));
}
}
public boolean equals (int oReffingOid, String oField) {
return (reffingOid == oReffingOid && field.equals(oField));
}
@Override public int hashCode () {
return reffingOid ^ field.hashCode();
}
@Override public String toString () {
return "[reffingOid=" + reffingOid + ", field=" + field +
", reffedOid=" + reffedOid + "]";
}
}
/** Used to profile time spent invoking units and processing events if such profiling is
* enabled. */
protected static class UnitProfile
{
public void record (long elapsed)
{
_totalElapsed += elapsed;
_histo.addValue((int)elapsed);
_longest = Math.max(elapsed, _longest);
}
@Override
public String toString ()
{
int count = _histo.size();
return _totalElapsed + "us/" + count + " = " + (_totalElapsed/count) + "us avg " +
StringUtil.toString(_histo.getBuckets()) + " " + _longest + "us longest";
}
protected long _totalElapsed, _longest;
protected Histogram _histo = new Histogram(0, 20000, 10);
}
/** Tracks necessary information on a proxy distributed object. */
protected static class ProxyReference
{
public int origObjectId;
public DObjectManager origManager;
public ProxyReference (int origObjectId, DObjectManager origManager)
{
this.origObjectId = origObjectId;
this.origManager = origManager;
}
}
/** Does some helpy bits for certain events. */
protected static interface EventHelper
{
public boolean invoke (DEvent event, DObject target);
}
/** A flag indicating that the event dispatcher is still running. */
protected boolean _running = true;
/** The event queue via which all events are processed. */
protected Queue