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:
// the Actor trait (AbstractActor class in Java) is the entry point for using the API class OrderProcessor extends Actor { // the receive method is where messages are processed def receive: Receive = { case order @ OrderProcessor.ProcessOrder => // the actorOf method spawns a new child of the invoking actor val connection = context.actorOf( BankConnection.props(order.bankIdentifier) ) // the ! method stands for send (in a fire-and-forget fashion) connection ! BankConnection.ExecuteOrder(order) } }
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:
// companion objects are a common place to keep message definnition in Scala // in Java, you'd use static classes to achieve the same effect // for clustered or persistent systems, messages are defined using a proper serialization mechanism such as protobuf object OrderProcessor { sealed trait Command final case class ProcessOrder(bank: BankId, fromAccount: AccountId, toAccount: AccountId, amount: Amount) extends Command }
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:
Loving how Windows Subsystem for Linux (WSL) keeps getting better. A dual purpose machine is almost there.
— 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:
- 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:
sealed trait ConfigurationMessage final case class RetrieveConfiguration(merchantId: MerchantId, replyTo: ActorRef[ConfigurationResponse]) extends ConfigurationMessage sealed trait ConfigurationResponse final case class Configuration(merchantId: MerchantId, ...) extends ConfigurationResponse final case class ConfigurationNotFound(merchanId: MerchantId) extends ConfigurationResponse
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:
class Configuration extends AbstractBehavior[ConfigurationMessage] { // ... }
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.
SEE ALSO: Akka anti-patterns: Too many actors
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.
Extending the AbstractBehavior
trait requires us to implement the onMessage
method which returns a Behavior
:
// the AbstractBehavior trait is the entry point for using the object-oriented style API class Configuration(context: ActorContext[ConfigurationMessage]) extends AbstractBehavior[ConfigurationMessage] { // the mutable state here holds the configuration values of each merchant we know about var configurations: Map[MerchantId, MerchantConfiguration] = Map.empty // the onMessage method defines the initial behavior applied to a message upon reception override def onMessage(msg: ConfigurationMessage): Behavior[ConfigurationMessage] = msg match { case RetrieveConfiguration(merchantId, replyTo) => configurations.get(merchantId) match { case Some(configuration) => // reply to the sender using the fire-and-forget paradigm replyTo ! ConfigurationFound(merchantId, configuration) case None => // reply to the sender using the fire-and-forget paradigm replyTo ! ConfigurationNotFound(merchantId) } // lastly, return the Behavior to be applied to the next received message // in this case, that's just the same Behavior as we already have this } }
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 Behavior
).
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:
class ConfigurationSpec extends ScalaTestWithActorTestKit with WordSpecLike { "The Configuration actor" should { "not find a configuration for an unknown merchant" in { // define a probe which allows it to easily send messages val probe = createTestProbe[ConfigurationResponse]() // spawn a new Configuration actor as child of the TestKit's guardian actor val configurationActor = spawn(Configuration()) // send a message to the actor under test with the probe's reference as sender configurationActor ! Configuration.RetrieveConfiguration(MerchantId("unknown"), probe.ref) // expect a certain type of message as response. there are many different ways to retrieve // or to expect messages val response = probe.expectMessageType[Configuration.ConfigurationNotFound] response.merchanId shouldBe MerchantId("unknown") } } }
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 descendents 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 /user
.
We could just go ahead and create the Configuration
actor as a child of the /user
guardian, but that wouldn’t be a good solution going forward – after all, our actor will be part of a system of actors that interact with one another, and therefore it would make sense for those actors to have a way to be coordinated should a problem occur. The reason for this is that 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.
If we would create the Configuration
actor as a child of the /user
guardian, then the default decision-making would be applied in case of crash – the Configuration
actor (or any of its siblings) would simply be restarted in case of a crash. Now whilst that behavior works well in quite a few cases, it might just be that restarting blindly in all cases won’t do for some of the actors we plan on creating. As such, in order to be able to control supervision, we are going to introduce a PaymentProcessor
actor that will be the parent of all the component actors we will create:
The 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 Behavior
:
object PaymentProcessor { def payment() = Behaviors.setup[Nothing] { context => context.log.info("Typed Payment Processor started") context.spawn(Configuration(), "config") Behaviors.empty } }
The 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 (/user/payment/config
).
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:
object Configuration { def apply(): Behavior[ConfigurationMessage] = Behaviors.setup(context => new Configuration(context)) }
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 top-level /user/payment
actor. Fortunately, there’s a factory method of the ActorSystem
that expects to be passed a guardian behavior:
object Main extends App { override def main(args: Array[String]): Unit = { ActorSystem[Nothing](PaymentProcessor.payment(), "typed-payment-processor") } }
And that’s it! If we now run our application, we’ll see the PaymentProcessor
starting:
[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:
This is it for this first part of exploring the Akka Typed API! You can find the source code of this article here.
This post was originally published on Manuel Bernhardt’s blog.
The post Tour of Akka Typed: Protocols and Behaviors appeared first on JAXenter.
Source : JAXenter