Draft! This page is a draft - a work in progress!

The heart of an actor Subscribe Pub

In our journey through the land of reactive programming we looked at actors, and remarked that they're one of the best abstractions to deal with the issues surrounding the decoupling required for effective reactive programming and building robust reactive systems.

Let's dissect actors a little more...

Actors and messages

We said that an actor is an object with state, and a mailbox full of messages, which are processed one at a time. The actor can change some local state (write to a database etc) and/or send messages to other actors.

Of course we can write any kind of logic possible, inside an actor, such as call out to a database, other systems etc... within the scope of a single message.

When using actors, we breakup the logical flow into explicit messages and the state of the entire flow is now encapsulated in the actor object and the messages that are now flying around the system.

The messages decouple the processing steps much more than closures or threads - since the entire state of this (and next steps) is now in the messages and possibly some in the actor itself.

Stateless actors

We saw that if we skip all the fluff around types and such, an actor starts to look like this (see compile-able code here):

class MyActor(orderId, notification) extends Actor {
  def receive = {
    case MsgNotify => db ! MsgFindOrder(orderId)
    case MsgOrderFound(order) => db ! MsgFindAccount(order.accountId)
    case MsgAccountFound(order) => email ! MsgSend(account.email, notification)
  }
}

Notice that this actor is stateless: the processing of a new message is independent of the message history... and this actor just transforms a message into another message with a different target... and this is one typical actor pattern: one that decomposes an incoming message into zero or more outgoing messages (addressed to other actors).

If we are to now throw away even more of the fluff around the main piece of logic there, we're left with something like this:

    case MsgNotify => db ! MsgFindOrder(orderId)
    case MsgOrderFound(order) => db ! MsgFindAccount(order.accountId)
    case MsgAccountFound(account) => email ! MsgSend(account.email, notification)

This is in fact the essence of that actor: the message decomposition rules: matches on the left and results on the right.

Using our diesel framework, this type of functionality can be expressed in the following form:

$when this.MsgNotify(orderId, notif) => db.MsgFindOrder(orderId, notif)
$when this.MsgOrderFound(order, notif) => db.MsgFindAccount(order.accountId, notif)
$when this.MsgAccountFound(account, notif) => email.MsgSend(account.email, notif)

Note that this is in some ways simpler:

  • it's a very direct expression of what we want
  • not worried about the communication pattern (! vs !? etc)
  • avoiding the ask pattern

We can quickly mock the other actors:

$when db.MsgFindOrder(orderId, notif) 
=> this.MsgOrderFound({accountId: '1'}, notif)

$when db.MsgFindAccount(order.accountId, notif) 
=> this.MsgAccountFound({email: 'razie@razie.com'}, notif)

The semantics of the execution are roughly similar as well: each message is processed independently, in its own akka actor/asynchronous context. However, there are differences as well, the biggest is that the message is decoupled from the actor and the actor becomes anonymous... this will make no real difference for stateless actors, but it would certainly make a big difference for stateful actors.

Stateful actors

A stateful actor will change some local state, which will impact the processing of the future messages, something like a state machine:

Advanced distributed fabric features

However, there are some benefits over the simple callback model:

  • if the node crashes, the state of the actor can be backed up and then recovered on restart, to the closest approximation
  • also, if the node crashes while processing a message, the system will retry the message on restart (assuming we persisted them)

This is a very important difference from all the previous models, which relied on a state maintained on the original node (the thread or the closures), which have to be available when the replies get there.

It can also be fairly easy to configure more processing power, as well as replication or load balancing etc, at the level of actors, so the db function is completely independently scaled from the email function etc.

Note that the operational and debugging issues are still there:

  • logging is not correlated, messages from different order IDs will interleave their logs

There is also a streams implementation using Akka, see [ http://doc.akka.io/docs/akka/current/scala/stream/index.html akka streams], very interesting to look at.

Now we're cooking!

Of course, there is some extra overhead from storing all those messages, but the glue usually has a very insignificant cost compared to the actual steps that we're trying to glue together. The actor subsystem is also very, very fast.

Of course, persisting the actor state and the messages would add quite a bit more overhead, but... there's no free lunch!

Workflows and rules engines

Workflow engines are typically used to glue asynchronous activities, at the macro level, coordinating and orchestrating worthy services.

Their benefits are quite similar to actors, plus:

  • graphical process designer
  • embedded error handling
  • etc

A good workflow library can also help with logging, as you can include the workflow ID in the logs, so you can easily find them in sequence. One could also add useful debugging features, such as visualizing the progress of a workflow based on logs and archived state as well as breakpoints, replaying troublesome workflows etc.

The workflow support would be fairly similar to the actor example, except the rules describing the message flow could be configuration, like:

Was this useful?    

By: Razie | 2017-05-22 .. 2017-10-01 | Tags: post


See more in: Cool Scala Subscribe

Viewed 131 times ( | Print ) this page.

You need to log in to post a comment!