Chapter 4 Actors

This chapter introduces Actors both conceptually through the Actor Model and concretely through Akka and discusses architectures with Actors. The essence of actors can be understood to be a mathematical model of concurrent computation with shared-nothing message-passing semantics. The concept of actors has become increasingly relevant in the last decade due to the advent and increasing popularity of multi-core CPUs. To make better usage of the multiple cores on modern CPUs shared-nothing message-passing concurrency is considered to be easier to implement and handle than lock-based concurrency building on shared mutable state. This is supported by multiple software libraries in the field of web applications and distributed computation, which have built on implementations of the actor model such as Akka.

Although Actors can be seen as a framework for a theoretical understanding computation, although we will have a look at its theoretical aspects, in this course we will rather focus on its message-based concurrency aspect. In general, we can distinguish between two types of concurrency:

  1. Shared mutable state Is where multiple processes or threads operate on shared state, which they can arbitrarily read and write. You are familiar with this concept already from Java threads, which all share the same address space within a process and can therefore access the global program state and mutate it arbitrarily. The challenge in this type of concurrency is to get the synchronisation right: whenever mutable shared state is modified, it must not be concurrently accessed. This is generally achieved using various types of locks such as monitors, sempahores, mutexes. The correct usage of these synchronisation primitives to write robust and correct concurrent software is considered to be very hard. We assume a basic familiarity with this kind of concurrency as it is beyond the scope of this course to give a more detailed introduction. We refer interested students to [16].

  2. Message passing In this type of concurrency there is no shared mutable state but the problem is split into multiple autonomous processes / threads which have their own local memory and communicate with each other through messages. These messages contain only immutable data and no references to local memory of a process / thread. This means that no information is shared between processes / threads, therefore no concurrent modification of state can happen - we speak of shared-nothing message-passing concurrency.

Actors were an essential influence for the Message passing kind of concurrency, and in this chapter we will have a more in-depth look into their concept, how they are implemented and how they are used architecturally. We start out with the rather theoretical Actor Model as all the following concepts build on an understanding of it.

4.1 The Actor Model

The actor model is a model of computation, inspirted by physics. It was also influenced by the programming language Lisp, Simula-67 and Smalltalk-72, as well as ideas for Petri Nets. First ideas were published in 1973 by Hewitt, Bishop and Steiger [19]. In subsequent work [3], [14], [6], [1] the ideas were refined and developed into a full actor model theory. In this section we give a brief overview of the theory of actors, following the work of Agha [1] and Hewitt hewitt2010actor.

4.1.1 Fundamentals

Actors are computational agents which map each incoming communication to a 3-tuple consisting of:

  1. A finite set of communications (messages) sent to other actors.
  2. A new behaviour, which will govern the response to the next communication processed.
  3. A finite set of new actors created.

Several observations are in order here. First, the behaviour of an actor can be history sensitive. Second, there is no presumed sequentiality in the actions an actor performs: mathematically each of of its actions is a function of the actor’s behaviour and the incoming communication. Third, actor creation is an integral part of the computational model. In the context of parallel systems, the degree to which a computation can be distributed over its lifetime is an important consideration. Therefore, creation of new actors provides the ability to abstractly increase the distributivity of the computation as it evolves.

Analogous to special relativity, information in each actor is localised within that actor and must be communicated before it is known to any other agent. As long as one assumes that there are limits as to how fast information may travel from one actor to another, the local states of one agent as recorded by another agent relative to the second agent’s local states will be different from the observations done the other way round. Therefore, in actor systems there is simply no global system state or view.

The consequence is that, for a distributed system such as actors, a unique (linear) global time is not definable. Instead, each computational agent has a local time which linearly orders the events as they occur at that agent, or alternatively orders the local states of that agent. These local orderings of events are related to each other by the activation ordering. The activation ordering represents the causal relationships between events happening at different agents. Thus the global ordering of events is a partial order in which events ocurring at different computational agents are unordered unless they are connected, directly or indrectly, because of one or more causal links.

It is possible to implement a globally synchronising mechanism, which controls the cycles of computation by each of the actors in the system, see Figure 4.1. Each actor carries out one step at a time and communicates with the global synchroniser for permission to carry out the step. Such a mechanism is extremely inefficient however, as very actor must wait for the slowest actor to complete its cycle.

A global synchronising mechanism. (Figure inspired by @agha1985actors)

Figure 4.1: A global synchronising mechanism. (Figure inspired by [1])

Decoupling the sender from the communications it sends was a fundamental advance of the actor model, enabling asynchronous communication and control structures as patterns of passing messages.

The actor model can be used as a framework for modeling, understanding, and reasoning about a wide range of concurrent systems. For example email can be modeled as an actor system, where the mail accounts are modeled as actors and email addresses as actor addresses. Web services can be modeled with endpoints modeled as actor addresses. Objects (with locks, such as in Java/C#) can be modeled as actors.

4.1.1.1 Types of Communication

Communication provides a mechanism by which each agent retains the integrity of information within it. Generally, there are two possible types of communication:

  1. Sychronous, where both the sender and receiver of a communication must be ready to communicate before a communication can be sent. A real-world example is the telephone system.
  2. Asynchronous, where the receiver does not have to be ready to accept a communication when the sender sends it. A real-world example is the postal system.

In the original actor model [3], asynchronous communication is assumed. In general, one can conclude that synchronous communication is built on asynchronous communication: intuitively, there can be no action at a distance. Before a sender can know that the receiver is “free” to accept a communication, it must send a communication to the receiver and vice-versa. In fact, synchronous communication is a special case of buffered asynchronous communication:

In any system, every communication is of some finite length and takes some finite time to transmit. During this time, some other actor may try to send another communcation to the actor receiving the first communication. To avoid interleaving arbitrary bits of one communication with the other, and to preserve the atomicity of the communication sent a solution is to provide a secretary to each agent. This secretary is basically a buffer which synchronises incoming messages. This buffered asynchronous communication solves multiple problems of synchronous communication:

  • The receive does not have to process the incoming information as fast as the sender transmits it.
  • The sender does not have to busy wait for the receiver to accept a communication before it proceeds to so some other processing.
  • It enables an actor to communicate with itself (which is highly important for various pro-active tasks). This would be impossible in synchronous communication as the actor is both receiver and sender which must be both “free”, which is of course impossible. This is a fundamentally important property to have as it allows to implement recursive control structure and mutual recursion, as shown below.
  • Building on buffered asynchronous communication we can implement synchronous communication as a special case, if required.

4.1.1.2 Nondeterminism

Nondeterminism arises quite inevitably in a distributed environment. In any real network of computational actors, one cannot predict precisely when a communication sent by one agent will arrive at another. This is particular true when the network is dynamic and the underlying architecture is free to improve performance reconfiguring the virtual computational elements.

Therefore a realistic model must assume that the arrival order of communications sent is both arbitrary and entirely unknown. In particular, communications from different actors to a given target (even from the same actor) may arrive at approximately the same time. When this happens, it is necessary to provide a mechanism which orders incoming messages. This is achieved at the hardware level by an element called the arbiter.

The use of the arbiter for serialisation implies that the arrival order is physically indeterminate.

The internal processes of arbiters are not public processes. Attempting to observe them affects their outcomes. Instead of observing the internals of arbitration processes, we necessarily await outcomes. Therefore, indeterminacy in arbiters produces indeterminacy in actors. See Figure 4.2 for a logic diagram of an arbiter primitive.

Arbiter Concurrency Primitive (Figure taken from @hewitt2010actor)

Figure 4.2: Arbiter Concurrency Primitive (Figure taken from [18])

4.1.1.3 Fairness and the Mailsystem

All messages that are sent are eventually received by their targets. If many messages are en route to a single target, their simultaneous arrival may overload the target or cause the mssages to interfere with one another. Therefore, to guard against these possibilities and ensure a kind of super-position principle for messages, an arbiter is used in front of every actor which allows only one message to be received at a time.

This directly leads to unbounded nondeterminism, which is a property of concurrency by which the amount of delay in servicing a request can become unbounded as a result of arbitration of contention for shared resources while still guaranteeing that the request will eventually be serviced [17].

This follows from the following:

  • There is no bound that can be placed on how long it takes the Arbiter to settle. They are used in computer to deal with the circumstance that computer clocks operate asynchronously with interrupts from outside, e.g. keyboard, disc access,… This means it could take an unbounded time for a message sent to a computer to be received and in the meantime the computer could traverse an unbounded number of states.
  • Email enables unbounded nondeterminism since mail can be stored on servers indefinitely before being delivered. Communication links to servers on the internet can be out of service indefinitely.

This results in two important concepts: nondeterminism vs. indeterminism

  • In nondeterminism if there are two ways to proceed with a computation, a coin is flipped with a random outcome
  • In indeterminism the things are decided by working themselves out. There is no flipped coin but how things worked out.

In concrete terms, for actor systems, typically we cannot observe the details by which the arrival order of messages for an actor is determined. Attempting to do so affects the results and can even push the indeterminacy elsewhere. Instead of observing the internals of arbitration processes of actor computations, we await outcomes. Physical indeterminacy in arbiter produce indeterminacy in actors. The reason that we await outcomes is that we have no alternatives because of indeterminacy.

The first models of computation (e.g. Turing machines, Post productions, the lambda calculus, etc.) were based on mathematics and made use of a global state to represent a computational step. Each computational step was from one global state of the computation to the next global state. The global state approach was continued in automata theory for finite state machines and push down stack machines, including their nondeterministic versions. Such nondeterministic automata have the property of bounded nondeterminism; that is, if a machine always halts when started in its initial state, then there is a bound on the number of states in which it halts.

Edsger Dijkstra further developed the nondeterministic global state approach, which gave rise to a controversy concerning unbounded nondeterminismim. Unbounded nondeterminism is a property of concurrency by which the amount of delay in servicing a request can become unbounded as a result of arbitration of contention for shared resources while providing a guarantee that the request will be serviced. The Actor Model provides the guarantee of service. In Dijkstra’s model, although there could be an unbounded amount of time between the execution of sequential instructions on a computer, a (parallel) program that started out in a well-defined state could terminate in only a bounded number of states. He believed that it was impossible to implement unbounded nondeterminism.

A nondeterministic system is defined to have unbounded nondeterminism exactly when both of the following hold:

  1. When started, the system always halts.
  2. For every integer n, it is possible for the system to halt with output that is greater than n.

It is important to note that an actor processes always one message at a time and there is no parallel processing of messages within an actor. This is a fundamentally different concept than concurrency in objects: actors are truly autonomous, active and proactive and can actually refuse to process messages. Methods called on objects (speak: events sent to objects) can come from arbitrary concurrenc sources if they hold a reference to that object e.g. multiple threads calling into an object: this very object will then process multiple messages at the same time, which results in desaster if no synchronisation is happening, in case of mutable storage.

An important concept in actors is the one of a Future. The idea of a Future is that you can create an actor which eventually will produce a result. For example you can create a Future of the factorial of 100 Million, which would take substantial time to compute but will eventually terminate. Therefore the fundamental idea of a Future is that you get a representation of a (very) long running computation now, which you can pass it around while it is computing concurrently, and eventually you will either wait blocking for its result or get notified if it has finished. So Futures can be passed around, stored, sent in messages, all while the actual computation is carried out concurrently in an actor specifically created for it. Using Futures allows actors to send messages to themselves, allowing them to be proactive and initiate actions by themselves.

4.1.1.4 Topology

The patterns of communication possible in any system of actors define a topology on those actors. Each actor may, at any given point in its local time, communicate with some set of actors. As the computation proceeds, a process may either communicate only with those processes that it could communicate with at the beginning of the computation, or it may evolve to communicate with other processes that it could not communicate with before. In the former case, the interconnection topology is said to be static, in the latter it is dynamic.

An example for a static topology is seen in Figure 4.3. In it a resource manager handles requests of n users for printing on two devices. The communication links are fixed in advance. Such a system design with a static topolocy is undesirable as it is neither reconfigurable or extensible: it does not allow to add dynamically more users nor allow the resource manager to autonomously chose which printing device to use.

A static graph linking the resource manager to two devices and two users. Such graphs are an unsatisfactory representation of systems that may dynamically evolve to include new users or devices. (Figure inspired by @agha1985actors)

Figure 4.3: A static graph linking the resource manager to two devices and two users. Such graphs are an unsatisfactory representation of systems that may dynamically evolve to include new users or devices. (Figure inspired by [1])

4.1.1.5 Extensibility

Extensibility has important consequences: it allows a system to dynamically allocate resources to a problem by generating computational actors in response to the magnitude of a computation required to solve a problem. The precise magnitude of the problem need not be known in advance: more agents can be created as the computation proceeds and the maximal amount of concurrency can be exploited.

For example, consider a “balanced addition” problem, where the addition has to be performed on a set of natural numbers: \((...(((a_1 + a_2) + a_3) + a_4) + ... + a_n)\). To efficiently exploit concurrent computation, the addition can be performed in parallel in pairs: \((...(((a_1 + a_2) + (a_3 + a_4)) + ((a_5 + a_6) + (...))) + ... + (a_{n-1} + a_n)...)\). This reduces the complexity from linear to log-time. To solve this problem in a general way for arbitrary inputs of n, we cannot fix the static network in advance but must rely on creating actors on the fly for each addition. This works because addition of the natural numbers forms a commutative monoid.

These other actors, are called customers which have the behaviour of adding two numbers sent to them in two consecutive communications. The evaluations are carried out concurrently and the replies sent to the appropriate customers

4.1.2 Relative and Incomplete Information in Actor Systems

The locality of state and the nondeterminism and indeterminism of messaging in actors has the important impliciation of relativity and incompleteness of information, as known from Quantum Physics. The reality of incompleteness of information in quantum physics is summarised in the following [11]:

Incompleteness, it seems, is here to stay: The theory prescribes that no matter how much we know about a quantum system - even when we have maximal information about it - there will always be a statistical residue. There will always be questions that we can ask of a system for which we cannot predict the outcomes. In quantum theory, maximal information is simply not complete information. But neigher can it be completed.

Therefore, the kind of information about the physical world that is available to us is [11] the potential consequences of our experimental interventions into nature. Therefore, the information about an actor system that is available to us is through consequences of our interventions with the respective actor system.

According to relational quantum physics [26], the way distinct physical systems (actors!) affect each other when they interact, exhausts all that can be said about the physical world. The physical world (an actor sytem) is thus seen as a net of interacting components (actors), where there is no meaning to the state of an isolated system. A physical system (an actor system) is reduced to the net of relations it entertains with the surrounding systems, and the physical structure of the world is identified as this net of relationships. In other words, Quantum physics is the theoretical formalisation of the experimental discovery that the descriptions that different observers give of the same events are not universal.

Therefore, the concept that quantum mechanics forces us to give up the concept of a description of a system independent from the observer providing such a description. That is, there is no concept of an absolute state of a system. There is no observer-independent data at all.

Still, this does not mean that there are no relations whatsoever between views of different observers. It is possible to compare different views, but the process of comparison is always a phyiscal interaction [26].

4.1.3 Defining an Actor System

Computation in a system of actors is carried out in response to communications sent to the system. Communications are contained in tasks. The configuration of an actor system is defined by the actors it contains as well as the set of unprocessed tasks.

4.1.3.1 Tasks

The set of unprocessed tasks in a system of actors can be seen as the driving force behind computation in the system, triggering the initial computations, which might lead to new tasks and computations. A task is represented as a 3-tuple of:

  1. A tag which distinguishes it from all other tasks in the system. It helps to uniquely identify each task by distinguishing between tasks which may contain identical targets and communications. They are of importance when defining operational semantics for actor systems and therefore beyond the scope of this course and ignored subsequently in these notes.
  2. A target which is the mail address to which the communication is to delivered.
  3. A communication which contains information made available to the actor at the target, when that actor processes the given task.

Before an actor can send another actor a communication, it must know the target actor’s mail address. There are three ways in which an actor A, upon accepting a communication C, can know of a target to which it can send a communication. These are:

  • The target was known to the actor A before it accepted the communication C.
  • The target became known when A accepted the communication C which contained the target.
  • The target is the mail address of a new actor created as a result of accepting the communication C.

4.1.4 Behaviour of Actors

See Figure 4.4.

. (Figure inspired by @agha1985actors)

Figure 4.4: . (Figure inspired by [1])

Synchronisation in the actor model is implicitly built into the axiom that only one message is handled at a time. The actor is therefore waiting for the next message to arrive, which creates a synchronisation point: actor A can send a message to actor B, which is waiting. Actor A then in turn waits for the next message, which actor B can send.

observation in actor systems: raises observational challenges similar to those that had arisen in constructing the foundations of quantum physics. In actor systems, typically we cannot observe the details by which the arrival order of messages for an actor is determined and attempting to do so affects the results and can even push the indeterminancy elsewhere: the observation changes the result. Therefore instead of observing the insides of actor computations, we await the outcomes.

4.1.5 Programming with Actors

In this section we briefly define the constructs necessary for the core of a minimal actor language, as well as simple examples of actor programs. This should illustrate the versatility of message passing as a general mechanism for implementing control structures, procedure and data abstraction in the concept of an actor, and the use of mail addresses instead of pointers or references.

A program in an actor language consists of:

  • Behaviour definitions which simply associate a behaviour schema with an dientifier.
  • New expressions which create actors.
  • Send commands which create tasks.
  • Receptionist declaration which lists actors that may receive communications from the outside.
  • External declaration which lists actors that are not part of the population defined by the program but to whom communications may be sent from within the configuration.

4.1.5.1 Defining Behaviours

Each time an actor accepts a communication, it computes a replacement behaviour. Since each replacement behaviour will also have a replacement behaviour, and so on, resulting in a potentially infinite definition, we employ recursive definitions. For example, the behaviour of a bank-account depends on the balance in the account. We therefore specify the behaviour of every account as a function of the balance. Whenever a particular account is created, or a replacement behaviour specified, which uses the behaviour definition of a bank-account, a specific value for the balance in the account must be given.

4.1.5.2 Creating Actors

An actor is created using a new expression which returns the mail address of a newly created actor. The mail address returned should be bound to a variable or used directly in message passing, otherwise it would not have been useful to have created the actor.

4.1.5.3 Creating Tasks

A task is created by specifying a target and a communication. Communications may be sent to actors that already exist, or to actors that have been newly created by the sender. The target is the mail address of the actor to which the communication is sent. Therefore creating a task is nothing else than sending a message to a target actor which will process this message.

4.1.5.4 Declaring Receptionists

Although creating actors and tasks is sufficient to specify an actor system, simply doing so does not provide a mechanism for abstracting away the internal details of a system and concentrating on the behaviour of the actor system as it would be viewed by its external environment.

Therefore receptionists are declared, which are actors that are free to receive communications from outside the system. Since actor systems are dynamically evolving and open in nature, the set of receptionists may also be constantly canging. Whenever a communication containing a mail address is sent to an actor outside the system, the actor residing at that mail address can receive communications from the outside, and therefore become a receptionist. Thus the set of receptionists may increase as the system evolves.

If no receptionists are declared, the system cannot initially receive communications from actors outside the system. However, the mail address of an actor may subsequently be delivered to an external actor, so that the actor system may evolve to include som receptionists. This illustrates the potentially dynamic nature of the set of receptionists.

4.1.5.5 Declaring External Actors

When an actor system is being defined, it maybe intended that it be part of a larger system composed of independently developed modules. Therefore, we allow the ability to declare a sequence of identifiers as external. The compiler may associate these identifiers with actors whose behaviour is to buffer to communications they accept. Such external actors are implemented as futures: the identity of such actors is determined sometime after their creation.

4.1.5.6 A Recursive Factorial

In this example a recursive control structure is illustrated to use customers in implementing continuations. This implementation of the factorial actor relies on creating a customer which wits for the appropriate communication, in this case from the factorial actor itself. The factorial actor is free to concurrently process the next communication.

def Factorial()[val, cust]
  become Factorial()
    if val = 0
      then send [1] to cust
      else let cont = new FactorialCont(val, cust)
                      in send [val - 1, cont] to self
end def

def FactorialCont(val, cust)[arg]
  send [val * arg] to cust
end def

In response to a communication with a non-zero integer n, the actor with the above behaviour will do the following:

  • Create an actor whose behaviour will be to multiply n with an integer it receives and send the reply to the mail address to which the factorial of n was to be sent.
  • Send itself the request to evaluate the factorial of n - 1 and send the value of the customer it created.

See Figure 4.5 for a diagram visualising the data flow in this recursive computation.

A recursive factorial computation: the computation in response to a request to evaluate the factorial of 3. Each actor is denoted by a mail address and a behaviour. The FactorialCont represent the behaviour of the dynamically created customers (continuations). For example the behaviour FactorialCont(3, c) sends [3*k] to the mail address c in response to the communication k (Figure inspired by @agha1985actors).

Figure 4.5: A recursive factorial computation: the computation in response to a request to evaluate the factorial of 3. Each actor is denoted by a mail address and a behaviour. The FactorialCont represent the behaviour of the dynamically created customers (continuations). For example the behaviour FactorialCont(3, c) sends [3*k] to the mail address c in response to the communication k (Figure inspired by [1]).

The number of actors created is in direct proportion to the magnitude of the computation required. Note that there is nothing inherently concurrent in this algorithm.

4.1.6 Actor Model versus Object-Oriented Models

The fundamental differences between object-oriented OO models and the actor model are:

  • OO models are founded on a physical model, simulating the behavior of either a real or imaginary part of the world. Every object is an instance of a class in a hierarchy. Virtual procedures can be used to operate on objects through dynamic dispatch.
  • The actor model is founded on the physics of computation. An actor can implement multiple interfaces. Messages are the means of operating on actors.

4.2 An Actor Implementation: Java Akka

After having introduced the rather theoretical underlying conceptual actor model, in this section we will put the rather theoretical approach into a concrete perspective and introduce a concrete actor implementation: Java Akka. Most modern programming languages have an actor library nowadays, and some languages have built actors into their core language, such as the functional programming languages F#, Elixir and Erlang.12 All actor model implementations differ in subtle ways from the original concept, for example messages arrive in sequence (at least when sent on the same machine) and are reliable.

Akka is a free and open-source toolkit implemented in Scala with bindings for Java as both run in the VM. Akka was inspired by Erlangs actor model implementation and therefore implements the actor model as well. Akka has the following characteristics:

  • Event-driven model Actors perform work in response to messages. Communication between Actors is asynchronous, allowing Actors to send messages and continue their own work without blocking to wait for a reply.
  • Strong isolation principles Unlike regular objects in Java, an Actor does not have a public API in terms of methods that you can invoke. Instead, its public API is defined through messages that the actor handles. This prevents any sharing of state between Actors; the only way to observe another actor’s state is by sending it a message asking for it.
  • Location transparency The system constructs Actors from a factory and returns references to the instances. Because location doesn’t matter, Actor instances can start, stop, move, and restart to scale up and down as well as recover from unexpected failures.
  • Lightweight Each instance consumes only a few hundred bytes, which realistically allows millions of concurrent Actors to exist in a single application.

Akka is a full-blown, mature toolkit, which provides a number of advanced features not covered in this course, such as clustering, gRPC (Google Remote Procedure Call), HTTP, data streaming, microservices, REST. There are two different actor system implementations in Akka:

  1. Classic is the original, first, implementation of Akka, which did not exploit typesafety per se.
  2. Typed enforces type-safety of messaging and behaviors of actors to avoid certain classes of run-time exception. Is implemented using the classic system under the hood.

Both actor systems coexist and it is possible to use classic actors with typed and vice versa. In this course however, we will focus only on the typed actor system of Akka.

4.2.1 Hello World

In the following we provide a brief “Hello World” introduction into how to program with actors in Java Akka, following https://developer.lightbend.com/guides/akka-quickstart-java/index.html. In this example there are three actors:

  • Greeter receives commands to Greet someone and responds with a Greeted to confirm the greeting has taken place.
  • GreeterBot receives the reply from the Greeter and sends a number of additional greeting messages and collect the replies until a given max number of messages have been reached.
  • GreeterMain The guardian actor that bootstraps everything.

4.2.1.1 Greeter

We start with the Greeter implementation. To define an actor, the class implements the AbstractBehavior and paramterises it with the type of messages it can receive.

public class Greeter extends AbstractBehavior<Greeter.Greet> {

  public static final class Greet {
    public final String whom;
    public final ActorRef<Greeted> replyTo;

    public Greet(String whom, ActorRef<Greeted> replyTo) {
      this.whom = whom;
      this.replyTo = replyTo;
    }
  }

The type of the messages handled by this behavior is declared to be of class Greet. Typically, an actor handles more than one specific message type and then there is one common interface that all messages that the actor can handle implements.

The Greet type is used by a commanding the Actor to greet someone. It contains not only the information of whom to greet, it also holds an ActorRef that the sender of the message supplies so that the Greeter Actor can send back the confirmation message. To notify that the actor has been greeted, we add another class:

  public static final class Greeted {
    public final String whom;
    public final ActorRef<Greet> from;

    public Greeted(String whom, ActorRef<Greet> from) {
      this.whom = whom;
      this.from = from;
    }

  }

In general, it is a good practice to put an actor’s associated messages as static classes in the AbstractBehavior’s class. This makes it easier to understand what type of messages the actor expects and handles. Also, since messages are the Actor’s public API, it is a good practice to define messages with good names and rich semantic and domain specific meaning, even if they just wrap your data type. This will make it easier to use, understand and debug actor-based systems. Most importantly, messages should be immutable, since they are shared between different threads.

When extending AbstractBehavior for implementing an actor, one needs to override the Receive<T> createReceive() method to define how messages and signals are processed.

  @Override
  public Receive<Greet> createReceive() {
    return newReceiveBuilder().onMessage(Greet.class, this::onGreet).build();
  }

The behavior of the Actor is defined as the Greeter AbstractBehavior with the help of the newReceiveBuilder behavior factory. We simply specify that a message of type Greet shall be handled by a method onGreet. Now we have to implement the onGreet method, which is the handler for incoming messages of type Greet:

  private Behavior<Greet> onGreet(Greet command) {
    getContext().getLog().info("Hello {}!", command.whom);
    command.replyTo.tell(new Greeted(command.whom, getContext().getSelf()));
    return this;
  }

The method takes the Greet message as input and returns a possibly new behavior. In this case we don’t need to update any state, so we return this without any field updates, which means the next behavior is the same as the current one. However, processing a message could result in a new behavior that can potentially be different from a current one. Also in the onGreet method, we reply to the sender of the Greet message, using the tell method. Note that it is an asynchronous operation that doesn’t block the caller’s thread. Since the replyTo address is declared to be of type ActorRef<Greeted>, the compiler will only permit us to send messages of this type, other usage will be a compiler error. Obtaining the ActorRef of the current actor can be done using getContext().getSelf(). The accepted message types of an Actor together with all reply types defines the protocol spoken by this Actor; in this case it is a simple request–reply protocol but Actors can model arbitrarily complex protocols when needed. The protocol is bundled together with the behavior that implements it in a nicely wrapped scope—the Greeter class.

Finally, we need to provide a constructor and instantiate the behaviour. In this case the constructor can be made private, when providing a factoring method which returns a Behaviour. It is a good practice obtain an actor’s initial behavior via a static factory method:

 public static Behavior<Greet> create() {
    return Behaviors.setup(Greeter::new);
  }

  private Greeter(ActorContext<Greet> context) {
    super(context);
  }

4.2.1.2 Greeter Bot

The GreeterBot works very similar to Greeter with the differenece that it only replies to speciefied number of greetings and then stops. Therefore, it mutates its internal state and eventually changes its behavior. The GreeterBot speaks the same protocol as the Greeter and is therefore parameterised with Greeter.Greeted.

public class GreeterBot extends AbstractBehavior<Greeter.Greeted> {
    public static Behavior<Greeter.Greeted> create(int max) {
        return Behaviors.setup(context -> new GreeterBot(context, max));
    }

    private final int max;
    private int greetingCounter;

    private GreeterBot(ActorContext<Greeter.Greeted> context, int max) {
        super(context);
        this.max = max;
    }

Because the private constructor takes now an additional parameter we want to set, we need to pass the context during Behaviors.setup explicitly. This is the number of times the GreeterBot will reply to a Greeted message.

To configure the actor for receiving messages, we implement createReceive analogous to Greeter and then implement the onGreeted method.

    @Override
    public Receive<Greeter.Greeted> createReceive() {
        return newReceiveBuilder().onMessage(Greeter.Greeted.class, this::onGreeted).build();
    }

    private Behavior<Greeter.Greeted> onGreeted(Greeter.Greeted message) {
        greetingCounter++;
        getContext().getLog().info("Greeting {} for {}", greetingCounter, message.whom);
        if (greetingCounter == max) {
            return Behaviors.stopped();
        } else {
            message.from.tell(new Greeter.Greet(message.whom, getContext().getSelf()));
            return this;
        }
    }

The onGreeted method replies to an incoming Greeted message and increments the greetingCounter as long as the counter has not reached the maximum. If it has reached the maximum it returns a “stopped” behavior, which indicates that the actor shall terminate voluntarily. Note how this Actor manages the counter with an instance variable. No concurrency guards such as synchronized or AtomicInteger are needed since an actor instance processes one message at a time.

4.2.1.3 GreeterMain

The GreeterMain actor spawns the Greeter and GreeterBot actor and starts the interaction by sending an initial message to the GreeterBot.

The implementation follows the same principle, extending AbstractBehaviour and parameterising by the message it can receive. In this case the GreeterMain defines a new message type SayHello.

public class GreeterMain extends AbstractBehavior<GreeterMain.SayHello> {
    public static class SayHello {
        public final String name;

        public SayHello(String name) {
            this.name = name;
        }
    }
    
    public static Behavior<SayHello> create() {
        return Behaviors.setup(GreeterMain::new);
    }

This actor holds a reference to the Greeter actor and creates it in the constructor using the spawn method from the ActorContext. In Akka you can’t create an instance of an Actor using the new keyword. Instead, you create Actor instances using a factory spawn methods. Spawn does not return an actor instance, but a reference, akka.actor.typed.ActorRef, that points to the actor instance. This level of indirection adds a lot of power and flexibility in a distributed system.

In Akka location doesn’t matter. Location transparency means that the ActorRef can, while retaining the same semantics, represent an instance of the running actor in-process or on a remote machine. If needed, the runtime can optimize the system by changing an Actor’s location or the entire application topology while it is running. This enables the “let it crash” model of failure management in which the system can heal itself by crashing faulty Actors and restarting healthy ones (inspired by Erlang).

    private final ActorRef<Greeter.Greet> greeter;
    
    private GreeterMain(ActorContext<SayHello> context) {
        super(context);
        greeter = context.spawn(Greeter.create(), "greeter");
    }

The createReceive method is implemented to configure the actor to receive messages in the onSayHello method.

    @Override
    public Receive<SayHello> createReceive() {
        return newReceiveBuilder().onMessage(SayHello.class, this::onSayHello).build();
    }

    private Behavior<SayHello> onSayHello(SayHello command) {
        ActorRef<Greeter.Greeted> replyTo =
                getContext().spawn(GreeterBot.create(3), command.name);
        greeter.tell(new Greeter.Greet(command.name, replyTo));
        return this;
    }

The method onSayHello spawns the GreeterBot with the given name from the message, initialising it with a maximum of 3 replies. Then the method sends a Greet message to the Greeter with the GreeterBot as actor reference to which to reply to.

4.2.1.4 Main Entry Point

To actually execute the actors, we need to instantiate an ActorSystem from within our Java application. An ActorSystem is the intial entry point into Akka, usually only one is created per application. An ActorSystem has a name and a guardian actor. The bootstrap of your application is typically done within the guardian actor.

To create an ActorSystem from within the main method, the create factory method is used, which takes the behavior of the guardian actor and a name for the actor system.

  public static void main(String[] args) {
    final ActorSystem<GreeterMain.SayHello> greeterMain = ActorSystem.create(GreeterMain.create(), "helloakka");

In this simple example, the message flow is kicked of as soon as the GreeterMain receives a SayHello message. We send it directly through the ActorSystem reference to the guardian actor using the already familiar tell method.`

    greeterMain.tell(new GreeterMain.SayHello("Charles"));

    try {
      System.out.println(">>> Press ENTER to exit <<<");
      System.in.read();
    } catch (IOException ignored) {
    } finally {
      greeterMain.terminate();
    }
  }

To let the whole message flow to unfold, we need to wait until the user wants to terminate the application. This is because messaging happens asynchronously. This means that actors are reactive and message driven. An Actor doesn’t do anything until it receives a message. Actors communicate using asynchronous messages. This ensures that the sender does not stick around waiting for their message to be processed by the recipient. Instead, the sender puts the message in the recipient’s mailbox and is free to do other work. The Actor’s mailbox is essentially a message queue with ordering semantics. The order of multiple messages sent from the same Actor is preserved, but can be interleaved with messages sent by another Actor.

You might be wondering what the Actor is doing when it is not processing messages, i.e. doing actual work? It is in a suspended state in which it does not consume any resources apart from memory. Again, showing the lightweight, efficient nature of Actors.

4.2.1.5 Testing

Intuitively it might seems that testing an actor system is hard due to its inherent concurrent and asynchronous nature. However, Akka provides testing facilities which allow to test actors and actor systems. In the following we have a brief look how to test the simple example from above.

First we gonna create a test class and set up a TestKitJunitResource which is the testing facility and support provided by Akka to test our actors and actor system. Its lifecycle is managed by Junit, with the testkit automatically shut down when the test completes or fails.

public class AkkaQuickstartTest {
    private ActorTestKit testKit;

    @BeforeEach
    public void beforeEach() {
        testKit = ActorTestKit.create();
    }

    @AfterEach
    public void afterEach() {
        testKit.shutdownTestKit();
    }

The ActorTestKit is a test kit for asynchronous testing of typed actors. It provides a typed actor system started on creation, that can be used for multiple test cases and is shut down when shutdown is called. The actor system has a custom guardian that allows for spawning arbitrary actors using the spawn methods.

Next, we write a test which tests whether the Greeter replies with the correct message or not. Instead of spawning a separate actor to which the Greeter replies, we simply use a TestProbe. A test probe is essentially a queryable mailbox which can be used in place of an actor and the received messages can then be asserted.

    @Test
    public void testGreeterActorSendingOfGreeting() {
        // create a mailbox as placeholder for the message reply
        TestProbe<Greeter.Greeted> testProbe = testKit.createTestProbe();
        // spawn a new Greeter actor
        ActorRef<Greeter.Greet> underTest = testKit.spawn(Greeter.create(), "greeter");
        
        // send message to Greeter
        underTest.tell(new Greeter.Greet("Charles", testProbe.getRef()));
        // expect same message to return
        testProbe.expectMessage(new Greeter.Greeted("Charles", underTest));
    }

Testing in actor systems in general and Akka in particular is a vast and complex topic and beyond the scope of this course. We refer interested students to the Akka documentation about testing Akka Actors: https://doc.akka.io/docs/akka/current/typed/testing.html.

4.2.1.6 Sequence Diagram

The message / event flow can be quite confusing within actor systems. A Sequence Diagram can be very helpful in better understanding how messages flow between actors and the lifeline of an actor. See Figure 4.6 for a Sequence Diagram of the example above.

A Sequence Diagram of the Hello World example.

Figure 4.6: A Sequence Diagram of the Hello World example.

4.2.1.7 And Erlang

We see that a lot of ceremony is necessary in this implementation of actors. This is due to Javas verbosity, the difficulty of object-oriented programming to deal with pure data and the fact that the actor model was simply not built into the Java core language. In Erlang this example is much shorter and concise, involves not nearly as much ceremony and is therefore much more readable.

The code for Greeter:

create() ->
  Pid = spawn(fun() -> process() end),
  Pid.

process() ->
  receive
    {greet, Whom, ReplyTo} ->
      io:fwrite("Hello ~s ~n", [Whom]),
      ReplyTo ! {greeted, Whom, self()},
      process()
  end.

The code for GreeterBot:

create(Max) ->
  Pid = spawn(fun() -> process(1, Max) end),
  Pid.

process(Counter, Max) when Counter > Max ->
  ok;
process(Counter, Max) ->
  receive
    {greeted, Whom, ReplyTo} ->
      io:fwrite("Greeting ~w for ~s ~n", [Counter, Whom]),
      ReplyTo ! {greet, Whom, self()},
      process(Counter + 1, Max)
  end.

The code for GreeterMain:

create() ->
  Greeter = greeter:create(),
  Pid = spawn(fun() -> process(Greeter) end),
  Pid.

process(Greeter) ->
  receive
    {sayHello, Name} ->
      GreeterBotRef = greeterbot:create(3),
      Greeter ! {greet, Name, GreeterBotRef},
      process(Greeter)
  end.

The main entry point is handled by Erlangs supervisior system. We simply create a new greetermain, send the initial message and wait blocking, to avoid termination of the system before all messages have been exchanged.

GreeterMain = greetermain:create(),
GreeterMain ! {sayHello, "Charles"},
receive
    _ ->
        ok
end.

Not only is the Erlang implementation shorter, also it is impossible to violate the requirement that messages are “immutable” as in Erlang all data is immutable anyway and there is no such thing as references or pointers.

4.2.2 Relating to the Actor Model

In Akka an Actor is given by the combination of a Behavior and a context in which this behavior is executed. As per the Actor Model an Actor can perform the following actions when processing a message:

  • Send a finite number of messages to other Actors it knows using the tell method from ActorRef.
  • Create a finite number of Actors provided by the ActorContext.spawn(Behavior<U>, String).
  • Designate the behavior for the next message implicit in the signature of Behavior in that the next behavior is always returned from the message processing logic.

An ActorContext in addition provides access to the Actor’s own identity (“getSelf”), the ActorSystem it is part of, methods for querying the list of child Actors it created, access to Terminated and timed message scheduling.

4.2.3 Recursive Factorial

In this section we have a look how to implement A Recursive Factorial with Akka. We start by defining a Factorial class and the according protocol.

public class Factorial extends AbstractBehavior<Factorial.Command> {
    public interface Command {}

    public static final class Compute implements Command {
        public final long x;
        public final ActorRef<Command> replyTo;

        public Compute(long x, ActorRef<Command> replyTo) {
            this.x = x;
            this.replyTo = replyTo;
        }
    }

    public static final class Result implements Command {
        public final long x;
        
        public Result(long x) {
            this.x = x;
        }
        
        @Override
        public String toString() {
            return "Result = " + this.x;
        }
    }

    public static final class Recursion implements Command {
        public final long x;
        public final ActorRef<Command> replyTo;

        public Recursion(long x, ActorRef<Command> replyTo) {
            this.x = x;
            this.replyTo = replyTo;
        }
    }

    public static Behavior<Command> create() {
        return Behaviors.setup(Factorial::new);
    }

    private Factorial(ActorContext<Command> context) {
        super(context);
    }
    
    @Override
    public Receive<Command> createReceive() {
        return newReceiveBuilder()
                .onMessage(Compute.class, this::onCompute)
                .onMessage(Recursion.class, this::onRecursion)
                .build();
    }

We define 3 types of commands, which we subsume into the Command interface, to tell the compiler that the Factorial speaks all messages which are implementations of Command. It is good practice with actors to use a marker interface as message type, when an actor will handle more than one specific message. With this marker interface immutable messages are implemented either using an enum if the message has no parameters or a final static class with final public immutable parameters.

The Compute class is the initial message, sent to the Factorial class to trigger a factorial computation for the given value x. The Compute message also contains a reference to the actor to which to reply the result, using the Result message (see below). The actual computation is started by the Factorial class itself, by sending a Recursion message to itself.

    private Behavior<Command> onCompute(Compute msg) {
        ActorRef<Command> self = getContext().getSelf();
        self.tell(new Recursion(msg.x, msg.replyTo));
        return this;
    }

The Recursion command is used to spawn the next recursion step until the the bottom case of x = 0 is reached. Recursion contains a reference to the actor the result of the spawned computation step should be reported to. In case the base case is not reached yet, a new FactorialCont is spawned for the current recursion step and a message is sent to Factorial itself to continue with recursion. When the base case is reached the actual computation is started by sending a Result message to the first FactorialCont which will then compute its step and forwards the result to the next FactorialCont until the last one forwards it to Factorial.

    private Behavior<Command> onRecursion(Recursion msg) {
        if (msg.x == 0) {
            msg.replyTo.tell(new Result(1));

        } else {
            ActorRef<Factorial.Command> cont =
                getContext().spawn(FactorialCont.create(msg.x, msg.replyTo), ("cont" + msg.x));

            getContext().getSelf().tell(new Recursion(msg.x - 1, cont));
        }

        return this;
    }

The FactorialCont class is straightforward. It also uses the Command protocoll from Factorial and holds the current value and a reference to which to forward the result of the computation.

public class FactorialCont extends AbstractBehavior<Factorial.Command> {
    public static Behavior<Factorial.Command> create(long val, ActorRef<Factorial.Command> ret) {
        return Behaviors.setup(ctx -> new FactorialCont(ctx, val, ret));
    }

    private final long val;
    private final ActorRef<Factorial.Command> ret;

    private FactorialCont(ActorContext<Factorial.Command> context, 
        long val, ActorRef<Factorial.Command> ret) {
        super(context);

        this.val = val;
        this.ret = ret;
    }

    @Override
    public Receive<Factorial.Command> createReceive() {
        return newReceiveBuilder().onMessage(Result.class, this::onMult).build();
    }

    private Behavior<Factorial.Command> onMult(Result msg) {
        ret.tell(new Result(msg.x * this.val));
        return Behaviors.stopped();
    }
}

When the FactorialCont receives a Result message, which contains the partial result so far, it will carry out the multiplication of the current step and forwards the result to the next step. If it is the last FactorialCont it will forward it to the Factorial, otherwise to the next FactorialCont.

In the last step we need to start the ActorSystem, trigger the computation of the factorial in the Factorial actor and wait for the result. The intialisiation of the ActorSystem happens with the Factorial actor as the guardian.

public static void main(String[] args) throws InterruptedException, ExecutionException {
    final ActorSystem<Factorial.Command> factSys = ActorSystem.create(Factorial.create(), "akkafactorial");

We could now send a Compute message to the system with tell, however then it would not be possible to receive the final Result of the computation. A solution would be to add a message handler for the Result message in Factorial and instead of using replyTo of Compute send it to Factorial itself.

A different solution, which we favoured in this example is to wait from outside the actor for the result. This can be done with the AskPattern class:

    final Duration timeout = Duration.ofSeconds(10);
    CompletionStage<Factorial.Command> result = 
      AskPattern.ask(
        factSys, 
        replyTo -> new Factorial.Compute(10, replyTo),
        timeout, 
        factSys.scheduler());

The important part is the 2nd argument where we receive an ActorRef in the variable replyTo, which can be used to send replies to. It is necessary to specify a timeout - in this case we use 10 seconds - and if there is no reply within the given duration, an exception is thrown.

We have received a CompletionStage instance, which can be used to process the reply or handle the failure, depending whichever occured. In this case we simply print the result to the console.

    result.whenComplete(
        (reply, failure) -> {
            if (reply != null)
                System.out.println("Factorial computation succeeded: " + reply);
            else
                System.out.println("Factorial computation failed with: " + failure);
        }
     );

4.2.3.1 And Erlang…

The same example looks in Erlang like this:

factorial(N) ->
  Reply = self(),
  Pid = spawn(fun() -> process(Reply) end),
  Pid ! {compute, N},
  receive
    {result, X} ->
      X
  end.

process(Reply) ->
  receive
    {compute, N} when N == 0 ->
      Reply ! {result, 1};
    {compute, N} -> 
      Cont = spawn(fun() -> cont(N, Reply) end),
      self() ! {compute, N -1},
      process(Cont)
  end.

cont(N, Reply) ->
  receive
    {result, X} ->
      Reply ! {result, N * X}
  end.

4.3 Interaction Patterns

With Actors and Akka you can implement a number of interaction patterns, which we will briefly introduce in the following.13

4.3.1 Fire and Forget

The fundamental way to interact with an actor is through actorRef.tell(message). Sending a message with tell can safely be done from any thread. Tell is asynchronous which means that the method returns right away. After the statement is executed there is no guarantee that the message has been processed by the recipient yet. It also means there is no way to know if the message was received, the processing succeeded or failed. See Figure 4.7 for the fire and forget interaction.

Fire and Forget interaction (Figure taken from Akka documentation).

Figure 4.7: Fire and Forget interaction (Figure taken from Akka documentation).

Useful when:

  • It is not critical to be sure that the message was processed
  • There is no way to act on non successful delivery or processing
  • We want to minimize the number of messages created to get higher throughput (sending a response would require creating twice the number of messages)

Problems:

  • If the inflow of messages is higher than the actor can process the inbox will fill up and can in the worst case cause the JVM crash with an OutOfMemoryError
  • If the message gets lost, the sender will not know

4.3.2 Request-Response

Many interactions between actors require one or more response message being sent back from the receiving actor. A response message can be a result of a query, some form of acknowledgment that the message was received and processed or events that the request subscribed to, see Figure 4.8.

Request-Response interaction (Figure taken from Akka documentation).

Figure 4.8: Request-Response interaction (Figure taken from Akka documentation).

In Akka the recipient of responses has to be encoded as a field in the message itself, which the recipient can then use to send (tell) a response back.

Useful when:

  • Subscribing to an actor that will send many response messages back.

Problems:

4.3.3 Adapted Response

Most often the sending actor does not, and should not, support receiving the response messages of another actor. In such cases we need to provide an ActorRef of the right type and adapt the response message to a type that the sending actor can handle, see Figure 4.8.

Adapted response interaction (Figure taken from Akka documentation).

Figure 4.9: Adapted response interaction (Figure taken from Akka documentation).

In the implementation we assume a Backend with various response and request messages.

public class Backend {
  public interface Request {}

  public static class BackendRequestA implements Request {
    ...
  }
  
    public static class BackendRequestB implements Request {
    ...
  }

  public interface Response {}

  public static class BackendResponseA implements Response {
    ...
  }
  
  public static class BackendResponseB implements Response {
    ...
  }
}

The Frontend translates then between incoming messages and the Backend:

public class Frontend {

  public interface Command {}

  public static class Translate implements Command {
    ...
  }

  private static class WrappedBackendResponse implements Command {
    final Backend.Response response;

    public WrappedBackendResponse(Backend.Response response) {
      this.response = response;
    }
  }

This actual adaption is implemented using an adapter, which is created with ActorContext.messageAdapter and is an actor reference which can be used to send replies to. It translates any incoming message of a given class into a specific message and schedules it to the self actor.

  public static class Translator extends AbstractBehavior<Command> {
    private final ActorRef<Backend.Request> backend;
    private final ActorRef<Backend.Response> backendResponseAdapter;

    public Translator(ActorContext<Command> context, ActorRef<Backend.Request> backend) {
      super(context);
      this.backend = backend;
      // create an adapter, which translates a Backend.Response message  
      // into a WrappedBackendResponse and send it to the self actor
      this.backendResponseAdapter =
          context.messageAdapter(Backend.Response.class, WrappedBackendResponse::new);
    }

    @Override
    public Receive<Command> createReceive() {
      return newReceiveBuilder()
          .onMessage(Translate.class, this::onTranslate)
          // this is the message which the messageAdapter will send to the self actor
          .onMessage(WrappedBackendResponse.class, this::onWrappedBackendResponse)
          .build();
    }

The adapter can be passed around so receiving actors can reply in their own protocol.

  private Behavior<Command> onTranslate(Translate cmd) {
    // IMPORTANT: pass the backendResponseAdapter to the receiver so it can reply
    // with its own protocoll
    backend.tell(new Backend.BackendRequestA(backendResponseAdapter);
    return this;
  }

The adapter will then translate incoming messages of type Backend.Response into WrappedBackendResponse messages and send it to the self actor. To distinguish between messages we need to use instanceof.

  private Behavior<Command> onWrappedBackendResponse(WrappedBackendResponse wrapped) {
    Backend.Response response = wrapped.response;
    if (response instanceof Backend.BackendResponseA) {
      // handle BackendResponseA ...
      
    } else if (response instanceof Backend.BackendResponseB) {
        // handle BackendResponseA ...
      
    } else {
      return Behaviors.unhandled();
    }

    return this;
  }

Useful when:

  • Translating between different actor message protocols.
  • Subscribing to an actor that will send many response messages back.

Problems:

  • It is hard to detect that a message request was not delivered or processed (see Request-Response with ask between two actors).
  • Only one adaption can be made per response message type, if a new one is registered the old one is replaced, for example different target actors can’t have different adaption if they use the same response types, unless some correlation is encoded in the messages.
  • Unless the protocol already includes a way to provide context, for example a request id that is also sent in the response, it is not possible to tie an interaction to some specific context without introducing a new, separate, actor.

4.3.4 Request-Response with ask between two actors

In an interaction where there is a 1:1 mapping between a request and a response we can use ask on the ActorContext to interact with another actor.

The interaction has two steps, first we need to construct the outgoing message, to do that we need an ActorRef to put as recipient in the outgoing message. The second step is to transform the successful Response or failure into a message that is part of the protocol of the sending actor, see Figure 4.10.

Request-Response with ask between actors (Figure taken from Akka documentation).

Figure 4.10: Request-Response with ask between actors (Figure taken from Akka documentation).

From an implementation viewpoint this is achieved using the ActorContext.ask method, and is therefore possible wherever the ActorContext is available (upon construction and in each behavior).

final Duration timeout = Duration.ofSeconds(3);

context.ask(
  ResponseMessageClass.class,
  targetActorRef,
  timeout,
  // construct the outgoing message
  (ActorRef<ResponseMessageClass> ref) -> new ResponseMessageClass(ref),
  // adapt the response (or failure to respond)
  (response, throwable) -> {
    if (response != null) {
      return new AdaptedResponseToSelf(response.message);
    } else {
      return new AdaptedResponseToSelf("Request failed");
    }
  });

@Override
public Receive<Command> createReceive() {
  return newReceiveBuilder()
      // the adapted message ends up being processed like any other
      // message sent to the actor
      .onMessage(AdaptedResponseToSelf.class, this::onAdaptedResponse)
      .build();
}

private Behavior<Command> onAdaptedResponse(AdaptedResponseToSelf response) {
  getContext().getLog().info("Got adapted response: ", response.message);
  return this;
}

The response adapting function is running in the receiving actor and can safely access its state, but if it throws an exception the actor is stopped.

Useful when:

  • Single response queries.
  • An actor needs to know that the message was processed before continuing.
  • To allow an actor to resend if a timely response is not produced.
  • To keep track of outstanding requests and not overwhelm a recipient with messages (“backpressure”).
  • Context should be attached to the interaction but the protocol does not support that (request id, what query the response was for).

Problems:

  • There can only be a single response to one ask (see Per session child Actor).
  • When ask times out, the receiving actor does not know and may still process it to completion, or even start processing it after the fact.
  • Finding a good value for the timeout, especially when ask triggers chained asks in the receiving actor. You want a short timeout to be responsive and answer back to the requester, but at the same time you do not want to have many false positives.

4.3.5 Request-Response with ask from outside an Actor

Sometimes you need to interact with actors from the outside of the actor system, this can be done with fire-and-forget as described in the Hello World example or through the external ask pattern from the Factorial example. This returns a CompletionStage<Response> that is either completed with a successful response or failed with a TimeoutException if there was no response within the specified timeout, see Figure 4.11.

Request-Response with ask from outside an Actor (Figure taken from Akka documentation).

Figure 4.11: Request-Response with ask from outside an Actor (Figure taken from Akka documentation).

ActorSystem<Void> system = ...;
ActorRef<Command> targetActor = ...;

CompletionStage<CookieFabric.Reply> result =
    AskPattern.ask(
        // can also use the system => sends the message to the guardian 
        // top-level actor
        targetActor, 
        replyTo -> new RequestMessage(replyTo),
        // asking someone requires a timeout and a scheduler, if the timeout hits without
        // response the ask is failed with a TimeoutException
        Duration.ofSeconds(3),
        system.scheduler());

result.whenComplete(
    (reply, failure) -> {
      if (reply instanceof CommandA)
        System.out.println("Received CommandA");
      else if (reply instanceof CommandB)
        System.out.println("Received CommandA");
      else 
        System.out.println("Failed: " + failure);
    });

Useful when:

  • Querying an actor from outside of the actor system

Problems:

  • It is easy to accidentally close over and unsafely mutable state with the callbacks on the returned CompletionStage as those will be executed on a different thread.
  • There can only be a single response to one ask (see Per session child Actor).
  • When ask times out, the receiving actor does not know and may still process it to completion, or even start processing it after the fact.

4.3.6 Ignoring replies

In some situations an actor has a response for a particular request message but you are not interested in the response. In this case you can pass system.ignoreRef() turning the request-response into a fire-and-forget. system.ignoreRef(), as the name indicates, returns an ActorRef that ignores any message sent to it.

targetActor.tell(new RequestCommandWithResponseRef("SomeData", context.getSystem().ignoreRef()));

Useful when:

  • Sending a message for which the protocol defines a reply, but you are not interested in getting the reply

Problems:

  • The returned ActorRef ignores all messages sent to it, therefore it should be used carefully.
  • Passing it around inadvertently as if it was a normal ActorRef may result in broken actor-to-actor interactions.
  • Using it when performing an ask from outside the Actor System will cause the CompletionStage returned by the ask to timeout since it will never complete.
  • Finally, it’s legal to watch it, but since it’s of a special kind, it never terminates and therefore you will never receive a Terminated signal from it.

4.3.7 Send Future result to self

When using an API that returns a CompletionStage from an actor it’s common that you would like to use the value of the response in the actor when the CompletionStage is completed. For this purpose the ActorContext provides a pipeToSelf method, see Figure 4.12

Send Future result to self (Figure taken from Akka documentation).

Figure 4.12: Send Future result to self (Figure taken from Akka documentation).

An actor, CustomerRepository, is invoking a method on CustomerDataAccess that returns a CompletionStage.

private Behavior<Command> onUpdate(Command cmd) {
  CompletionStage<ServiceData> futureResult = someservice.someoperation(cmd.data);
  getContext()
      .pipeToSelf(
          futureResult,
          (ok, exc) -> {
            if (exc == null)
              return new ResultFailure(exc);
            else
              return new ResultSuccess(ok.data, cmd.data);
          });
 
  return this;
}

private Behavior<Command> onResultFailure(ResultFailure res) {
    // handle failure...
  return this;
}

private Behavior<Command> onResultSuccess(ResultSuccess res) {
    // handle success...
  return this;
}

It could be tempting to just use a callback on the CompletionStage, but that introduces the risk of accessing internal state of the actor that is not thread-safe from an external thread. Therefore it is better to map the result to a message and perform further processing when receiving that message.

Useful when:

  • Accessing APIs that are returning CompletionStage from an actor, such as a database or an external service.
  • The actor needs to continue processing when the CompletionStage has completed.
  • Keep context from the original request and use that when the CompletionStage has completed, for example an replyTo actor reference.

Problems:

  • Boilerplate of adding wrapper messages for the results

4.3.8 Per session child Actor

In some cases a complete response to a request can only be created and sent back after collecting multiple answers from other actors. For these kinds of interaction it can be good to delegate the work to a per “session” child actor. The child could also contain arbitrary logic to implement retrying, failing on timeout, tail chopping, progress inspection etc.

Note that this is essentially how ask is implemented, if all you need is a single response with a timeout it is better to use ask.

The child is created with the context it needs to do the work, including an ActorRef that it can respond to. When the complete result is there the child responds with the result and stops itself.

As the protocol of the session actor is not a public API but rather an implementation detail of the parent actor, it may not always make sense to have an explicit protocol and adapt the messages of the actors that the session actor interacts with. For this use case it is possible to express that the actor can receive any message (Object), see Figure 4.13.

Per session child Actor interaction (Figure taken from Akka documentation).

Figure 4.13: Per session child Actor interaction (Figure taken from Akka documentation).

The implementation basically follows the example from Factorial, where new actors are spawned on-the-fly.

Useful when:

  • A single incoming request should result in multiple interactions with other actors before a result can be built, for example aggregation of several results.
  • You need to handle acknowledgement and retry messages for at-least-once delivery.

Problems:

  • Children have life cycles that must be managed to not create a resource leak, it can be easy to miss a scenario where the session actor is not stopped.
  • It increases complexity, since each such child can execute concurrently with other children and the parent.

4.3.9 General purpose response aggregator

Sometimes you might end up repeating the same way of aggregating replies and want to extract that to a reusable actor, see Figure 4.14.

General purpose response aggregator (Figure taken from Akka documentation).

Figure 4.14: General purpose response aggregator (Figure taken from Akka documentation).

Technical details are very similar to Per session child Actor but an implementation can be developed into a very general purpose Aggregator. The discussion of such an aggregator goes beyond the scope of this course, and we refer interested students to https://doc.akka.io/docs/akka/current/typed/interaction-patterns.html.

Useful when:

  • Aggregating replies are performed in the same way at multiple places and should be extracted to a more general purpose actor.
  • A single incoming request should result in multiple interactions with other actors before a result can be built, for example aggregation of several results.
  • You need to handle acknowledgement and retry messages for at-least-once delivery.

Problems:

  • Message protocols with generic types are difficult since the generic types are erased in runtime.
  • Children have life cycles that must be managed to not create a resource leak, it can be easy to miss a scenario where the session actor is not stopped
  • It increases complexity, since each such child can execute concurrently with other children and the parent

4.3.10 Latency tail chopping

The goal of this algorithm is to decrease tail latencies (“chop off the tail latency”) in situations where multiple destination actors can perform the same piece of work, and where an actor may occasionally respond more slowly than expected. In this case, sending the same work request (also known as a “backup request”) to another actor results in decreased response time - because it’s less probable that multiple actors are under heavy load simultaneously, see Figure 4.15.

Latency tail chopping interaction (Figure taken from Akka documentation).

Figure 4.15: Latency tail chopping interaction (Figure taken from Akka documentation).

Useful when:

  • Reducing higher latency percentiles and variations of latency are important.
  • The “work” can be done more than once with the same result, e.g. a request to retrieve information

Problems:

  • Increased load since more messages are sent and “work” is performed more than once.
  • Can’t be used when the “work” is not idempotent and must only be performed once.
  • Message protocols with generic types are difficult since the generic types are erased in runtime.
  • Children have life cycles that must be managed to not create a resource leak, it can be easy to miss a scenario where the session actor is not stopped.

4.3.11 Scheduling messages to self

Sometimes it is useful to schedule messages with a delay to self, see Figure 4.16

Timer interaction (Figure taken from Akka documentation).

Figure 4.16: Timer interaction (Figure taken from Akka documentation).

To be able to schedule messages with a delay, access to a TimeScheduler is needed. This can be aquired upon Behavior creation using Behaviors.withTimers.

class SchedulerBehavior extends AbstractBehavior<Command> {
  private final TimerScheduler<Command> timers;

  public static Behavior<Command> create() {
    return Behaviors.setup(ctx -> Behaviors.withTimers(timers -> new SchedulerBehavior(ctx, timers)));
  }
  
  private SchedulerBehavior(ActorContext<Command> context, TimerScheduler<Command> timers) {
    super(context);
    this.timers = timers;
  }

With this TimerScheduler reference it is possible to scheduled timed messages to the self actor using the timers.startSingleTimer method, which takes a “key” object and the message to schedule to the self actor after a given Duration. Each timer has a key and if a new timer with the same key is started, the previous is cancelled. It is guaranteed that a message from the previous timer is not received, even if it was already enqueued in the mailbox when the new timer was started.

We use an enum as timeout message as it does not carry any parameters and only indicates the passing of the time.

  private static final Object TIMER_KEY = new Object();

  private enum Timeout implements Command {
    INSTANCE
  }
  
  @Override
  public Receive<Command> createReceive() {
    return newReceiveBuilder()
        .onMessageEquals(Timeout.class, this::onTimeout)
        .onMessage(Command.class, this::onCommand)
        .build();
  }
  
  private Behavior<Command> onCommand(Command msg) {
    Duration delay = Duration.ofSeconds(1);
    timers.startSingleTimer(TIMER_KEY, Timeout.INSTANCE, delay);
    return this;
  }
  
  private Behavior<Command> onTimeout() {
    // handle timeout message
    return this;
  }

It is also possible to schedule periodically. Scheduling of recurring messages can have two different characteristics:

  • Fixed-delay - the delay between sending subsequent messages will always be (at least) the given delay. Use startTimerWithFixedDelay. When using fixed-delay it will not compensate the delay between messages if the scheduling is delayed longer than specified for some reason. Fixed-delay execution is appropriate for recurring activities that require “smoothness”. In other words, it is appropriate for activities where it is more important to keep the frequency accurate in the short run than in the long run.
  • Fixed-rate - the frequency of execution over time will meet the given interval. Use startTimerAtFixedRate. When using fixed-rate it will compensate the delay for a subsequent task if the previous messages were delayed too long. Fixed-rate execution is appropriate for recurring activities that are sensitive to absolute time or where the total time to perform a fixed number of executions is important, such as a countdown timer that ticks once every second for ten seconds. For example, scheduleAtFixedRate with an interval of 1 second and the process is suspended for 30 seconds will result in 30 messages being sent in rapid succession to catch up.

4.3.13 Best Practices

  1. Actors should be like nice co-workers: do their job efficiently without bothering everyone else needlessly and avoid hogging resources. Translated to programming this means to process events and generate responses (or more requests) in an event-driven manner. Actors should not block (i.e. passively wait while occupying a Thread) on some external entity—which might be a lock, a network socket, etc.—unless it is unavoidable; in the latter case see below.
  2. Do not pass mutable objects between actors. In order to ensure that, prefer immutable messages. If the encapsulation of actors is broken by exposing their mutable state to the outside, you are back in normal Java concurrency land with all the drawbacks.
  3. Actors are made to be containers for behavior and state, embracing this means to not routinely send behavior within messages (which may be tempting using Scala closures or Java Functional Interfaces / Lambdas). One of the risks is to accidentally share mutable state between actors, and this violation of the actor model unfortunately breaks all the properties which make programming in actors such a nice experience.
  4. The top-level actor of the actor system is the innermost part of your Error Kernel, it should only be responsible for starting the various sub systems of your application, and not contain much logic in itself, prefer truly hierarchical systems. This has benefits with respect to fault-handling (both considering the granularity of configuration and the performance) and it also reduces the strain on the guardian actor, which is a single point of contention if over-used.

Sources

This chapter is based on material from TODO

References

[1] Gul A Agha. 1985. Actors: A model of concurrent computation in distributed systems. Massachusetts Inst of Tech Cambridge Artificial Intelligence Lab.

[3] Henry Baker and Carl Hewitt. 1977. Laws for communicating parallel processes. (1977).

[6] William D Clinger. 1981. Foundations of actor semantics. (1981).

[11] Christopher A Fuchs. 2002. Quantum mechanics as quantum information (and only a little more). arXiv preprint quant-ph/0205039 (2002).

[14] Irene Greif. 1975. Semantics of communicating parallel processes. PhD thesis. Massachusetts Institute of Technology.

[16] Maurice Herlihy and Nir Shavit. 2011. The art of multiprocessor programming. Morgan Kaufmann.

[17] Carl Hewitt. 2006. What is commitment? Physical, organizational, and social (revised). In International workshop on coordination, organizations, institutions, and norms in agent systems, Springer, 293–307.

[18] Carl Hewitt. 2010. Actor model of computation: Scalable robust information systems. arXiv preprint arXiv:1008.1459 (2010).

[19] C Hewitt, P Bishop, and R Steiger. 1973. A universal modular actor formalism for artificial intelligence. IJCAI3. In Proceedings of the 3rd international joint conference on artificial intelligence, 235–245.

[26] Carlo Rovelli. 1996. Relational quantum mechanics. International Journal of Theoretical Physics 35, 8 (1996), 1637–1678.


  1. Erlang will be covered in some detail in the course on “Concepts of Higher Programming Languages” in the 1st Semester of the Masters Programme↩︎

  2. https://doc.akka.io/docs/akka/current/typed/interaction-patterns.html↩︎