Files
Michael Bayne e0aea87f62 Moved the Presents overview out of the javadocs. Added some simple top-level
pages which give us something useful to link to when we want to point to all of
the javadocs for a particular subtree of the Narya packages.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@5411 542714f4-19e9-0310-aa3c-eee0fc999fb1
2008-10-02 00:47:06 +00:00

607 lines
25 KiB
HTML

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<!-- $Id: package.html 617 2001-11-13 00:12:20Z mdb $ -->
<link rel="stylesheet" href="../stylesheet.css" type="text/css"/>
</head>
<body bgcolor="white">
<h2>Presents Distributed Object System</h2>
The Presents Distributed Object System is a framework for distributing
information between multiple separate applications (over a network) and for
coordinating control flow between those applications in the form of remote
procedure call services. The normal configuration of the Presents system is
client/server; generally with many clients connecting to a single
server. All information transfer takes place through the server using the
distributed object system documented below.
<ul>
<li><a href="#distributed_objects">Distributed Objects</a>
<li><a href="#event_listeners">Event Listeners</a>
<li><a href="#distributed_collections">Distributed Collections</a>
<li><a href="#invocation_services">Invocation Services</a>
<li><a href="#ant_tasks">Ant Tasks</a>
</ul>
<p> <em>A note to the reader:</em> the Presents system is a complex
one and though a great deal of code is provided in explaining the
services it provides, it is not the intent that one should start from
only these examples and build a working system. A better approach is
to read through this documentation to come to an understanding of the
concepts and mechanisms that define the system and then take a look at
some working sample code which is provided in the <code>tests</code>
directory of this distribution.
<h3><a name="distributed_objects">Distributed Objects</a></h3>
The Presents services allow applications to access and update shared
information through a mechanism known as distributed objects.
Distributed objects are maintainedon the server and clients
"subscribe" to the objects and are provided with proxy copies which
are updated by a stream of events sent by the server when any state
changes in the objects.
<p> Clients cannot modify their proxy distributed objects directly,
instead they make use of setter methods which package up the requested
change into an event and send that event to the server for processing.
After performing access control checks, the server will apply the
event to the primary distributed object instance and then dispatch
that event to all subscribed clients. Those clients (including the
original change requesting client) then apply the event to their proxy
copy of the object and in this way all clients maintain an up to date
copy of the object's data.
<p align="center"> <img src="images/dobject.png">
<h4>Defining an object</h4>
A distributed object is defined just like a regular Java object and is
then run through a post-processor which inserts methods and constants
into the object definition which are needed by the distributed object
system. Here is a distributed object as originally defined:
<pre class="example">
public class CageObject extends DObject
{
/** The number of monkeys in the cage. */
public int monkeys;
/** The name of the owner of this cage. */
public String owner;
}</pre>
Note that all distributed fields, or attributes (fields in a
distributed object are frequently referred to as <i>attributes</i> in
this documentation and elsewhere in the system), are public fields in
our distributed object. Non-public fields will be ignored by the
system and not transmitted when a proxy object is delivered over the
network to a subscriber. Further, fields marked <code>transient</code>
will also be ignored by the system.
<p> We then run our class definition through a post-processor which
turns it into the following:
<pre class="example">
public class CageObject extends DObject
{
<b>// AUTO-GENERATED: FIELDS START
/** The field name of the <code>monkeys</code> field. */
public static final String MONKEYS = "monkeys";
/** The field name of the <code>owner</code> field. */
public static final String OWNER = "owner";
// AUTO-GENERATED: FIELDS END</b>
/** The number of monkeys in the cage. */
public int monkeys;
/** The name of the owner of this cage. */
public String owner;
<b>// AUTO-GENERATED: METHODS START
/**
* Requests that the <code>monkeys</code> field be set to the
* specified value. The local value will be updated immediately and an
* event will be propagated through the system to notify all listeners
* that the attribute did change. Proxied copies of this object (on
* clients) will apply the value change when they received the
* attribute changed notification.
*/
public void setMonkeys (int value)
{
int ovalue = this.monkeys;
requestAttributeChange(
EVEN_BASE, new Integer(value), new Integer(ovalue));
this.monkeys = value;
}
/**
* Requests that the <code>owner</code> field be set to the
* specified value. The local value will be updated immediately and an
* event will be propagated through the system to notify all listeners
* that the attribute did change. Proxied copies of this object (on
* clients) will apply the value change when they received the
* attribute changed notification.
*/
public void setOwner (String value)
{
String ovalue = this.owner;
requestAttributeChange(
ODD_BASE, value, ovalue);
this.owner = value;
}
// AUTO-GENERATED: METHODS END</b>
}</pre>
The contents of the methods are not too important, the main things to
note are that setter methods for the two attributes were generated and
constants were defined that will be used to identify which attribute
changed if we choose to inspect an event notifying us of such a
change. Note also that additional methods may be added to a
distributed object class as long as nothing is modified in the
<code>AUTO-GENERATED</code> section. As new fields are added and the
post-processing tool re-run, everything outside the auto-generated
section will be preserved.
<p> One may also notice that attribute change requests result in the
new value of the attribute being immediately written to the local copy
of the object. This is a convention that was decided upon after
repeatedly running into trouble when users of the system would set a
value in an object and immediately assume it held the new value rather
than realizing that an event would have to propagate back from the
server before the value was in fact updated. By setting the value
immediately, these problems are avoided and the opposite assumption is
almost never made. This is further justified by the fact that, in
general, attribute changes never originate on a client but instead
originate on the server after processing a request from the client
(via the below documented <a href="#invocation_services">invocation
services</a>) to do something application-specific that results in one
or more attribute changes taking place.
<p> See the section on <a href="#ant_tasks">Ant Tasks</a> for
information on how to configure and run this post-processor.
<h4>Creating an object</h4>
Generally, some entity on the server will choose to create a new
instance of a distributed object. Rather than simply instantiate the
object directly, one must create the object through the {@link
com.threerings.presents.dobj.DObjectManager}:
<pre class="example">
public class ServerEntity implements Subscriber {
public void init (DObjectManager omgr) {
omgr.createObject(CageObject.class, this);
}
// inherited from interface Subscriber
public void objectAvailable (DObject object) {
// yay! we created our object
_object = (CageObject)object;
}
// inherited from interface Subscriber
public void requestFailed (int oid, ObjectAccessException cause) {
// oh the humanity, we failed to create our object; in
// general this would only happen if we did something silly like
// passed in a DObject class that didn't extend DObject
}
protected CageObject _object;
}</pre>
You'll notice that we provide an instance of a <code>Subscriber</code>
when creating our object. This subscriber instance is in fact
subscribed to the newly created object in the same manner as is
described below for all additional subscribers to the object. It is
possible to instruct an object to automatically destroy itself when
all subscribers have unsubscribed. (See the not very terse {@link
com.threerings.presents.dobj.DObject}.setDestroyOnLastSubscriberRemoved()).
<h4>Subscribing to an object</h4>
<p> The client obtains a proxy of the object by a process called
subscription, which is accomplished via {@link
com.threerings.presents.dobj.DObjectManager}.subscribeToObject():
<pre class="example">
public class ObjectUser implements Subscriber {
public void init (Client client, int objectId) {
client.getDObjectManager().subscribeToObject(objectId, this);
}
// inherited from interface Subscriber
public void objectAvailable (DObject object) {
// yay! we got our object
_object = (CageObject)object;
}
// inherited from interface Subscriber
public void requestFailed (int oid, ObjectAccessException cause) {
// oh the humanity, we failed to subscribe
}
protected CageObject _object;
}</pre>
<p> Later a client would relinquish its subscription to the object
using a similar mechanism:
<pre class="example">
public class ObjectUser implements Subscriber {
// ...
public void shutdown (Client client) {
client.getDObjectManager().unsubscribeFromObject(
_object.getOid(), this);
_object = null;
}
// ...
}</pre>
However, this is a fine time to point out the dangers of working in an
asynchronous distributed environment. There is no guarantee that your
object subscription request will be completed before the client
decides to call shutdown() on its <code>ObjectUser</code>. Thus, in
the previous code, we could get a null pointer exception, and even
worse, we could remain subscribed to the object even though we didn't
want to be. To avoid these sorts of problems, the {@link
com.threerings.presents.util.SafeSubscriber} class is provided:
<pre class="example">
public class ObjectUser implements Subscriber {
public void init (Client client, int objectId) {
<b>_safesub = new SafeSubscriber(objectId, this);
_safesub.subcribe(client.getDObjectManager());</b>
}
// inherited from interface Subscriber
public void objectAvailable (DObject object) {
// yay! we got our object
_object = (CageObject)object;
}
// inherited from interface Subscriber
public void requestFailed (int oid, ObjectAccessException cause) {
// oh the humanity, we failed to subscribe
}
public void shutdown (Client client) {
<b>_safesub.unsubscribe(client.getDObjectManager());</b>
_object = null;
}
<b>protected SafeSubscriber _safesub;</b>
protected CageObject _object;
}</pre>
The safe subscriber will pass the object availability on to your
subscriber and when the time comes to unsubscribe, it will cope with
the case where the original subscription was not fully processed and
stick around long enough to ensure that once it is, the request to
unsubscribe is also dispatched. It will also cope with a request to
<code>unsubscribe()</code> even if the original subscription request
failed.
<h3><a name="event_listeners">Event Listeners</a></h3>
Once a client has subscribed to a distributed object, all events
pertaining to that object will be delivered to the client. Frequently,
it is useful to respond dynamically to changes in distributed object
values and this is accomplished using listeners. A client can register
any number of listeners on an object and when the object is finally
unsubscribed from and garbage collected, the listener registrations
all go away as well.
<p> The basic listener is the {@link
com.threerings.presents.dobj.AttributeChangeListener} which is
informed of all simple attribute changes (setting a primitive field to
a new value is called an attribute change). We return to our trusty
example:
<pre class="example">
public class ObjectUser
implements Subscriber, <b>AttributeChangeListener</b> {
// ...
public void init (Client client, int objectId) {
_safesub = new SafeSubscriber(_subscriber, objectId);
_safesub.subcribe(client.getDObjectManager());
}
// inherited from interface Subscriber
public void objectAvailable (DObject object) {
// yay! we got our object
_object = (CageObject)object;
<b>_object.addListener(this);</b>
}
// inherited from interface Subscriber
public void requestFailed (int oid, ObjectAccessException cause) {
// oh the humanity, we failed to subscribe
}
<b>// inherited from interface AttributeChangeListener
public void attributeChanged (AttributeChangedEvent event)
{
System.out.println("Wow! The " + event.getName() +
" field changed to " + event.getValue() + ".");
}</b>
public void shutdown (Client client) {
_safesub.unsubscribe(client.getDObjectManager());
<b>if (_object != null) {
// removing our listener not necessary as we are
// unsubscribing, but it's a good habit to develop as
// frequently listeners will come and go during the
// lifetime of an object subscription
_object.removeListener(this);
_object = null;
}</b>
}
protected SafeSubscriber _safesub;
protected CageObject _object;
}</pre>
The <code>attributeChanged()</code> method of our registered listener
will be called whenever an event is received as a result of one of the
setter methods being called on the <code>CageObject</code> by
<em>any</em> participant in the distributed system. The setter creates
an event which is sent to the server, the server dispatches the event
to all subscribers of the object and the Presents system dispatches
the event notification to all registered listeners when the event is
received on the client. Note that listeners are also used on the
server as entities on the server also frequently need to respond to
attribute changes. They are notified immediately after the server has
dispatched the event (over the network) to all subscribed clients.
<p> It is useful to note that listeners are notified of a changed
attribute <b>after</b> the change has been applied to the object. The
previous value of the attribute is available through the {@link
com.threerings.presents.dobj.AttributeChangedEvent#getOldValue}
method, though in spite of many years of experience using this system
in a variety of circumstances, we have rarely found that we cared to
know the previous value.
<h3><a name="distributed_collections">Distributed collections</a></h3>
One soon discovers that primitive object fields do not make for a very
useful information distribution mechanism and that more complex data
structures are necessary. Two collection types, sets and arrays, are
supported, and a mechanism is provided for allowing whole objects to
be passed around in toto as if they were a primitive field.
<p><b>Distributed Arrays</b><br>
Arrays of primitive types can be used in a distributed object and the
system will detect their use and provide a mechanism for updating the
entire array and an additional mechanism for updating a single element
at a time:
<pre class="example">
public class ChessObject extends DObject
{
// AUTO-GENERATED: FIELDS START
/** The field name of the <code>state</code> field. */
public static final String STATE = "state";
// AUTO-GENERATED: FIELDS END
/** Used to track our board state. */
public int[] state;
// AUTO-GENERATED: METHODS START
/**
* Requests that the <code>state</code> field be set to the
* specified value. The local value will be updated immediately and an
* event will be propagated through the system to notify all listeners
* that the attribute did change. Proxied copies of this object (on
* clients) will apply the value change when they received the
* attribute changed notification.
*/
public void setState (int[] value)
{
int[] ovalue = this.state;
requestAttributeChange(
STATE, value, ovalue);
this.state = (value == null) ? null : (int[])value.clone();
}
/**
* Requests that the <code>index</code>th element of
* <code>state</code> field be set to the specified value.
* The local value will be updated immediately and an event will be
* propagated through the system to notify all listeners that the
* attribute did change. Proxied copies of this object (on clients)
* will apply the value change when they received the attribute
* changed notification.
*/
public void setStateAt (int value, int index)
{
int ovalue = this.state[index];
requestElementUpdate(
STATE, index, new Integer(value), new Integer(ovalue));
this.state[index] = value;
}
// AUTO-GENERATED: METHODS END
}</pre>
To correspond with what is called an "element update" (the
modification of a single element in an array), there is the {@link
com.threerings.presents.dobj.ElementUpdateListener}. When an element
is updated, listeners implementing that interface will be notified.
Remember that if the whole array is changed using
<code>setState()</code>, the normal {@link
com.threerings.presents.dobj.AttributeChangeListener} is the interface
one uses to hear about it.
<p> <em>Note</em> that distributed arrays are <em>not</em>
automatically resized. If a request is made to update the element at
index 9 of an array, the array must be of at least size 10 or an array
index out of bounds exception will be thrown (as should be evident
from inspecting the code above). For more dynamic collections of
objects, see the documentation below about distributed sets.
<p> This mechanism is not actually limited to arrays of primitive
types. It also works for arrays of objects that implement the {@link
com.threerings.io.Streamable} interface which is documented next.
<p><b>Streamable and its good friend SimpleStreamableObject</b><br>
The {@link com.threerings.io.Streamable} interface is used to mark
objects that can be sent over the network by using them in distributed
object fields by using arrays of such objects as a field. This
interface functions in much the same way that {@link
java.io.Serializable} does in that it simply marks the class and an
underlying mechanism uses reflection to actually marshall and
unmarshall the object on the network. In fact, all
non-<code>transient</code> fields of a streamable object are included
during the marhsalling process. Here's an example:
<pre class="example">
public class Player implements Streamable
{
/** This player's name. */
public String name;
/** This player's rating. */
public int rating;
}
public class ChessObject extends DObject
{
/** A record for each player in the game. */
public Player[] players;
}</pre>
The generated methods are ommitted for the sake of brevity, but as you
would expect, both a <code>setPlayers(Player[] value)</code> and a
<code>setPlayersAt(Player value, int index)</code> method will be
generated and do just what you expect.
<p> It should be pointed out that streamable objects sent over the
network are sent in their entirety. No mechanism is provided for
updating just a single field in a streamable instance both because
that would increase the complexity of the system tremendously and
because it is generally not very useful. If conservation of bandwidth
is of extreme importance, special {@link
com.threerings.presents.dobj.DEvent} derived classes can be created to
transmit precisely what is desired and nothing more. Doing so is
beyond the scope of this introduction, but will hopefully be covered
in an additional tutorial.
<p> The {@link com.threerings.io.SimpleStreamableObject} class is a
convenient way to create a simple record like the <code>Player</code>
record above that implements <code>Streamable</code> and provides a
default implementation of <code>toString()</code> that uses reflection
to print out the actual values of the fields in the object (a boon
when logging and debugging).
<p><b>Distributed Sets</b><br>
In developing a distributed system, one frequently encounters
situations where one wants distributed collection of objects where
order is generally not important but the ability to fluidly add and
remove elements is. For such occasions we provide the distributed set
or {@link com.threerings.presents.dobj.DSet}.
<p> A <code>DSet</code> contains entries (called entries rather than
elements to avoid confusion with array "element updating") which must
implement the {@link com.threerings.presents.dobj.DSet.Entry}
interface. This automatically makes them {@link
com.threerings.io.Streamable} and requires that they provide a {@link
java.lang.Comparable} key which is used to distinguish them from other
entries in the set (and look them up via an efficient binary search).
<p> When using a <code>DSet</code> one is provided with three new
operations: <code>addToFoo()</code>, <code>updateFoo()</code> and
<code>removeFromFoo()</code>. Once again an example is in order:
<pre class="example">
public class Monkey implements DSet.Entry
{
/** The monkey's name. */
public String name;
/** The monkey's age. */
public int age;
// documentation inherited from interface DSet.Entry
public Comparable getKey ()
{
return name;
}
}
public class CageObject extends DObject
{
// AUTO-GENERATED: FIELDS START
/** The field name of the <code>monkeys</code> field. */
public static final String MONKEYS = "monkeys";
// AUTO-GENERATED: FIELDS END
/** A collection of monkeys. */
public DSet monkeys;
// AUTO-GENERATED: METHODS START
/**
* Requests that the specified entry be added to the
* <code>monkeys</code> set.
*/
public void addToMonkeys (DSet.Entry elem)
{
requestEntryAdd(MONKEYS, monkeys, elem);
}
/**
* Requests that the entry matching the supplied key be removed from
* the <code>monkeys</code> set.
*/
public void removeFromMonkeys (Comparable key)
{
requestEntryRemove(MONKEYS, monkeys, key);
}
/**
* Requests that the specified entry be updated in the
* <code>monkeys</code> set.
*/
public void updateMonkeys (DSet.Entry elem)
{
requestEntryUpdate(MONKEYS, monkeys, elem);
}
/**
* Requests that the <code>monkeys</code> field be set to the
* specified value.
*/
public void setMonkeys (DSet value)
{
requestAttributeChange(MONKEYS, value, this.monkeys);
this.monkeys = (value == null) ? null : (DSet)value.clone();
}
// AUTO-GENERATED: METHODS END
}</pre>
It is possible to set the entire set (which is necessary to establish
its original value even if one decides to set it to the empty set),
but more commonly one will simply add entries to the set, update those
entries and remove them using the provided methods.
<p> In conjunction with the <code>DSet</code> there exists the {@link
com.threerings.presents.dobj.SetListener} which is notified when
changes are made to a distributed set. This functions in the same was
as the previously documented listeners, so I will refrain from boring
you with yet more sample code.
<h3><a name="invocation_services">Invocation Services</a></h3>
TBD
<h3><a name="ant_tasks">Ant Tasks</a></h3>
TBD
</body>
</html>