Actor.Messages.As.Methods

One of the best expedient to explain the interactions between Actors is thinking as method calls. It is useful to look at in this way during the design phase because it helps to better structure the code.

A method is identified by the name, the arguments and the return value. The first describes the operation, the second the data necessary to complete it and the last one the result.

The method is also defined in a class or and interface that defines its context.

class Contact {
  public boolean addNumber(String number) {
    ...
  }
}

For example a method without name that takes a string and returns a boolean can be very generic, while the method addNumber is more specific. The same method name can also have different meaning if defined in a different class.

A message in the Actor model is an immutable class that is sent to an actor. The concept is the same of method: the class name is the method name and the properties are the method arguments. The actor is context of the message like the class is for the method.

Method Name      = Message Class Name
Method Arguments = Message Class Properties
Method Class     = Actor

The message response is similar to the return value but contains also information about the origin of the message (operation and actor).

Four lessons

  1. The method name should be meaningful, the message name should be the same.
  2. There are generic methods (toString), there are generic messages (PoisonPill). All the specif messages to an actor should be used only by the actor (into the companion object).
  3. The method should do one thing, the actor should treat a message in the same way. Avoid to use parameters meaning different operations, like boolean switcher. Create instead another message as you would create another method.
  4. The result of a message tells also the origin of the messages. Avoid generic or shared messages, use actor specif result messages.

Actor.Types

The actor in the actor model is defined in Wikipedia as “the universal primitives of concurrent computation”. This definition places actors at the same level of language primitives like if, for, while, etc… in a concurrency context.

Like a primitive, an actor can be used to solve the problems in many way and, like the name suggests, can act a different role that is specific to solve the problem.

Here a not exhaustive list of type of possible types of actor.

Actor as Worker

The actor acts as worker of a specific operation, it has not state and it is specialized to solve the problem.

class Worker extends Actor {
  def receive = {
    case Operation(data) => ...
  }
}

The same operation can be parallelized creating many instances of the same actor with a router (see the documentation).

val router: ActorRef =
  context.actorOf(RoundRobinPool(5).props(Props[Worker]),"router")

The worker runs in a different context from the invoker (the component that has sent the operation) and the other workers. This isolation is used for obtaining parallelism but it also has benefit to separate blocking or failing operations.

Mapping Resources

When a worker (or a groups of workers) maps an external resource, like a connection pool, it creates a protection from the rest of the application. This protection can isolate an external blocking resource if the worker runs in a different thread pool or dispatcher (link to the documentation).

worker-dispatcher {
  type = Dispatcher
  executor = "fork-join-executor"
  throughput = 100
}

In this way only the worker thread is blocked and not the rest of the application.

The dispatcher (thread pool) can be easily associated to a router.

val router: ActorRef =
  context.actorOf(
     RoundRobinPool(5).props(Props[Worker])
     .withDispatcher("my-dispatcher"), "router")

In case of a pool of resources it is possible to match the size of the pool with the number of workers (connection pool of 10 = a router of 10 actors).

Failure of a Worker

The isolation of the worker can be useful to handle failures without the risk of propagation to the rest of the application. The worker can for example implement a retry logic or a circuit breaker or can benefit of the supervisor model, escalating the failure to the parent actor (see the documentation).

Domain Actor

The domain actor represents an instance of a single domain object like for example a person identified by first and second name. The life cycle of the object and the actor is the same, as is the status. The actor messages are the way the object interacts with the rest of the world.

class Person(firstName: String, secondName: String) extends Actor {
  var status = Status()
  def receive = {
    case Create(status) => ...
    case Read() => ...
    case Update(change) => ...
    case Delete() => ...
  }
}

An example of domain actor is the one that maps a single entity, like a row in a database table. Due to the nature of the actor all the operations on the entity are atomic, so this model can be adopted to implement transaction when the storage system doesn’t support it.

The domain actor status is important because it is the entity status and must be protected to not lose it in case of failure. So linking the actor to a storage system is a natural consequence and can be done synchronizing when a change occurs. In this way the actor can crash, can be stopped and restarted without loosing any data.

An actor can persist its status storing all the events that change it. This is know as the event sourcing model and it has been implemented into the Akka Persistence module.

class Person(firstName: String, secondName: String) extends PersistentActor {
  def persistenceId = firstName + "-" + secondName
  val receiveCommand: Receive = {
    case create: Create => persist(create) { event => ... }
    case Read() => ...
    case update: Update => persist(update) { event => ... }
    case delete: Delete => persist(delete) { event => ... }
  }
  val receiveRecover: Receive = {
    case Create(status) => ...
    case Update(change) => ...
    case Delete() => ...
  }
}

The actor persists the event before to update the internal status. This operation is not needed in case of read operations. The persistence actor is identified by the persitenceId that is used as key during the storing and recovery process.

Event Sourcing vs Status Persistence

The vantage of an event sourcing model is the possibility to rebuild the internal status of the actor from the events have contributed to build it. During this process the actor may have changed its behavior that can differ to the initial one. Replacing only the status would not help in this case.

Request Actor

The request actor is an actor created to satisfy a single request. Its status represents the initial request and the progress. A new actor is created for each request and when the request is completed the actor is stopped.

class RequestActor(<request parameters>) extends Actor {
  val requestProgress = ...
  override def preStart() {
    <start interacting with the rest of the system>
  }
  def receive = {
    case MessageFromSystem() => ...
    case LastMessageNeeded() =>
      <send request response back>
      context stop (self)
  }
}

Binding a request to an actor finds vantages in the status. The actor can track the progress, reacts to failures, implements retry logic and can make decisions that influence the result of the request.