aktrs 1.1 manual

updated on 27 May 2016 by Raffaele Arecchi


aktrs is a Java application library implementing the Actor Model. It provides remote actors through TCP sockets, actor profiling and tunable logging. It's API is easy to use and its core is written in few lines of source code.

aktrs requires Java version 1.5 or newer, has no additional dependencies and can be packaged into any application according to its LGPL license.

Contents:

  1. What actors are
  2. A bit of aktrs' ontology
  3. Getting started with actors
  4. Actors adresses
  5. Summoning actors
  6. Actor initialization
  7. Telling messages
  8. Customizing mailbox size
  9. Processing messages
  10. Stopping actors
  11. Shutting down the ActorSystem
  12. Remote systems and remote actors
  13. Logging
  14. Profiling
  15. More examples

What actors are

Actors are independent entities which communicate by sending messages to each other and which, in response to the messages they receive, are able to make decisions, perform elaborations, send messages to other actors and eventually creating new actors.
Designing a software with actors could add complexity to the domain the software models, but with the advantage to have clear, understandable and testable components especially when the domain relates to high concurrent environments.

A bit of aktrs' ontology

Actors are objects that operate independently and in parallel. An actor communicate to another actor by sending, or telling, it a message to its mailbox. A mailbox is a container of messages ordered by delivery time (also known as first-in-first-out queue). An actor waits while its mailbox is empty, otherwise it picks the oldest message, performs behavior related to that message, and repeats this sequence as long as the mailbox is not empty.
An actor system, or simply system, is a container of actors and is able to create, or summon, new actors and to stop, or dissolve, any actor. An actor is said to belong to a system if that system contains the actor.
Two systems can be linked togheter. A system is able to summon any actor on any linked system.
An actor is said to be local or remote relative to a system. An actor is local to a system if it belongs to that system. An actor is remote to a system A if it belongs to another system B which is linked to A. Any actor can tell messages to any local or remote actor.
An actor is operative if it is ready to process messages from its mailbox, otherwise it is not operative.

Getting started with actors

The main entities are instances of the aktrs.rt.ActorSystem and aktrs.rt.Actor classes. An ActorSystem is a container of actors and is responsible for their life-cycle management. An actor is implemented as an instance of the Actor class. The Actor class is expected to be sub-classed to provide custom actor behavior.

To create a custom actor class, just extends Actor providing it a default constructor, then override the Actor.onMessage(Object message, Actor sender) method with the desired behavior:

import aktrs.rt.Actor;

public class HelloActor extends Actor {
  public HelloActor(){}		

  @Override
  protected void onMessage(Object message, Actor sender) {
    System.out.println("Hello " + message.toString());
  }
} 

To use this actor, we need to "summon" it through the method ActorSystem.summon(String systemId). In the example below we let an actor send two messages to itself by the instance method Actor.tell(Object message, Actor recipient), then we finally shutdown the ActorSystem:

import aktrs.rt.Actor;
import aktrs.rt.ActorSystem;

public class HelloTest {

  public static void main(String[] args) {
    ActorSystem mySystem = new ActorSystem("my-sys");
    mySystem.setLogLevel(Level.FINE);
    Actor helloActor = mySystem.summon("/my-sys/hello-actor", HelloActor.class);
    helloActor.tell(helloActor, "John");
    helloActor.tell(helloActor, "Tim");
    mySystem.shutdown("finished");
  }
}

which logs on the standard output:

11-02-2016 05:23:34.349-[main                 ]-1-[aktrs.rt.ActorSystem.]-Actor System my-sys started.
11-02-2016 05:23:34.365-[main                 ]-1-[aktrs.rt.ActorSystem.summon]-Actor System my-sys. Summoning  /my-sys/hello-actor, Class:  prova.HelloActor, Initialization object: null
11-02-2016 05:23:34.365-[main                 ]-1-[aktrs.rt.ActorSystem.summon]-Actor System my-sys. Actor /my-sys/hello-actor is a local actor
11-02-2016 05:23:34.365-[main                 ]-1-[aktrs.rt.ActorSystem.summon]-Actor System my-sys. Actor /my-sys/hello-actor summoned
11-02-2016 05:23:34.365-[/my-sys/hello-actor  ]-12-[aktrs.rt.Actor.run]-Starting Actor /my-sys/hello-actor
11-02-2016 05:23:34.365-[/my-sys/hello-actor  ]-12-[aktrs.rt.Actor.run]-Actor /my-sys/hello-actor, mail box size: -1
11-02-2016 05:23:34.365-[main                 ]-1-[aktrs.rt.Actor.tell]-Actor /my-sys/hello-actor telling to a local Actor /my-sys/hello-actor
11-02-2016 05:23:34.365-[/my-sys/hello-actor  ]-12-[aktrs.rt.Actor.run]-Actor /my-sys/hello-actor is operative
11-02-2016 05:23:34.365-[main                 ]-1-[aktrs.rt.Actor.tell]-Actor /my-sys/hello-actor telling to a local Actor /my-sys/hello-actor
11-02-2016 05:23:34.365-[/my-sys/hello-actor  ]-12-[aktrs.rt.Actor.run]-Actor /my-sys/hello-actor, received message. Sender: /my-sys/hello-actor , message: John
11-02-2016 05:23:34.381-[main                 ]-1-[aktrs.rt.Actor.tell]-Actor /my-sys/system telling to a local Actor /my-sys/system
11-02-2016 05:23:34.381-[/my-sys/system       ]-11-[aktrs.rt.Actor.run]-Actor /my-sys/system, received message. Sender: /my-sys/system , message: stop:{finished}
11-02-2016 05:23:34.381-[/my-sys/system       ]-11-[aktrs.rt.Actor.run]-Actor /my-sys/system is no more operative
11-02-2016 05:23:34.381-[/my-sys/system       ]-11-[aktrs.rt.SystemActor.manageStop]-Shutting down Actor System my-sys : finished
11-02-2016 05:23:34.381-[/my-sys/system       ]-11-[aktrs.rt.ActorSystem.dissolve]-Actor System my-sys. Dissolving local Actor /my-sys/hello-actor : finished
Hello John
11-02-2016 05:23:34.381-[/my-sys/system       ]-11-[aktrs.rt.Actor.tell]-Actor /my-sys/system telling to a local Actor /my-sys/hello-actor
11-02-2016 05:23:34.381-[/my-sys/hello-actor  ]-12-[aktrs.rt.Actor.run]-Actor /my-sys/hello-actor, received message. Sender: /my-sys/hello-actor , message: Tim
Hello Tim
11-02-2016 05:23:34.381-[/my-sys/hello-actor  ]-12-[aktrs.rt.Actor.run]-Actor /my-sys/hello-actor, received message. Sender: /my-sys/system , message: stop:{finished}
11-02-2016 05:23:34.381-[/my-sys/hello-actor  ]-12-[aktrs.rt.Actor.run]-Actor /my-sys/hello-actor is no more operative

The logs show the asynchronous nature of the system in which the call to ActorSystem.shutdown(String reason) could happen before the messages are actually processed; the reason why here the actor continues to process is because the shutdown actually send Stop messages (more on the semantic of shutdown in Stopping actors).
The ActorSystem is instantiated with an ID my-sys. While not necessary for this example, the ID is important when other remote ActorSystems must be connected togheter (linked) and be identifiable by themselves.
An Actor with address /my-sys/hello-actor is summoned specifying its real type HelloActor. The system ID in the address string is not superfluous, in fact every ActorSystem is able to summon any actor on any remote linked ActorSystem.
By Actor.tell(Object message, Actor recipient), the actor sends to itself the messages (in this case strings) "John" and "Tim".
By Actor.onMessage(Object message, Actor sender), the actor carries out its job, in this case printing a greeting message on the standard output.

Actor addresses

Every actor has an address. This address is specified by the string /system-id/actor-name. The beginning and the second slashes in the address are mandatory, their mean is to identify the ActorSystem ID which manages the actor. For this reason a system ID could be any sequence of characters but must not contain any slash. The actor name instead may contain any characters including slashes.
The internal representation of an actor address is realized by class aktrs.rt.ActorAddress. The constructor of this class admits a valid address as string and will throw a RuntimeException if the address is not admissible.
Most of the API in aktrs provides method versions with addresses both in String and ActorAddress formats.

Summoning actors

To create an actor, the ActorSystem class provides the following instance methods:

All of these accept the address both in String or in ActorAddress format and the class for the actor to be summoned; they return the summoned actor instance. Some of them provide an optional initial object input, used by the actor in the initialization phase.
The summoning of an actor consists of the following steps: The Thread associated to the summoned actor execute these tasks:
  1. initialize the actor mailbox with a java.util.concurrent.LinkedBlockingQueue for the messages,
  2. if an initObj was provided, execute method Actor.init(Object initObj) (see Actor initialization),
  3. make the actor operative,
  4. begin to consume synchronously the messages inside its mailbox.

Actor initialization

Initialization is performed by Actor.init(Object initObj). The default implementation does nothing, so to customize initialization an override of the method must be implemented.

An example of custom initialization is the following, where an address of an actor is passed as initialization object and the reference to that actor is stored inside:

import aktrs.rt.Actor;
import aktrs.rt.ActorAddress;

public class HelloActor extends Actor {
  private Actor forwardActor;

  public HelloActor() {}

  @Override
  protected void init(Object initObj) {
    forwardActor = getSystem().evoke((ActorAddress) initObj);
  }

  @Override
  protected void onMessage(Object message, Actor sender) {
    tell(forwardActor, "Hello " + message.toString());
  }
}

The Actor.getSystem() returns the ActorSystem to which it belongs, the ActorSystem.evoke(ActorAddress address) returns the actor registered at the address parameter, or null if no actor is registered at that address.
Hence the behavior of this actor is to forward a greeting message to another actor.

If an Exception is thrown while initializing, the actor will go to a non operative state and its associated Thread terminates.

Telling messages

Telling a message to another actor is done by the instance method Actor.tell(Object message, Actor recipient).
Calling this method is blocking until the message is successfully delivered to the recipient actor's mailbox. The method returns as soon as the message is on the recipient mailbox, therefore from an actor perspective the call may be seen as asynchronous.

If the recipient actor is not operative, the delivery is delayed to a maximum time of 1000 milliseconds (1 second), after which if the recipient is still not operative a RuntimeException is thrown.
The reason for this retry is because the actor could be not operative during its initialization phase.

This example show an "echo actor" which tells to the caller the same message:

import aktrs.rt.Actor;

public class EchoActor extends Actor {
  public EchoActor() {}

  @Override
  protected void onMessage(Object message, Actor sender) {
    tell(sender,message);
  }
}

Customizing mailbox size

The Actor class implements its mailbox as an unbounded java.util.concurrent.LinkedBlockingQueue. It is possible to define a maximum capacity for the mailbox overriding Actor.mailboxSize().

In the example below the capacity is set to 1000:

import aktrs.rt.Actor;

public class HelloActor extends Actor {
  public HelloActor() {}

  @Override
  protected int mailBoxSize() {
    return 1000;
  }

  @Override
  protected void onMessage(Object message, Actor sender) {
    System.out.println("Hello " + message.toString());
  }
}

Every Actor.tell method call to an actor with a full fixed-size mailbox is blocking until the mailbox will be freed to accommodate new incoming messages.
It is not possible to change the size of the mailbox once it is initialized.

Fixed mailbox size is useful in high-throughput contexts where memory consumption must be controlled and actor processing time is slow or uncertain.

Processing messages

An actor processes the messages from its mailbox once at a time by method Actor.onMessage(Object message, Actor sender). The classes extending Actor are expected to override this method to accomplish their purpose.

If an Exception is raised while processing a message, method onProcessingError(Exception e, Object message, Actor sender) is called. Actor classes should override this method if custom operations are needed. An Exception raised while processing a message does not cause an actor to stop nor its associated Thread to die, instead the actor will continue to process messages from its mailbox.

Here an example of an actor who computes squares, complaining with the sender actor if the message is not an Integer:

import aktrs.rt.Actor;

public class SquareActor extends Actor {
  public SquareActor() {}

  @Override
  protected void onMessage(Object message, Actor sender) {
    Integer value = (Integer) message;
    System.out.println("Square of " + value + " is " + value * value);
  }

  @Override
  protected void onProcessingError(Exception e, Object message, Actor sender) {
    if (e instanceof ClassCastException) {
      tell(sender, "Cannot compute square of " + message + "!!!");
    }
  }
}

Stopping actors

Every actor could stop another actor by sending it a aktrs.messages.Stop message. Below an example of an actor who stops who tells it a message:

import aktrs.messages.Stop;
import aktrs.rt.Actor;

public class KillerActor extends Actor {
  public KillerActor() {}

  @Override
  protected void onMessage(Object message, Actor sender) {
    tell(sender, Stop.create("no real reason"));
  }
}

Sending a Stop is not an appropriate solution to terminate an actor: when receiving a Stop the actor:

  1. clears its mailbox,
  2. puts itself in not operative state,
  3. calls Actor.manageStop(Stop message).
The method Actor.manageStop is designed to be overridden if specific stop processing is needed.
Sending a Stop will not cause the actor to be removed from the ActorSystem to which it belongs. Also this causes its address to remain assigned in its ActorSystem.

To gracefully terminate an actor, use one of these methods in ActorSystem:

The reason parameter is optional, the actor parameter could be furnished in two forms: by reference or by address. ActorSystem.dissolve will send a Stop message and unregister the actor from the ActorSystem it belongs to.

The previous example could be rewritten as:

import aktrs.rt.Actor;

public class KillerActor extends Actor {
  public KillerActor() {}

  @Override
  protected void onMessage(Object message, Actor sender) {
    getSystem().dissolve(sender);
  }
}

Shutting down the ActorSystem

Shutdown the system with one of the following method in ActorSystem:

Both will dissolve all local actors and close all opened resources. To shutdown a linked ActorSystem see Remote systems and remote actors.

Another way to shutdown is done by sending a Stop message to the system actor:

import aktrs.messages.Stop;
import aktrs.rt.Actor;
import aktrs.rt.ActorSystem;

public class Test {
  public static void main(String[] args) {
    ActorSystem mySystem = new ActorSystem("my-sys");
    Actor systemActor = mySystem.evoke("/my-sys/system");
    systemActor.tell(systemActor, Stop.create("time to die"));
  }
}

Remote systems and remote actors

When two ActorSystems are linked togheter, all their actors can communicate each other by sending messages and even summoning actors on the linked system. Linking is actually implemented by TCP socket connections. There is no limit to how many systems can be linked togheter. To let an ActorSystem be linkable, it must be instantiated with a listening port.

Running a remote system is simple as to run this class:

import aktrs.rt.ActorSystem;

public class RemoteSys {
  public static void main(String[] args) {
    new ActorSystem("remote-sys", 9002);
  }
}

This new system remote-sys will accept TCP connections to port 9002. No other work is needed to do here: summoning, sending messages, dissolving, shutting down and all operations can be done by a remote linked system.
A deployment pattern could be: run this class as a single JVM process on any machine, then run the actual software only on one system who will orchestrate everything. Of course, if changes to the software are made, a full restart must be done since dynamic class reloading is not implemented.

This example class, which we run on the same machine, shows a linking of the previous system, a remote summon, a remote tell and a remote dissolve:

import java.net.InetAddress;
import java.net.UnknownHostException;
import aktrs.rt.Actor;
import aktrs.rt.ActorSystem;

public class Main {
  public static void main(String[] args) throws UnknownHostException {
    ActorSystem system = new ActorSystem("main-sys", 9001);
    system.link("remote-sys", InetAddress.getLocalHost(), 9002);
    Actor remoteActor = system.summon("/remote-sys/remote-actor", Actor.class);
    system.evoke("/main-sys/system").tell(remoteActor, "Hello!");
    system.dissolve(remoteActor);
  }
}

Linking is done by instance method ActorSystem.link(String remoteId, InetAddress netAddress, int port). If an ActorSystem with the same id is already linked, disconnection and reset with the new parameters are performed. Linking is lazy in that a connection will established only when the first actor-tell or summon or any "remotable" operation is performed. If an ActorSystem A become linked to an ActorSystem B then automatically B will be linked to A.

All tells to a remote actor must send a java.io.Serializable message. If the message is not serializable, a RuntimeException will occur.

A remote shutdown is not possible, instead send a Stop message to the actor system belonging to the remote ActorSystem.

Internally, the reference to an Actor which belongs to a linked remote ActorSystem is implemented in aktrs with a proxy/mirror, so that all remote operations are actually redirected. aktrs automatically manages the creation of these proxies so that working with remote actors is transparent inside code.

Logging

By default, the ActorSystem instantiate a java.util.logging.Logger with a Level.INFO and printing to the standard output, precisely using a aktrs.util.ConsoleOutputHandler with formatter aktrs.util.AktrsFormatter.

To provide a custom java.util.logging.Logger, ActorSystem can be instantiated with the following contructors:

Change of logging level at runtime may be accomplished by method ActorSystem.setLogLevel(Level newLevel).

Profiling

Processing data time can be collected from any actor. An actor can be told to enable/disable profiling by receiving a message aktrs.messages.ToggleProfilingMessage, whose property mode can be set to aktrs.messages.ProfilingMode.ON / aktrs.messages.ProfilingMode.OFF respectively.

To get the average processing time of an actor, i.e. the mean time of all processing times for Actor.onMessage, send it a message aktrs.messages.AverageProcessingTimeRequest with field replyTo compiled with the address of the actor who will receive the report data aktrs.messages.AverageProcessingTimeResponse.
Average processing time che also be requested only for a specific class of messages: the request is a message aktrs.messages.AverageProcessingTimeByClassRequest whose field replyTo is compiled as before and field klass is compiled with the class instance of the messages argument of the request: in this case the report is a aktrs.messages.AverageProcessingTimeByClassResponse.

Optionally, aktrs.messages.ToggleProfilingMessage can be instanciated with the following parameters:

Parameter name Type Default Description
flowMetricsPrecision long 1000 Specify the minimum time interval (milliseconds) by which collect flow metrics (see below)
flowMetricsRetention long 2 Specify the maximum number of time intervals which collect the flow metrics

If profiling is enabled, upon each message receiving the actor collects some metrics about processing time, idle time and queue sizes.
On subsequent message, if the time elapsed from the start of the collection of metrics is greater than those specified by flowMetricsPrecision, the information gathered is finalized into a aktrs.rt.FlowMetric and a new collection of metric is performed.
The numbers of maximum collected metrics is defined by flowMetricsRetention, after which the oldest metrics are discarded.

To request the metrics, send to the actor a message aktrs.messages.FlowMetricsRequest with field replyTo compiled with the address of the actor who will receive the report data aktrs.messages.FlowMetricsResponse. The response is filled with the mailbox capacity, with the flowMetricsPrecision and with a list of aktrs.messages.FlowMetricsData which collects the metrics on time intervals ordered by the most recent to the oldest.

A FlowMetricsData is described by (start-time and end-time are defined by milliseconds from 1 Gen 1970):

Parameter name Type Description
startInterval long The start-time of the interval
startQueueSize long The size of the mailbox at start-time of the interval
elapsedWorkTime long The processing-time (non-idle state) in the interval
endInterval long The end-time of the interval, -1 if the collection of this metric is not completed
endQueueSize long The size of the mailbox at end-time of the interval,-1 if the collection of this metric is not completed

For example, from this data it is possible to know the idle time (processing time minus interval size).
Note: the FlowMetricsRequest is just like any message sent to an actor, therefore response is produced whenever the actor is able and free to process the response.

More examples

The source package of this software includes the source code for tests under directory src/test. These tests contains some little explanations comments to what they do, and may be useful as examples.


Author: Raffaele Arecchi. Contact the author of this document via mail to: raffaele.arecchi{at}gmail{point}com
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.