Tour of Akka Typed: Protocols and Behaviors
Update 18.08.2019: corrected actor hierarchy and default supervision strategy
Ukrainian translation / Chinese translation
In this series we are going to explore Akka Typed, the new Akka Actor API that brings significant advantage over the classic one. Akka Typed is ready for production since April and although the API is still marked as may change I think it is a good time to look into it and to learn what’s new.
If you’re not familiar with the Akka Actor API, don’t worry — this series is intended to be understandable even if you do not. If you are familiar with the Actor API then this series will help you understand and learn how to work with Akka Typed.
Why Akka Typed
The actor model has proven itself to be a powerful abstraction when it comes to build real-world, fault-tolerant, concurrent and distributed systems. It is based on the notion of message being passed between individual actors that understand these messages and react to them. Fault-tolerance is provided via hierarchical supervision: actors can create child actors which they monitor for failure an can restart or recreate if necessary, such that parts of an actor system (or the system as a whole) are capable of healing themselves after crashes.
The classic Akka actor API embodies these principles by providing a very simple set of methods to process incoming messages, send messages and create children:
This model and API have quite an advantage over threads: the handling of messages by an actor is guaranteed to happen sequentially, the state of an actor can only be altered by the actor itself and as such it is possible to reason about what happens much more easily than with shared state accessed concurrently.
At the same time, there are a few limitations to this API, some of the most common ones being described in the Akka anti-patterns series.
The main issue - and I’ve seen this being the case in quite a few larger Akka projects over the years - is that the API is making it difficult to scale and maintain larger actor systems as they grow. This is because the API does not enforce a “protocol-first” approach. Indeed, one of the first things you learn about in the introductory Akka training course is to clearly define your protocol in terms of handled messages as well as to use the full path (
OrderProcessor.ProcessOrder) to access messages. Coming back to the example above, this is how the protocol of the
OrderProcessor actor looks like:
That is, this best practice will only get you that far: even if you carefully use it everywhere, there is still one major problem. Indeed, nothing prevents you from sending a message to an actor that cannot handle it. An actor’s
receive method will accept any message, and the default behaviour will be to pass those unhandled messages to the
unhandled method of an actor, which logs them by default — if the logging of unhandled messages is configured correctly. This can be the source of extreme frustration to newcomers as you litteraly can’t see anything going wrong and yet your system does not work.
Going one step further, there is no supporting mechanism to help you with evolutions of the protocl over time. This is to say that introducing new messages may provide to be quite difficult as it is always possible to forget their processing in one place or another. Tests will help, of course, but unless you setup advanced log filters for tests that verify that no unhandled messages have been logged, you’re still at risk of missing one place or two.
This is where the Akka Typed API comes in. This API is designed to be “protocol-first”: you no longer have a choice but to spend at least a little bit of time thinking about the messages each actor can deal with. Unlike the classic API where following this best practice is optional, you need to formalize the set of handled messages during implementation.
There’s one thing I’d like to stress at this point, after having seen a fair share of real-world Akka systems: the intent of Akka Typed is not merely to make sure that messages get declared in a structured way and to ensure that a few unhandled messages aren’t missed. Instead its aim is to lead you to really think about the system design upfront. With the right set of actors at the right granularity communicting with the right message patterns, it is possible to build very powerful systems that are, at their core, still quite simple — or, should I say, as simple as the domain permits. Unfortunately what I’ve observed many times over is that people tend to overdo the “many actors, many messages” part and end up with unnecessary complexity which is hard to get rid of afterhand. As Martin Thompson says:
Identifying unnecessary complexity is one of the most important skills in software development.— Martin Thompson (@mjpt777) June 11, 2019
Let’s build a payment processor
For this article series we’re yet again going to use the example of a payment processor (as in the Tour of Akka Cluster series - this domain just never gets old (and also I happen to have quite a bit of experience with it, Akka being well-suited for building high-volume, low-latency transaction systems).
Our Payment Processor is capable of handling payments for multiple types of payment methods: various credit cards (Visa, Master Card, American Express, etc.), SEPA payments, Apple Pay, Google Pay, Amazon Pay, PayPal - you name it. It supports a variety of payment flows with different validation mechanisms, recurring payments and many more options of the like.
In order to deal with such a versatilty in business requirements, our system is divided in several components so as to make it possible to easily add new payment methods:
!(/wp-content/processor-components.png “The initial components of the Payment Processor)
- API: this is the entry point to our payment processor. It handles authentication, supports multiple formats and dispatches the request to the right downstream components. For the purpose of this article series, we’re going to keep the implementation very simple.
- Payment Handler: this key component understands the core payment requests. Based on the information that it retrieves from the configuration component, it then orchestrates the handling of a payment request which can have several steps (validation, execution, etc.).
- Configuration: this component stores the configuration associated with API users as well as with the entities allowed to request payments (in domain slang, that’s a merchant).
- Payment processors: this family of components is responsible for executing payments. In our example we will only feature a simple Credit Card Payment Processor, in a real system there’d be many more of these components. The processors also typically communicate with more downstream components or third-party systems but for the sake of complexity we will not show any of this here.
Note that in a real system there’d be more concerns than modeled here - for example, we don’t talk about registering payment methods with our processor at all - but for the purpose of introducing Akka Typed, this should do.
Protocols in Akka Typed
As explained at length earlier on, protocols matter, and Akka Typed makes it possible to express them (or at least, to some extent — to the very least, much more so than with the classic Akka API).
“But what’s a protocol?”, you ask. “Isn’t that just messages?”. There’s a little bit more to it. To put it simply, I would define a protocol as a set of messages exchanged between two ore more parties in a particular order and combination. There’s a variety of protocol families (check out the OSI model), you are likely familiar with famous protocols such as TCP or HTTPS. In our case, we’re operating at the application layer. You can think of protocols as APIs on steroids: whilst APIs only describe individual calls (including parameters, request contents and response contents), protocols describe how calls interact with one another in order to reach a desired target state of the systems in communication.
In the Typed Actor API, protocols can be expressed in terms of classes and typed actor references. Let’s take the example of a simple protocol that lets us retrieve configuration data from the configuration component:
This example follows the request-response message pattern. To learn more about message patterns (and reactive system design in general) check out the Reactive Design Patterns book.
If you have already used the classic Akka actor API, you will notice two major changes to how this pattern is implemented. First off, the sender address is included in the message definition. In the classic API this was handled transparently by Akka, which was capturing the actor reference of the sender of a message and allowed to reply to it by sending a message to the
sender(). Second, and most importantly, the
ActorRef is now typed: it refers to a particular type of message that the sender can understand. In our case, we are using traits (such as the
ConfigurationResponse trait) in order to allow the sender to deal with more than one type of response.
In order to understand why this matters and how this key change enables Akka Typed applications to be safer and easier to evolve than the classic variant, we need to have a look at the Actor definition. Say we wanted to implement the
Configuration actor. One way of doing it would be the following:
What you notice here is that we’re defining a class that inherits from the
AbstractBehavior trait, which takes a type parameter. This way, we declare that the
Configuration actor is only understanding messages of type
ConfigurationMessage — in other words, this makes it possible for the compiler to statically check whether the recipient of a message can indeed handle the message it is being sent.
Note that in the example above we are using the object-oriented style of declaring an actor in this example - we’ll look at the functional style later.
Implementing our first typed actor
We’ll start with building out a fairly simple version of the
Configuration component which should be capable of retrieving (and later also storing) merchant configuration. We’ll continue using the object-oriented style as we started using it earlier — if you have used the classic actor API before this style should be fairly familiar.
AbstractBehavior trait requires us to implement the
onMessage method which returns a
At first sight, this implementation looks fairly similar to the example of the classic actor API in the beginning of this article. We override a message and match the message we receive and then do something as a result.
The difference here is that we now return a
Behavior. The behavior of an actor when it receives a message is defined by Hewitt et al to be one or more of the following actions:
- it can send one or more messages to other actors
- it can create new child actors
- it can specify a (possibly different) behavior to be applied to the next message
In the Akka Typed API, a
Behavior is both responsible for processing a message as well as for indicating how the next message should be handled, which it can do so by returning a
Behavior. If nothing changes (as in the example above) the same
Behavior can be returned (which is why we return
this here, which in the case of the object-oriented API makes sense as the instance of the actor class implements a
We will talk more about Behaviors throughout the series. They’re one of the essential building blocks of Akka Typed and as we will see later, they can easily be composed and tested.
Talking about testing, the actor implemented above can easily be tested using the Typed Akka TestKit. In combination with ScalaTest, this is how a test case can be setup:
Supervising and starting the actor
Actors can’t run alone in isolation — they’re part of an Actor System which is the environment which allocates the resources and provides the overall infrastructure for actors to exist and to interact.
Within the Akka Actor System, each actor is the child of another actor. The actor at the very top of that hierarchy is called the root guardian (
/), its direct descendants are the user guardian (
/user) for actors created in userspace and the system guardian (
/system) for actors created and managed by Akka. Therefore the path of all the actors that we’ll be creating starts by
With Akka Typed there is an important difference to the classic API as to how the user guardian is handled. In the classic API, Akka provides a user guardian actor. In the Typed API, it is up to the user to provide the behavior of the user guardian. In other words, it is our job as application developer to implement the behavior of the user guardian and think a little about how it should behave.
In order to create our
Configuration actor, we could just go ahead and pass it to the
ActorSystem to act as our user guardian, but that wouldn’t make a lot of sense given that we also want to create other actors and there’s no reason that the
Configuration actor should supervise them all. Furthermore, in the actor model, parental supervision (i.e. the hierarchy of actors) goes hand in hand with failure handling: the parent of an actor is responsible for deciding what to do should a child actor crash (which it does if it throws an exception), and therefore the grouping of actors directly influences how crashes can be managed. As such we should be using a dedicated parent actor that deals with supervising the actors of our application so that it can also decide how to deal with the failure of its child actors. With the Akka Typed API, the default supervision strategy is to stop a failing child actor (this, too, is an important difference from the classic API, where actors would get restarted). With our own user guardian actor we can react differently to different types of failure causes (exceptions) and make different decisions as to how the failure should be handled. Therefore we will introduce a
PaymentProcessor actor that will be the parent of all the component actors we will create and that will be acting as the user guardian. In terms of actor paths, this is how our simple hierarchy will look like:
PaymentProcessor actor in itself doesn’t have much to do, except for creating the child
Configuration actor when it is started. It has no state and does not need to handle messages. We’ll use the functional style of the Typed Actor API to implement it so instead of extending a trait we create a function that returns a
Behaviors.setup method is the entry point for creating
Behaviors. It provides an
ActorContext which we’ll use to log the fact that the processor has started and to create the first
Configuration child actor via the
spawn method. If you’re not familiar with Scala, the first argument passed to the method will call the
apply() method of the
Configuration companion object (see below) which itself exposes a
Behavior. The second argument is the name of the actor, which is also used in the path (
Notice that we call
setup[Nothing] — indeed, the
PaymentProcessor actor is excepted to handle no message.
In order to spawn a
Configuration child actor, we need a
Behavior. Passing in a new instance of the
Configuration class itself, which is a subclass of
AbstractBehavior won’t do — we need to encapsulate that
Behavior in the
setup Behavior like so:
Now that the supervisor is in place, all we need in order to start things up is an
ActorSystem which we’ll instruct to create our user guardian actor. Akka provides a factory method of the
ActorSystem that expects to be passed a user guardian behavior:
And that’s it! If we now run our application, we’ll see the
[info] Running io.bernhardt.typedpayment.Main [INFO] [07/10/2019 09:36:42.483] [typed-payment-processor-akka.actor.default-dispatcher-5] [akka://typed-payment-processor/user] Typed Payment Processor started
In order to see the
Configuration actor do something we will need another actor for it to interact with, which we’ll create in the next article of the series.
Concept comparison table
At the end of each article we’ll have a little table that attempts to map familiar concepts from the classic API to the Typed API (this isn’t meant to be a strict mapping, think of it more as a quick way to be able to relate to the Akka Typed API if you are familiar with the classic Akka Actor API).
Here’s the one from this article (see also the official migration guide):
|Akka Classic||Akka Typed|
|default supervision strategy decision: restart child actor on failure||default supervision strategy decision: stop child actor on crash|
This is it for this first part of exploring the Akka Typed API! You can find the source code of this article here.