-
Notifications
You must be signed in to change notification settings - Fork 8
Activation
To understand the concept of activation, one must first understand the fundamental architectural decision underpinning Indigo - dynamic actors, as we have christened it. In just about every actor framework, an actor is structural element that must be defined and explicitly instantiated before being used; conversely, an actor that does not exist cannot receive and process messages. Indigo requires only one of these two constraints - an actor must be defined before it can be used, that is to say its behaviour must be completely prescribed. The physical instantiation of the actor and its subsequent life-cycle is managed by the framework. The actor is loaded into memory when a message is destined for it. At any point, the actor may choose to passivate itself, which will eventually result in the disposal of the actor's state and any associated infrastructure (such as its mailbox). We refer to this trait of an actor as being dynamic. This is similar to the virtual actor concept in the Orleans framework for .NET.
Note: Depending on how one looks at it, an actor is never really created or destroyed - it merely transitions from one state to another. For example, the state of an actor might be persisted to a database. When an actor is messaged, its state is loaded into memory and the actor's behaviour may be exercised by the JVM repeatedly until it is voluntarily passivated, at which point it writes its state back to the database. Because an actor's state transcends the physical actor object on the JVM, the actor lives on. That's the power of the dynamic actor concept, and why Indigo can support an unbounded number of actors.
An actor definition applies to a specific role, in effect being a prototypical definition; it doesn't apply to a specific (future) instance of an actor. To use the metaphor of real actors in a play, a manuscript defines how a particular role should behave, agnostic of any specific actor who might be given the role. So irrespective of how many actors are created for a given role, they will all derive from the same prototypical behaviour. Of course, the practice of multiple actors all doing the same thing would be wasteful, and so the behaviour is defined in terms of two elements: the actor's optional state and the message handed to it. While the role defines the basic behaviour, the identity of an actor is brought about by its key, such that a new instance of an actor is created automatically when a message destined to a new key is observed.
Note: The astute reader will recognise that partitioning actors by an arbitrary key is a sharding mechanism, which is very useful for the parallel processing of large streams of data, as well as distributing application state. It makes Indigo well-suited for running applications in the cloud, where variations in load warrant elastic scalability.
Due to the virtual and dynamic nature of actors, the actor's definition is decoupled from its identity. The Activation is the 'glue' that binds the two together, and is automatically created by the framework when an actor comes into existence on the JVM. The Activation object is a micro-container for the actor instance, also comprising the actor's mailbox and message stash. In effect, the activation is a shim between the actor and the rest of the actor system. The Activation class is one of the most crucial elements of the Indigo framework, topped only by the Actor interface.
While the Activation object serves several purposes, by far the most relied upon is its ability to send and receive messages. In the last chapter, we've only briefly touched on some examples.
The Activation object passed to an actor's act() method use a fluent builder pattern to compose messages. The first step in composing a message is to invoke to(ActorRef), passing the address of the recipient.
The simplest form of message passing is a fire-and-forget tell(). This basically means that the calling actor doesn't care about the outcome of the message, only in that it gets delivered and processed. By calling tell() with no arguments, a null message body will be sent. Conversely, passing a single object to tell(Object) will send a message with the specified body, as in the example below. Assuming a is the Activation instance, this code is sending the message new Add(5) to the singleton actor of role AdderContract.ROLE.
a.to(ActorRef.of(AdderContract.ROLE)).tell(new AdderContract.Add(5));When a request-response interaction style is required, use the ask() method. Like tell, ask can be called with an optional argument, specifying the message body. Calling ask() doesn't immediately send the message - this is deferred until the caller specifies the onResponse behaviour. The example below is a slightly modified version of the test case from the previous chapter. It sends a Get request to the adder actor and prints the response to System.out.
a.to(ActorRef.of(AdderContract.ROLE))
.ask(new AdderContract.Get())
.onResponse(r -> System.out.println(r.body()));Request-response scenarios are typically more complex than a simple ask, as the caller's behaviour will depend on the response, or lack thereof. An application could have soft real-time requirements, and may not be tolerant to a late response from an actor. Such cases should be handled with a combination of await and onTimeout, as shown in the slightly reworked example below.
a.to(ActorRef.of(AdderContract.ROLE))
.ask(new AdderContract.Get())
.await(10_000).onTimeout(() -> System.err.println("Timed out"))
.onResponse(r -> System.out.println(r.body()));The await() method specifies the number of milliseconds that the actor system will allow before invoking a Runnable handler provided to onTimeout().
An actor may, for whatever reason, be unable to successfully complete a request. It could be something as simple as a programming defect, resulting in an uncaught exception. Alternatively, it could be a dependency, such a downstream service, that could fail while an actor is trying to invoke it. At any rate, one shouldn't naively assume that a particular element of a system is infallible.
Handling of faults is done by providing an onFault handler, as shown in the example below.
a.to(ActorRef.of(AdderContract.ROLE))
.ask(new AdderContract.Get())
.await(10_000).onTimeout(() -> System.err.println("Timed out"))
.onFault(f -> System.err.println("Fault reason: " + f.getReason()))
.onResponse(r -> System.out.println(r.body()));Fault handling is an important element of Indigo framework, and will be discussed separately.
The Activation object ensures that at most one of the onTimeout, onResponse or onFault handlers will ever be called. For example, if the receiving actor responds just after the timeout event has been raised, the Activation object will silently discard the response. Furthermore, Indigo guarantees that a callback to a message request will never be interleaved with the call to act, so that the body of the act method will only ever be traversed by a single execution thread at any given time. In other words, if the code inside act() is busy responding to a callback, it will not be given a message to process until the callback completes. Conversely, a callback will not be run if act() is busy processing a new message.
Note: As a minor convenience, the
times()method allows you to send identical messages multiple times without using aforloop. These could betelloraskmessages, and in the latter case, the callbacks will be invoked once per message.
Actors are subject to two life-cycle events - activation, when an actor is brought to live and passivation - when an actor is being suspended. The framework guarantees that both methods will be called exactly once. Furthermore, a strict ordering guarantee is provided, such that the activated() method is be called before the act() method which, in turn, precedes a call to passivated().