Multiverse Messaging System

From Multiverse

Jump to: navigation, search

Contents

Overview

The plug-in servers use messages to communicate with each other and with world managers. The Multiverse messaging system follows a publish/subscribe model. That is, there are message publishers, that produce messages, and subscribers that consume certain messages. The model abstracts server location so a server plug-in does not need to have information about message origin or destination. This scheme enables plug-ins to run on any physical server, since the messaging system handles routing messages to the correct server.

Note: This documenation uses "publisher" and "producer" interchangably. They mean the same thing.

The publish/subscribe system enables objects to move independently of notifying the producer of the messages. For example, the Proxy Server is a plugin that relays messages to the client, accepts client commands, and transmits object properties and movement. It doesn't need to know how many world managers are active or even which world managers it is communicating with, because the Proxy Server is subscribed to certain messages.

For a general introduction to publish/subscribe systems, see the Wikipedia article on publish/subscribe.

There are two main differences between the Multiverse messaging system and other messaging systems:

  • Messages are not routed through a central messaging server. This avoids bottlenecks. The Multiverse Domain Server acts as a registry for publishers and subscribers, but does not route messages or subscriptions.
  • Multiverse uses a more powerful filtering system than other messaging systems. A filter defines a subscription--determines which messages the plugin gets.
  • Filters are arbitrary comparators on any message content. This enables filtering based for example on geography. Conventional messaging systems have limited comparators.
  • Built-in support for Remote Procedure Call (RPC) style programming.
  • Very efficient network transport.

The Multiverse message server is implemented in the multiverse.msgsys package.

Features

Important features of the messaging system:

  • Messages are strictly ordered, and every message has a unique identifier.
  • Messages are received in order they are sent from a given producer.
  • When a subscription is created, it does not return until all publishers have registered the subscription. Thus, no messages will be dropped.
  • When a server plugin starts, it waits until it connects to all other plugins and receives all existing subscriptions.
  • Message RPC requests must have just one subscriber.

Message domain server

The Multiverse message domain server keeps a registry of all messaging participants (message agents). The multiverse.msgsys.DomainServer class implements the message domain server. No subscription or message traffic passes through the domain server.

Each agent connects to the domain server to register itself and learn the network locations of other agents. Methods on multiverse.msgsys.MessageAgent enable connecting to the domain server. A message domain is a set of communicating servers (multiverse.msgsys.MessageAgent instances) that use the same domain server.

multiverse.server.engine.Engine automatically connects to the domain server defined by properties multiverse.msgsvr_hostname and multiverse.msgsvr_port (default is 20374).

Message types

Every message sent between server plugins has a message type. To improve performance, message types are instances of the multiverse.msgsys.MessageType class. This class does not have a public constructor. Instead, create an instance as follows:

MessageType msgType = MessageType.intern("string");

Where string is the string identifying the message type. This technique is called string interning; see String intern pool (Wikipedia) for more information. Because message types are interned, there is only one copy of any message type in any server process, and comparisons between message types are more efficient.

For example:

 public static MessageType MSG_TYPE_FOO = MessageType.intern("wrldMgr_foo");

Then set the message type as follows

msg = Message();
msg.setMsgType(MSG_TYPE_FOO);

All MessageTypes should be added to a Message Catalog (see below).

Message catalogs

A message catalog increases efficiency of the messaging system by associating message types with numbers. The number reduces message size when transmitting messages between plugins. A catalog declares ownership over a range of message type numbers. You may define several message catalogs for a single server installation, but the message type range of one catalog cannot overlap with that of another. All script files that define or add to message catalogs are read at startup by every plugin in the Multiverse server. The startup script multiverse.sh by default looks in two places for message catalog files: /config/common/mvmessages.py, and config/worldname/worldmessages.py.

Multiverse message catalog

The script mv-home/config/common/mvmessages.py defines the default message catalog for the Multiverse servers. This script defines the catalog mvMessageCatalog:

mvMessageCatalog = MessageCatalog.addMsgCatalog("mvMessageCatalog", 1, 500)

The catalog mvMessageCatalog starts with type number one (1), and can accommodate up to 500 message types.

Later lines in mvmessages.py add message types to the mvMessageCatalog, for example:

MessageCatalog.addMsgTypeTranslation(mvMessageCatalog, WorldManagerClient.MSG_TYPE_FOG)

This line adds the message type WorldManagerClient.MSG_TYPE_FOG to the mvMessageCatalog, assigning it the next available message type number in the catalog.

For information on the built-in message catalog, see Multiverse Message Catalog.

Adding world-specific message types

In addition to mvmessages.py, at startup every server plug-in reads config/worldname/worldmessages.py, if it exists. Add world-specific messages to the message catalog that you define in that file. For an example, see multiverse/config/sampleworld/worldmessages.py. That file creates a catalog instance called worldMessageCatalog. To add world-specific messages to that catalog, add lines of the form:

MessageCatalog.addMsgTypeTranslation(worldMessageCatalog, MessageType.intern("message_type"))

You can replace worldMessageCatalog as the name of the catalog you've created.

Adding a custom catalog

If for some reason the worldMessageCatalog isn't sufficient, you can create message catalogs from scratch by calling MessageCatalog.addMsgCatalog() as follows:

custom_catalog = MessageCatalog.addMsgCatalog("name", startMsgNum, numberOfMsgs)

Where name is the name of the catalog, startMsgNum is the number of the first message, and numberOfMsgs is the total number of messages.

Then add messages as follows:

MessageCatalog.addMsgTypeTranslation(custom_catalog, message_type)

Where custom_catalog is the name of the catalog you've created, and message_type is an object of multiverse.msgsys.MessageType, probably created by a call to MessageType.intern(string).

Note that since all plugins must agree on MessageType numbers, you must ensure that your custom catalog definition is read by all plugins. One easy way to do this is to put the catalog definition in /config/worldname/worldmessages.py along with the predefined world message catalog.

Messages

A message is the communication unit in the Multiverse message system. A message has a MessageType, a producer-unique message ID, and may optionally have additional properties defined by implementing a sub-class of multiverse.msgsys.Message.

Message classes

There are two general purpose message classes:

  • SubjectMessage - a message about an object (identified by an OID). The message should be received by all parties interested in the subject object. For example, when an avatar dances, a SubjectMessage is published so other objects/players know they're dancing.
  • TargetMessage - a message targeted at a specific object, about an object (identified by an OID). A target message has both a target OID and subject OID. The message only applies to the target OID. For example, when a player displays his quest log, a TargetMessage is published containing the player's quest status. If this were a SubjectMessage then all players would receive quest status about every other player.

Typically, there is a one-to-one correspondence between MessageType and Message sub-classes. However, this is not enforced. One MessageType can be used with multiple Message sub-classes. And a single Message sub-class can be published with different MessageTypes.

Extension messages

Use the ExtensionMessage class to send a message to the client that encapsulates a set of string key/value pairs. ExtensionMessage is a subclass of SubjectMessage, which means that it has one OID that the message is "about".

Use TargetedExtensionMessage to send a message to only the client that matches the specified OID. TargetedExtensionMessage extends TargetMessage, so it has two OIDs: a subject OID that specifies the subject of the message, and a target OID that specifies to whom the message is sent. All clients (players) that can perceive the subject recieve ExtensionMessage, while only the client (player) that matches the target OID receive a TargetedExtensionMessage.

For more information about extension messages, see Using Extension Messages.

Messages that support property maps

Certain message classes support property maps that enable you to transmit a complex structure as a single property value. A property map can contain values types such as List, Map, and Set. These values can in turn contain other complex types, (including other Lists, Maps and Sets). This enables deeply nested values.

The following messages classes support property maps:

All these message classes have a setProperty() method to set property map values and a getProperty() method to get values of property maps. The setProperty() method takes two parameters: a String to specify the key and an object (that implements java.io.Serializable) to specify the value. The getProperty() method takes a String parameter and returns the corresponding java.io.Serializable value.

Thus, property map keys must be strings.

Values can be a String or any of the following primitive types: long, int, boolean, float, double; or null. Values can also be any of the following complex types:

  • Vector of three ints, used to represent positions.
  • Vector of three floats, used for directions and other purposes.
  • Quaternion, used to represent orientations
  • Color, containing a representation of rgba; each color is a byte whose value is 0-255
  • List of elements, each of which can be any property map value type.
  • Set of elements, each of which can be any property map value type.
  • Map of strings to elements, each of which can be any property map value type. Essentially, maps are property maps in their own right.

Marshalling

Marshalling is the act of converting a message to and from a byte stream. The Server Message Marshalling feature automatically converts Message instances into a byte stream. The technique used is extremely efficient and easy to use. If required, classes can over-ride the default marshaller, for example, to support "interned" objects.

Subscriptions

A subscription is a combination of a filter and a message callback. A filter matches properties of a message such as message type, object id, or location. Once created a subscription is "listening" for matching messages. A matching message is delivered to the message callback which does something with the message.

Create subscriptions with the MessageAgent.createSubscription() method.

Create new filters by sub-classing multiverse.msgsys.Filter. Several general purpose filters are available, including:

  • multiverse.msgsys.MessageTypeFilter - matches any message matching one of the filter's message types.
  • multiverse.msgsys.SubjectFilter - matches messages by message type about a subject OID. Specifically, SubjectMessage instances (and sub-classes) with a matching subject OID, and TargetMessage instances (and sub-classes) whose target matches the filter's subject.
  • multiverse.msgsys.TargetFilter - matches messages by message type, intended for a specific object about another object. Specifically, TargetMessage instances (and sub-classes) with matching target and subject OIDs, and SubjectMessage instances (and sub-classes) whose subject matches the filter's subject.

Need some examples in this section. Also need a discussion some where on the difference between using the built-in EnginePlugin message callback and writing your own MessageCallback (mostly about threading).

Custom filters

If one of the existing filters does not suit your needs, create a custom message filter using any desired criteria to match messages. You must make several decisions before writing a custom filter:

  • Which class to sub-class:
    • Filter - The filter base-class, provides no default implementation
    • MessageTypeFilter - The most common filter, filters based on a set of MessageType
    • SubjectFilter - Sub-class of MessageTypeFilter, filters SubjectMessage and TargetMessage based on subject OID
    • TargetFilter - Sub-class of MessageTypeFilter, filters TargetMessage and SubjectMessage based on target and subject OID
  • Whether to support filter updates:
    • Do your subscriptions need to be changed after creation?
  • Whether to optimize filter matching:
    • Will there be 100+ subscriptions using this filter?

Filter sub-class

Consider that MessageTypeFilter and its sub-classes benefit from optimized message matching (see DefaultFilterTable). If you sub-class Filter, then performance could suffer for your subscriptions, but only if you create large numbers of subscriptions using the filter. See Filter Tables for more information on subscription and filter performance.

Filter is an abstract class; sub-classes must implement the following methods:

  • boolean matchMessageType(Collection<MessageType> messageTypes) - must return true if the filter matches one of the messageTypes. Currently, this is only called with a list of advertisements when determining to which plugin to forward a subscription.
  • boolean matchRemaining(Message message) - must return true if the filter matches criteria other than the message type. This will only be called after the filter has been determined to match the message type, so there's no need to check the message type matching again.
  • Collection<MessageType> getMessageTypes() - returns the set of message types matched by this filter. DefaultFilterTable uses this method to optimize subscription matching.

If you sub-class MessageTypeFilter, then you only need to implement:

 boolean matchRemaining(Message message)

because MessageTypeFilter provides implementations of matchMessageType() and getMessageTypes().

Your matchRemaining() method can use any criteria to match the message to the filter. If you expect high-volumes of messages through this filter (or large numbers of filters), then you should make this method as efficient as possible. If you expect large number of filters, then consider writing a FilterTable to optimize the matching.

Filters tend to be Message class specific; the filter only applies to messages of a particular class. For example, a ChatFilter only makes sense for ChatMessages. The filter should contain the MessageType(s) used by ChatMessage, but there's no guarantee that ChatMessage is the only message to use those MessageType(s). Within your matchRemaining() it's good defensive practice to use Java instanceof to filter for the applicable Java classes.

Supporting filter updates

You can make your filter dynamic by supporting filter updates. The subscriber can update a subscription using MessageAgent.applyFilterUpdate(). The subscriber creates a multiverse.msgsys.FilterUpdate object describing the filter changes. The update is sent to all agents that received the subscription. The remote agents call Filter.applyFilterUpdate() on their copy of the subscription filter. Your implementation should apply the FilterUpdate instructions however makes sense for your filter.

The FilterUpdate defines operations OP_ADD, OP_REMOVE, and OP_SET applicable to certain field identifiers (integers). Your filter class should provide constants for the supported fields.

Optimizing filter matching

There are two approaches to optimizing filter matching.

  • Write a filter that can aggregate multiple interests into a single or few subscriptions.
  • Write a filter class specific FilterTable that collects multiple filters for optimized matching.

Filter tables are covered below.

PerceptionFilter is an example of the first approach. Plug-ins can use a single multiverse.msgsys.PerceptionFilter to subscribe to subject and target messages for multiple object OIDs. Since there is only one subscription from the plugin, there's no need for further optimization. This approach often requires cooperation with the plugin implementation. For example, the proxy uses FilterUpdates to keep its PerceptionFilter target set in-sync with the logged in players.

For example, imagine a chat plug-in subscribing to a set of chat channels. The plug-in could make a subscription for each chat channel. Message matching performance will scale linearly with the number of channels; not good. Instead, you could implement a ChatFilter that filters for multiple channels. If the filter maintains the channels in a HashSet then matching is likely O(1). You can support FilterUpdates to allow the filter's channel set to change.

Remote procedure calls

Remote procedure calls (RPC) enable applications to send a message and wait for a response. Any message can be used as an RPC request. RPC messages only match subscriptions with the MessageAgent.RESPONDER flag. The RPC request must match one and only one subscription. The matching subscription callback must send a multiverse.msgsys.ResponseMessage using MessageAgent.sendResponse(). For example, to send an RPC request:

 ResponseMessage response;
 response = Engine.getAgent().sendRPC(request-message);

RPC responders have a subscription with the RESPONDER flag:

   Engine.getAgent().createSubscription(filter,new MyCallback(), MessageAgent.RESPONDER);

and to send a response:

   class MyCallback implements MessageCallback {
       public void handleMessage(Message message, int flags) {
           ResponseMessage response = new ResponseMessage(message);
           Engine.getAgent().sendResponse(response);
       }
   }

If you forget to use the MessageAgent.RESPONDER flag, the publisher will get a runtime exception when calling sendRPC().

There are several convenience methods for RPC that return common data types, as well as corresponding derived types of ResponseMessage. For example, to send an RPC and get a string response from the subscriber, a plugin would say:

   String responseString = Engine.getAgent().sendRPCReturnString(msg);

The subscriber, when receiving the RPC message, responds by saying:

   Engine.getAgent().sendStringResponse(msg, string value);

MessageAgent.sendStringResponse() sends a multiverse.msgsys.StringResponseMessage back to the process that invoked the RPC.

Analogous MessageAgent methods and derived types of ResponseMessage exist for types Boolean, Long, Integer and Object.

A NoRecipientsException is thrown if there are zero responders for an RPC request.

A MultipleRecipientsException is thrown if there are more than one responders for an RPC request.

A RPCException is thrown if the remote agent throws an exception while handling an RPC request. The RPCException carries the full remote stack context from the point the exception was thrown.

Asynchronous calls

If the RPC request publisher does not want to block waiting for the response, it can use asynchronous RPC. The publisher provides a callback with the request message. The callback is invoked when the response message arrives. For example, to send an asynchronous RPC and handle the response:

   MyRequest request = new MyRequest();
   Engine.getAgent().sendRPC(request,new MyResponseCallback());
   // ... do other things ...

   class MyResponseCallback implements  ResponseCallback {
       MyResponseCallback() { }
       public void handleResponse(ResponseMessage response) {
           System.out.println("Got response to MyRequest");
       }
   }

The RPC subscriber/responder is the same for asynchronous RPC.

Asynchronous RPC helps reduce thread resource requirements by not blocking a worker thread that could otherwise be doing useful work.

If the remote agent throws an exception while handling the RPC request, then an ExceptionResponseMessage will be delivered to the callback instead of the expected message. Pass the contained ExceptionData into an RPCException to get the remote stack trace.

Broadcast calls

Broadcast RPC allows the publisher to get responses from multiple subscribers. The request message is sent to all responders. Responses are delivered to the publisher's callback. For example, to send a broadcast RPC:

   MyRequest request = new MyRequest();
   Engine.getAgent().sendBroadcastRPC(request,new MyResponseCallback());
   // ... do other things ...

Advertisements

Message agents are required to "advertise" the message types they expect to publish. Agents use the advertisements to determine where to forward a subscription. This has a significant performance benefit when creating a subscription; the subscriber only waits for those agents that could publish a matching message.

Each process lists their advertisements in mv-home/config/common/process-name-ads.txt or mv-home/config/world-name/process-name-ads.txt. The contents of the two files are merged.

The logs will contain errors ("NEED ADVERT") if a process publishes message types it does not advertise. Incorrect advertisements can result in undelivered messages and RPC exceptions.

Another reason the "NEED ADVERT" log message can often show up if you do not add your message to the message catalog.

Name advertisements based on the name of the Message Type. For example, mv.ADD_ITEM, mv.INV_REMOVE, mv.ABILITY_UPDATE.

NOTE: When editing advertisement file, you must go the last entry and hit enter, add your message, and then hit enter again so that there is a new line after the last line. This is not obvious if you are editing using notepad. Use Worldpad to edit -ads files.

Advanced messaging

The Multiverse message system contains a number of advanced features. If used, the features enable highly scalable messaging. The Multiverse "perception system" makes use of the features to support efficient perceiver messaging for thousands of players. For more information on the perception system, see Perception Messaging.

Thread use

Three kinds of threads invoke application message callbacks

  • MessageIO - This thread handles sending and receiving messages from other agents.
  • SelfMessage - This thread handles messages and responses sent and received within the same agent.
  • ResponseMsg - This thread pool invokes ResponseCallbacks for responses from other agents.

MessageIO and SelfMessage are not thread pools. If one of these threads blocks in an application callback, then subsequent message processing is delayed for the whole process. For this reason, application code should handle messages as quickly and efficiently as possible. If message processing requires any network or disk I/O, then it should be handled by a separate thread.

The MessageCallback implemented by EnginePlugin places messages into a thread pool for processing. If you implement your own MessageCallback (or over-ride handleMessage() in your plugin), then you should set up your own thread pool for message processing.

If your callback object also implements multiverse.msgsys.MessageDispatch, then messages will be sent to MessageDispatch.dispatchMessage() instead of MessageCallback.handleMessage(). The intent is that dispatchMessage() can put the message into a thread pool. The thread pool worker method (usually Runnable.run()) simply calls callback.handleMessage().

Engine provides a default message dispatcher that uses a thread pool of 10 fixed threads. To use this dispatcher, call Engine.defaultDispatchMessage() in your MessageDispatch.dispatchMessage(). MARS contains some examples of this usage in the SpawnGenerator and QuestState.

A thread pool calls response message callbacks (multiverse.msgsys.ResponseCallback). The thread pool defaults to ten fixed threads. You can provide your own response thread pool with MessageAgent.setResponseThreadPool().

Filter tables

As covered in Custom Filters, there are two approaches to optimizing subscription filter matching. Filter tables are the more general purpose approach as they can optimize multiple classes of filters.

All filters are stored in filter tables. By default, filters are stored in a DefaultFilterTable which optimizes MessageType matching. Given a Message instance, the DefaultFilterTable can find the set of matching subscriptions with an O(1) lookup. This is only the set of subscriptions with matching message types, matchRemaining() still needs to be called on each subscription filter.

When there are hundreds of subscriptions, the DefaultFilterTable is a huge performance win. However, if you write your own custom filter and have hundreds of subscriptions, the cost of calling matchRemaining() may be too expensive. If there's a way to optimize matchRemaining() by storing all the filters in a single data structure, then writing a filter table allows you to do that.

Filter tables are created by sub-classing multiverse.msgsys.FilterTable. You must implement three methods:

   public abstract void addFilter(Subscription sub, Object object);
   public abstract void removeFilter(Subscription sub, Object object);
   public abstract int match(Message message, Set<Object> matches,
       List<Subscription> triggers);

And your filter implementation must return an instance of your filter table from the following methods:

   public FilterTable getSendFilterTable();
   public FilterTable getReceiveFilterTable();
   public FilterTable getResponderSendFilterTable();
   public FilterTable getResponderReceiveFilterTable();

Each method must return a different instance of your filter table. You can define four class static data members containing the filter tables.

addFilter() is called when a new subscription is created. The Subscription object contains the subscription's id, filter, and trigger. The object identifies the subscription's message handler. This is an internal identifier that should not be modified. Method match() must return a set of these objects.

removeFilter() is called when a subscription is removed. The Subscription and object parameters identify which subscription to remove.

match() is called to find subscription matches for a new message. The method should add matching objects to matches and subscription triggers to triggers. These parameters may already contain things from other filter tables, so do not remove items.

Example

Here's an example filter table for the ChatMessage and ChatFilter described in Custom Filters. NOTE: This example has not been tested.

First, define the ChatMessage as a SubjectMessage. The message subject is the chat sender (player or mob). The chat is directed at one chat channel identified by a number.

   public class ChatMessage extends SubjectMessage {
       public ChatMessage() { }
       public ChatMessage(long oid, int channelId, String chat) {
           super(MSG_TYPE_CHAT,oid);
           this.channelId = channelId;
           this.chat = chat;
       }
       public int getChannelId() { return channelId; }
       public String getChat() { return chat; }

       int channelId;
       String chat;

       public static final MessageType MSG_TYPE_CHAT = MessageType.intern("my.CHAT");
   }

Next, define the ChatFilter. We could sub-class MessageTypeFilter, but the ChatMessage has only one fixed message type (MSG_TYPE_CHAT), so MessageTypeFilter is overkill. The ChatFilter filters for a single channel. A chat plugin might subscribe to hundreds of channels and channels may be added and removed dynamically.

   public class ChatFilter extends Filter {
       public ChatFilter() { }
       public ChatFilter(int channelId) {
           this.channelId = channelId;
       }
       public Collection<MessageType> getMessageTypes() {
           return filterTypes;
       }
       public boolean matchMessageType(Collection<MessageType> messageTypes) {
           return messageTypes.contains(ChatMessage.MSG_TYPE_CHAT);
       }
       public boolean matchRemaining(Message message) {
           if (message instanceof ChatMessage)
               return (((ChatMessage)message).getChannelId() == channelId);
           else
               return false;
       }
       public int getChannelId() { return channelId; }

       public FilterTable getSendFilterTable() { return sendFilterTable; }
       public FilterTable getReceiveFilterTable() { return receiveFilterTable; }
       public FilterTable getResponderSendFilterTable() { return responderReceiveFilterTable; }
       public FilterTable getResponderReceiveFilterTable() { return responderSendFilterTable; }
 
       int channelId;

       static List<MessageType> filterTypes = new ArrayList<MessageType>(1);
       static {
           filterTypes.add(ChatMessage.MSG_TYPE_CHAT);
       }

       static FilterTable sendFilterTable = new ChatFilterTable();
       static FilterTable receiveFilterTable = new ChatFilterTable();
       static FilterTable responderReceiveFilterTable = new ChatFilterTable();
       static FilterTable responderSendFilterTable = new ChatFilterTable();
   }

In addition to implementing the standard filtering methods, ChatFilter also implements the four methods to return filter table instances. Class static variables are used to create singleton ChatFilterTable instances.

Finally, we can implement the ChatFilterTable. The ChatFilterTable methods should use exclusive locking (such as synchronized) to prevent race conditions.

   public class ChatFilterTable extends FilterTable
   {
       public synchronized void addFilter(Subscription sub, Object object)
       {
           Integer channel = ((ChatFilter)sub.getFilter()).getChannelId();
   
           Map<Object,List<Subscription>> objectMap = channels.get(channel);
           if (objectMap == null) {
               objectMap = new HashMap<Object,List<Subscription>>();
               channels.put(channel,objectMap);
           }
           LinkedList<Subscription> subList =
               (LinkedList<Subscription>) objectMap.get(object);
           if (subList == null) {
               subList = new LinkedList<Subscription>();
               objectMap.put(object,subList);
           }
           if (sub.getTrigger() != null)
               subList.addFirst(sub);
           else
               subList.addLast(sub);
       }
   
       public synchronized void removeFilter(Subscription sub, Object object)
       {
           Integer channel = ((ChatFilter)sub.getFilter()).getChannelId();
   
           Map<Object,List<Subscription>> objectMap = channels.get(channel);
           if (objectMap == null) {
               return;
           }
           List<Subscription> subList = objectMap.get(object);
           if (subList == null) {
               return;
           }
   
           ListIterator<Subscription> iterator = subList.listIterator();
           while (iterator.hasNext()) {
               Subscription ss = iterator.next();
               if (ss.subId == sub.subId) {
                   iterator.remove();
                   break;
               }
           }
           if (subList.size() == 0) {
               objectMap.remove(object);
           }
       }
   
       public synchronized int match(Message message, Set<Object> matches,
           List<Subscription> triggers)
       {
           Integer channel = ((ChatMessage)message).getChannelId();
           Map<Object,List<Subscription>> objectMap = channels.get(channel);
           if (objectMap == null)
               return 0;
           int count = 0;
           for (Map.Entry<Object,List<Subscription>> entry : objectMap.entrySet()) {
               List<Subscription> subs = entry.getValue();
               boolean matched = false;
               for (Subscription sub : subs) {
                   if (sub.filter.matchRemaining(message))  {
                       if (!matched && matches.add(entry.getKey())) {
                           count++;
                           matched = true;
                       }
                       if (triggers != null && sub.getTrigger() != null &&
                                   sub.getTrigger().match(message))
                           triggers.add(sub);
                       else
                           break;
                   }
               }
           }
           return count;
       }
   
       Map<Integer,Map<Object,List<Subscription>>> channels =
                   new HashMap<Integer,Map<Object,List<Subscription>>>();
   }

Message triggers

Message triggers enable you to run code on publishers when they publish a message matching a particular subscription. The trigger can be associated when a subscription is created. The trigger object is duplicated on each agent receiving the subscription (just like the Filter object is duplicated). A trigger object should only be associated with one Filter instance. The trigger can modify the message or the filter, but cannot prevent delivery. Triggers are run after subscription matching, but before message delivery.

Create message triggers by sub-classing multiverse.msgsys.MessageTrigger. Your implementation should include the following methods:

  • public abstract void setFilter(Filter filter) - saves the filter in the trigger object.
  • public boolean match(Message message) (optional)
  • public abstract void trigger(Message message, Filter filter, MessageAgent agent) - implements the trigger behavior.

See the MessageTrigger Javadoc for implementation instructions.

The Multiverse "perception system" uses a trigger to automatically update subscription filters on the world manager when it publishes PerceptionMessages. See multiverse.msgsys.PerceptionTrigger</class1.2>

Personal tools