Allow downstream and upstream messages to be compounded

A client can collect a bunch of messages into a CompoundUpstreamMessage, which are unpacked and
dispatched in order by the server. The server collects any messages sent while processing that
compound message into a CompoundDownstreamMessage, which is unpacked and dispatched by the client.

This allows the client to indicate that it's going to send enough messages that it'd blow out the
throttle, but that it's doing so intentionally.



git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@6659 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
Charlie Groves
2011-06-16 03:37:08 +00:00
parent 604e11e93e
commit 52023608c8
10 changed files with 333 additions and 43 deletions
@@ -20,7 +20,6 @@
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.client { package com.threerings.presents.client {
import flash.events.EventDispatcher; import flash.events.EventDispatcher;
import flash.events.TimerEvent; import flash.events.TimerEvent;
@@ -280,6 +279,21 @@ public class Client extends EventDispatcher
return true; return true;
} }
/**
* Runs the given function while collecting any generated messages in a CompoundEvent, which
* will be sent when the function returns.
*/
public function inCompoundMessage (run :Function) :void
{
_comm.startCompoundMessage();
try {
run();
} finally {
_comm.finishCompoundMessage();
}
}
/** /**
* Create the client property object to pass to {@link ObjectInputStream}. Subclasses may * Create the client property object to pass to {@link ObjectInputStream}. Subclasses may
* add references useful during e.g. deserialization. * add references useful during e.g. deserialization.
@@ -20,6 +20,7 @@
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.client { package com.threerings.presents.client {
import com.threerings.presents.net.CompoundDownstreamMessage;
import flash.events.Event; import flash.events.Event;
import flash.events.TimerEvent; import flash.events.TimerEvent;
@@ -175,6 +176,10 @@ public class ClientDObjectMgr
} else if (msg is UpdateThrottleMessage) { } else if (msg is UpdateThrottleMessage) {
_client.setOutgoingMessageThrottle((msg as UpdateThrottleMessage).messagesPerSec); _client.setOutgoingMessageThrottle((msg as UpdateThrottleMessage).messagesPerSec);
} else if (msg is CompoundDownstreamMessage) {
for each (var submsg :DownstreamMessage in CompoundDownstreamMessage(msg).msgs) {
processMessage(submsg);
}
} }
} }
@@ -20,6 +20,7 @@
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.client { package com.threerings.presents.client {
import com.threerings.presents.net.CompoundUpstreamMessage;
import flash.events.Event; import flash.events.Event;
import flash.events.IOErrorEvent; import flash.events.IOErrorEvent;
@@ -75,6 +76,10 @@ public class Communicator
public function postMessage (msg :UpstreamMessage) :void public function postMessage (msg :UpstreamMessage) :void
{ {
if (_compound != null) {
_compound.msgs.push(msg);
return;
}
_outq.push(msg); _outq.push(msg);
if (_writer != null) { if (_writer != null) {
sendPendingMessages(null); sendPendingMessages(null);
@@ -109,6 +114,23 @@ public class Communicator
return false; return false;
} }
public function startCompoundMessage () :void
{
if (_compound == null) {
_compound = new CompoundUpstreamMessage();
}
_compoundDepth++;
}
public function finishCompoundMessage () :void
{
if (--_compoundDepth == 0) {
var toSend :CompoundUpstreamMessage = _compound;
_compound = null;
postMessage(toSend);
}
}
/** /**
* Attempts to logon on using the port at the specified index. * Attempts to logon on using the port at the specified index.
*/ */
@@ -383,6 +405,13 @@ public class Communicator
protected var _writer :Timer; protected var _writer :Timer;
protected var _notedThrottle :Boolean = false; protected var _notedThrottle :Boolean = false;
protected var _compound :CompoundUpstreamMessage;
/**
* The count of startCompoundMessage calls that have occurred without a finishCompoundMessage
*/
protected var _compoundDepth :int;
protected const log :Log = Log.getLog(this); protected const log :Log = Log.getLog(this);
/** The current port we'll try to connect to. */ /** The current port we'll try to connect to. */
@@ -0,0 +1,38 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2011 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.net {
import com.threerings.io.streamers.ArrayStreamer;
import com.threerings.io.TypedArray;
import com.threerings.io.ObjectInputStream;
public class CompoundDownstreamMessage extends DownstreamMessage
{
public var msgs :TypedArray = TypedArray.create(DownstreamMessage);
override public function readObject (ins :ObjectInputStream) :void
{
super.readObject(ins);
msgs = ins.readField(ArrayStreamer.INSTANCE);
}
}
}
@@ -0,0 +1,41 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2011 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.net {
import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.io.TypedArray;
import com.threerings.io.streamers.ArrayStreamer;
import com.threerings.presents.net.UpstreamMessage;
public class CompoundUpstreamMessage extends UpstreamMessage
{
public var msgs :TypedArray = TypedArray.create(UpstreamMessage);
override public function writeObject (out :ObjectOutputStream) :void
{
super.writeObject(out);
out.writeField(msgs, ArrayStreamer.INSTANCE);
}
}
}
@@ -31,11 +31,6 @@ import com.threerings.io.Streamable;
public class PongResponse extends DownstreamMessage public class PongResponse extends DownstreamMessage
{ {
public function PongResponse ()
{
super();
}
public function getPackStamp () :Long public function getPackStamp () :Long
{ {
return _packStamp; return _packStamp;
@@ -47,6 +47,8 @@ import com.threerings.presents.dobj.ObjectDestroyedEvent;
import com.threerings.presents.dobj.Subscriber; import com.threerings.presents.dobj.Subscriber;
import com.threerings.presents.net.BootstrapData; import com.threerings.presents.net.BootstrapData;
import com.threerings.presents.net.BootstrapNotification; import com.threerings.presents.net.BootstrapNotification;
import com.threerings.presents.net.CompoundDownstreamMessage;
import com.threerings.presents.net.DownstreamMessage;
import com.threerings.presents.net.EventNotification; import com.threerings.presents.net.EventNotification;
import com.threerings.presents.net.FailureResponse; import com.threerings.presents.net.FailureResponse;
import com.threerings.presents.net.ForwardEventRequest; import com.threerings.presents.net.ForwardEventRequest;
@@ -184,44 +186,52 @@ public class ClientDObjectMgr
// process the next event on our queue // process the next event on our queue
Object obj; Object obj;
if ((obj = _actions.getNonBlocking()) != null) { if ((obj = _actions.getNonBlocking()) != null) {
// do the proper thing depending on the object dispatchAction(obj);
if (obj instanceof EventNotification) { }
dispatchEvent(((EventNotification)obj).getEvent()); }
} else if (obj instanceof BootstrapNotification) { protected void dispatchAction (Object obj)
BootstrapData data = ((BootstrapNotification)obj).getData(); {
_client.gotBootstrap(data, this); if (obj instanceof EventNotification) {
dispatchEvent(((EventNotification)obj).getEvent());
} else if (obj instanceof ObjectResponse<?>) { } else if (obj instanceof BootstrapNotification) {
registerObjectAndNotify((ObjectResponse<?>)obj); BootstrapData data = ((BootstrapNotification)obj).getData();
_client.gotBootstrap(data, this);
} else if (obj instanceof UnsubscribeResponse) { } else if (obj instanceof ObjectResponse<?>) {
int oid = ((UnsubscribeResponse)obj).getOid(); registerObjectAndNotify((ObjectResponse<?>)obj);
if (_dead.remove(oid) == null) {
log.warning("Received unsub ACK from unknown object", "oid", oid);
}
} else if (obj instanceof FailureResponse) { } else if (obj instanceof UnsubscribeResponse) {
notifyFailure(((FailureResponse)obj).getOid(), ((FailureResponse)obj).getMessage()); int oid = ((UnsubscribeResponse)obj).getOid();
if (_dead.remove(oid) == null) {
} else if (obj instanceof PongResponse) { log.warning("Received unsub ACK from unknown object", "oid", oid);
_client.gotPong((PongResponse)obj);
} else if (obj instanceof UpdateThrottleMessage) {
UpdateThrottleMessage upmsg = (UpdateThrottleMessage)obj;
_client.setOutgoingMessageThrottle(upmsg.messagesPerSec);
} else if (obj instanceof ObjectAction<?>) {
ObjectAction<?> act = (ObjectAction<?>)obj;
if (act.subscribe) {
doSubscribe(act);
} else {
doUnsubscribe(act.oid, act.target);
}
} else {
log.warning("Unknown action", "action", obj);
} }
} else if (obj instanceof FailureResponse) {
notifyFailure(((FailureResponse)obj).getOid(), ((FailureResponse)obj).getMessage());
} else if (obj instanceof PongResponse) {
_client.gotPong((PongResponse)obj);
} else if (obj instanceof UpdateThrottleMessage) {
UpdateThrottleMessage upmsg = (UpdateThrottleMessage)obj;
_client.setOutgoingMessageThrottle(upmsg.messagesPerSec);
} else if (obj instanceof ObjectAction<?>) {
ObjectAction<?> act = (ObjectAction<?>)obj;
if (act.subscribe) {
doSubscribe(act);
} else {
doUnsubscribe(act.oid, act.target);
}
} else if (obj instanceof CompoundDownstreamMessage) {
for (DownstreamMessage submsg : ((CompoundDownstreamMessage)obj).msgs) {
dispatchAction(submsg);
}
} else {
log.warning("Unknown action", "action", obj);
} }
} }
@@ -0,0 +1,40 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2011 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.net;
import java.util.List;
import com.google.common.collect.Lists;
/**
* Groups messages to be dispatched without triggering the message throttle.
*/
public class CompoundDownstreamMessage extends DownstreamMessage
{
public List<DownstreamMessage> msgs = Lists.newArrayList();
@Override
public String toString ()
{
return "[type=COMPOUND, msgid=" + messageId + "]";
}
}
@@ -0,0 +1,42 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2011 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.net;
import java.util.List;
import com.google.common.collect.Lists;
/**
* Groups messages to be dispatched without triggering the message throttle. Any messages sent to
* a client while processing an upstream message will be sent back en masse in a
* CompoundDownstreamMessage.
*/
public class CompoundUpstreamMessage extends UpstreamMessage
{
public List<UpstreamMessage> msgs = Lists.newArrayList();
@Override
public String toString ()
{
return "[type=COMPOUND, msgid=" + messageId + "]";
}
}
@@ -54,6 +54,8 @@ import com.threerings.presents.dobj.ProxySubscriber;
import com.threerings.presents.net.AuthRequest; import com.threerings.presents.net.AuthRequest;
import com.threerings.presents.net.BootstrapData; import com.threerings.presents.net.BootstrapData;
import com.threerings.presents.net.BootstrapNotification; import com.threerings.presents.net.BootstrapNotification;
import com.threerings.presents.net.CompoundDownstreamMessage;
import com.threerings.presents.net.CompoundUpstreamMessage;
import com.threerings.presents.net.Credentials; import com.threerings.presents.net.Credentials;
import com.threerings.presents.net.DownstreamMessage; import com.threerings.presents.net.DownstreamMessage;
import com.threerings.presents.net.EventNotification; import com.threerings.presents.net.EventNotification;
@@ -70,6 +72,7 @@ import com.threerings.presents.net.TransmitDatagramsRequest;
import com.threerings.presents.net.UnsubscribeRequest; import com.threerings.presents.net.UnsubscribeRequest;
import com.threerings.presents.net.UnsubscribeResponse; import com.threerings.presents.net.UnsubscribeResponse;
import com.threerings.presents.net.UpdateThrottleMessage; import com.threerings.presents.net.UpdateThrottleMessage;
import com.threerings.presents.net.UpstreamMessage;
import com.threerings.presents.server.net.PresentsConnection; import com.threerings.presents.server.net.PresentsConnection;
import com.threerings.nio.conman.Connection; import com.threerings.nio.conman.Connection;
@@ -433,8 +436,6 @@ public class PresentsSession
// from interface Connection.MessageHandler // from interface Connection.MessageHandler
public void handleMessage (Message message) public void handleMessage (Message message)
{ {
_messagesIn++; // count 'em up!
// if the client has been getting crazy with the cheeze whiz, stick a fork in them; the // if the client has been getting crazy with the cheeze whiz, stick a fork in them; the
// first time through we end our session, subsequently _throttle is null and we just drop // first time through we end our session, subsequently _throttle is null and we just drop
// any messages that come in until we've fully shutdown // any messages that come in until we've fully shutdown
@@ -447,6 +448,16 @@ public class PresentsSession
handleThrottleExceeded(); handleThrottleExceeded();
} }
dispatchMessage(message);
}
/**
* Processes a message without throttling.
*/
protected void dispatchMessage (Message message)
{
_messagesIn++; // count 'em up!
// we dispatch to a message dispatcher that is specialized for the particular class of // we dispatch to a message dispatcher that is specialized for the particular class of
// message that we received // message that we received
MessageDispatcher disp = _disps.get(message.getClass()); MessageDispatcher disp = _disps.get(message.getClass());
@@ -464,8 +475,7 @@ public class PresentsSession
* applied to this client. The old client object will not yet have been destroyed, so any final * applied to this client. The old client object will not yet have been destroyed, so any final
* events can be sent along prior to the new object being put into effect. * events can be sent along prior to the new object being put into effect.
*/ */
protected void clientObjectWillChange ( protected void clientObjectWillChange (ClientObject oldClobj, ClientObject newClobj)
ClientObject oldClobj, ClientObject newClobj)
{ {
} }
@@ -901,6 +911,31 @@ public class PresentsSession
}); });
} }
/**
* Collects downstream messages in a compound message until finishCompoundMessage is called.
*/
protected void startCompoundMessage ()
{
if (_compound == null) {
_compound = new CompoundDownstreamMessage();
}
_compoundDepth++;
}
/**
* Sends the compound message created in startCompoundMessage.
*/
protected void finishCompoundMessage ()
{
if (--_compoundDepth == 0) {
CompoundDownstreamMessage downstream = _compound;
_compound = null;
if (!downstream.msgs.isEmpty()) {
postMessage(downstream, null);
}
}
}
/** Queues a message for delivery to the client. */ /** Queues a message for delivery to the client. */
protected boolean postMessage (DownstreamMessage msg, PresentsConnection expect) protected boolean postMessage (DownstreamMessage msg, PresentsConnection expect)
{ {
@@ -914,6 +949,11 @@ public class PresentsSession
return false; return false;
} }
if (_compound != null) {
_compound.msgs.add(msg);
return true;
}
// make sure we have a connection at all // make sure we have a connection at all
if (conn != null) { if (conn != null) {
conn.postMessage(msg); conn.postMessage(msg);
@@ -1124,6 +1164,29 @@ public class PresentsSession
} }
} }
/**
* Processes compound messages.
*/
protected static class CompoundDispatcher implements MessageDispatcher
{
public void dispatch (final PresentsSession client, Message msg)
{
// Compound downstream messages sent while dispatching the upstream compound message
client._omgr.postRunnable(new Runnable() {
public void run () {
client.startCompoundMessage();
}});
for (UpstreamMessage submsg : ((CompoundUpstreamMessage)msg).msgs) {
client.dispatchMessage(submsg);
}
// Send any messages produced en masse now that we've finished dispatching
client._omgr.postRunnable(new Runnable() {
public void run () {
client.finishCompoundMessage();
}});
}
}
/** /**
* Processes unsubscribe requests. * Processes unsubscribe requests.
*/ */
@@ -1236,6 +1299,18 @@ public class PresentsSession
protected ClientObject _clobj; protected ClientObject _clobj;
protected IntMap<ClientProxy> _subscrips = IntMaps.newHashIntMap(); protected IntMap<ClientProxy> _subscrips = IntMaps.newHashIntMap();
/**
* Message in which we're currently compounding messages to send, or null if we're sending them
* straight on.
*/
protected CompoundDownstreamMessage _compound;
/**
* The count of startCompoundMessage calls that have occurred without a finishCompoundMessage.
* _compound won't be set until this reaches 0 again.
*/
protected int _compoundDepth;
/** The Oids of objects that have been destroyed while we were subscribed. */ /** The Oids of objects that have been destroyed while we were subscribed. */
protected HashSet<Integer> _destroyedSubs = Sets.newHashSet(); protected HashSet<Integer> _destroyedSubs = Sets.newHashSet();
protected ClassLoader _loader; protected ClassLoader _loader;
@@ -1282,5 +1357,6 @@ public class PresentsSession
_disps.put(TransmitDatagramsRequest.class, new TransmitDatagramsDispatcher()); _disps.put(TransmitDatagramsRequest.class, new TransmitDatagramsDispatcher());
_disps.put(ThrottleUpdatedMessage.class, new ThrottleUpdatedDispatcher()); _disps.put(ThrottleUpdatedMessage.class, new ThrottleUpdatedDispatcher());
_disps.put(LogoffRequest.class, new LogoffDispatcher()); _disps.put(LogoffRequest.class, new LogoffDispatcher());
_disps.put(CompoundUpstreamMessage.class, new CompoundDispatcher());
} }
} }