Event-Oriented Programing Using Annotations

Overview

There are two different programming paradigms that you may use when developing applications using the MASS library:

Traditionally, the MASS library has been used in the centralized mode of operation, using patterns and methods similar to those found with other distributed computing frameworks like MPI. In this style of development, methods within Places and Agents would be called from a main application, and thus the performance of the simulation is directly linked to the master node.

With the 1.3.0 release of the MASS library, a new paradigm is offered that provides an “event-centric” model for developing applications that aims to simplify development and provide a more intuitive interface for certain types of agent-based implementations. In this style, methods within Agents and Places are executed upon certain lifecycle event triggers.

How it Works

In the event-oriented model, code that would direct Agent movement or Place operations is invoked from “within” the Agent or Place. For example, code that controls Agent movement is contained entirely within the Agent class, rather than partially split between the Agent and main application (“callAll”). This code is invoked during the lifecycle of the agent, as the simulation executes. Event triggers are synchronized with the master node, thus preventing out-of-order execution caused by individual node performance.

Events

There are currently three events that trigger execution of methods in Agents and Places:

  1. Intialization: “OnCreation”
  2. Movement (Arrival and Departure): “OnArrival” and “OnDeparture”
  3. On Message Delivery (Agents only): “OnMessage”

Annotations are used to determine which methods are invoked when these events occur. These events are described individually in more detail below.

Agent Event Annotations

@OnCreation

A method annotated with “@OnCreation” will be called once, after initial creation of the Agent and after association with a Place.

@OnArrival

A method annotated with “@OnArrival” will be called after migration to a new Place. This method will be called AFTER the Place has executed “@OnArrival” for this Agent.

@OnDeparture

A method annotated with “@OnDeparture” will be called immediately before the Agent actually beings migrating to a new Place. This method will be called BEFORE the Place has executed “@OnDeparture” for this Agent.

Place Event Annotations

@OnCreation

A method annotated with “@OnCreation” will be called once, after initial creation of the Place.

@OnArrival

A method annotated with “@OnArrival” will be called when an Agent migrates to this Place. This method will be called BEFORE the Agent has executed “@OnArrival”.

@OnDeparture

A method annotated with “@OnDeparture” will be called before the Agent actually migrates away from this Place. This method will be called AFTER the Agent has executed “@OnDeparture”.

Messaging Annotations

@OnMessage

A method annotated with “@OnMessage” will be called when a message addressed to the Agent/Place “arrives”, and the (optional) Class of the argument matches the Class of the message. For example, if the method annotated with “OnMessage” accepts an argument of type “Integer”, then that method will be called if an Integer message arrives. If a message of type “String” arrives, that method would not be called.

Application Structure

An event-centric MASS application will typically contain at least three classes:

  1. Main Application: Responsible for initializing the library and main simulation iteration loop (more on this later)
  2. Place: Represents a location for Agents to visit and examine. As Agents arrive and depart, events are triggered within the Place class to react to the movement of Agents to/from the Place.
  3. Agent: A (typically) mobile entity that roams from Place to Place, performing actions on Places or making computations based on Place data.

The event-centric MASS main application will call “MASS.init()” to set up the cluster and to start initial distribution of Places and Agents. After initialization has been performed, the main application can cycle through the simulation using either “doAll”, “doUntil”, or “doWhile” methods.

doAll

Instead of repeatedly calling (Agents) callAll() and manageAll() as you would with the traditional MASS application, you can use the “doAll” method, which will perform the same action. A variant of “doAll” accepts an iteration count, so the simulation will perform the specified number of iterations without having to explicity create a “for” loop.

doUntil

The “doUntil” method accepts a function as an argument. While the function continues to evaluate to FALSE, the “doAll” method is called repeatedly. For each iteration the function is reevaluated to see if the condition has changed.

doWhile

The “doWhile” method operates the same as “doUntil”, except that that logic is opposite; while the function continues to evaluate to TRUE, the “doWhile” method is called repeatedly. For each iteration the function is reevaluated to see if the condition has changed.

Examples

Following are code snippets that provide examples of how annotated event methods may be used by Agents and Places. #### Initializing a Place or Agent #### This snippet shows how an Agent can be initialized and prepared immediately before the simulation begins.

#!java

    @OnCreation
    public void doInitializationTasks() {

        // Determine the first Place I should start working at

        // Set my initial state

        // Ready to start!

    }

Invoking an Agent Method Upon Arrival at a Place

#!java

    @OnArrival
    public void onArrival() {
        
        Node node = ( Node ) getPlace( );
        
        // have I not arrived at the target node yet?
        if ( nextNode != node.getIndex( )[0] ) return;
        
        // any where else to go? was I not the first to arrive at this node?
        if ( node.getFootprint() == Node.VISITED || node.getNeighbors() == null || node.getNeighbors().length == 0 ) {
            
            MASS.getLogger( ).debug( "Agent [" + getAgentId( ) + "] terminated onArrival at a deadend [" + getPlace( ).getIndex( )[0] + "]" );
            
            kill();
            return;
        
        }

        // upon arrival at a Node, set footprint
        node.setFootprint( Node.VISITED );

    }

Invoking a Place Method Upon Agent Arrival

#!java
    @OnArrival
    public void doAgentArrival() {

        // increment the visit count
        numAgentsVistited++;

    }

Sending Messages

There are several different ways to send a message, your selection will depend on which Agents/Places should receive the message and the message payload (if any):

#!java

    // Send a String object as a message to ALL Agents
    MASS.getMessagingProvider().sendAgentMessage( MessageDestination.ALL_AGENTS, "Hello!" );

    // Send an Integer to a single, specific Agent by ID
    MASS.getMessagingProvider().sendAgentMessage( 1, 42 );  // send "42" to Agent #1

    // Send a broadcast message to ALL Places
    MASS.getMessagingProvider().sendPlaceMessage( MessageDestination.ALL_PLACES, "Hello!" );

Here is an example of an Agent sending a broadcast message informing all other Agents that it has found a new best value:

#!java

    // Have I found a better value? Broadcast it if so!
    if ( bestPV < bestGV ) {

        BestXYV newBestCoordinates = new BestXYV( bestPX, bestPX, bestPV );
        MASS.getMessagingProvider().sendAgentMessage( MessageDestination.ALL_AGENTS, newBestCoordinates );

    }

Receiving a Message

Messages may be used to convey information or to simply trigger behavior. The annotated method must either accept no arguments or match the data type of the transmitted message payload.

#!java

    @OnMessage
    public void receiveIntegerPayloadMessage( int payload ) {
    
        // do something with the received message
        myLocalCount += payload;

    }

    @OnMessage
    public void receiveAnyMessage() {

        // this is called whenever a message is received
        doTriggeredActivity();

    }

Using doWhile in a Main Application

The following code shows how the main application iterates through a simulation until a condition evaluates to false. In this example, the Agents “die” once their mission has completed. When all Agents have completed, the condition evaluates to FALSE and the simulation ends.

#!java

    // start MASS
    MASS.init();
        
    // Time measurement starts
    System.out.println( "Go!" );
    long startTime = System.currentTimeMillis( );

    // Create a map
    Places network = new Places( 1, Node.class.getName( ), null, nNodes );
    long networkGenerationEndTime = System.currentTimeMillis( );
        
    // Instantiate agents
    Args2Agents args2agents = new Args2Agents( src );
    Agents agents = new Agents( 2, Crawler.class.getName( ), ( Object ) args2agents, network, 1 );

    // run the simulation until all agents have completed their tasks
    agents.doWhile( () -> agents.hasAgents() );

    // Time measurement ends
    long graphGenTime = networkGenerationEndTime - startTime;
    long elapsedTime = System.currentTimeMillis( ) - startTime;

    // Stats
    System.out.println( "Statistics:");
    System.out.println( "Time to generate graph (ms): " + ( graphGenTime ) );
    System.out.println( "Time to traverse graph (ms): " + ( elapsedTime - graphGenTime ) );
    System.out.println( "Total Elapsed time (ms): " + elapsedTime );

    MASS.finish( );