Tour of Akka Typed: Message Adapters, Ask Pattern and Actor Discovery
Contents
In the previous article of this series we’ve explored the basics of the Akka Typed API: why it was created and what are its benefits in comparison to the classic Actor API, how to build typed actor systems via protocols and behaviors and how to create typed actors. In this series we’re going to go further down the route of building typed actor systems by looking at fundamental concepts necessary for the interaction between actors.
Let us start by defining the centerpiece actor of this system: the PaymentHandler
. This is the actor that is responsible for handling the requests that are received by the API by retrieving the necessary configuration and then orchestrating the handling through the adequate components.
This time around we will be using the functional style of the typed API to create the actor.
|
|
The API will send us a HandlePayment
message upon which we can retrieve the necessary configuration from our Configuration
actor:
If you still have the first article of this series in mind, you may have anticipated that at this point there will be an issue with our protocol definition and the use of the typed actor API. Let’s have a quick look at the message definitions:
|
|
Indeed, the PaymentHandling
actor does not have the knowledge of the response messages of Configuration
- and in all fairness, it shouldn’t. I’ve always found this aspect of using the classic Akka API with message patterns of type Request-Response / Command-Event to be a bit cumbersome as I find it to be perfectly legitimate to define the response message types (or events, when you’re using that semantic) close to the actor that emits the messages but unfortunately this means that the actors that receive a “foreign” message will not have it as part of their own protocol. This in turn leads to the protocol definition in classic Akka API systems being incomplete, or should I say scattered, in the sense that the message protocols of one actor will always miss some messages sent by other actors.
So how do we work with this in the Akka Typed API? The answer is that those cases need to be made explicit with the use of Adapted Responses.
Adapted Responses
In order for PaymentHandling
to consume responses of the Configuration
actor we’ll need two things:
- to define a message in the protocol of
PaymentHandling
reflecting those responses - to translate the responses from one protocol to the other, which we’ll do using a message adapter
Since in our case there are several possible response messages from Configuration
, it would not be very practical to redefine all of them as part of the payment handling protocol. A simple mechanism is to use a wrapper like so:
|
|
Let’s now go ahead and implement the behavior of PaymentHandling
using adapted responses:
|
|
The part that’s most interesting to us here is the configurationResponseAdapter
:
|
|
This is the mechanism by which we can turn an ActorRef[PaymentHandlingMessage]
into an ActorRef[ConfigurationResponse]
, thus allowing the Configuration
actor to reply to the PaymentHandling
actor and to translate the messages transparently. As shown here, using a wrapper to this effect makes sense when there are several possible responses to translate - for simpler cases, a direct mapping without a wrapper may be enough.
Since we are using the functional style of the Typed Actor API here, the state of the actor is not kept in a mutable data structure but instead passed down from one behavior to the next by virtue of the function definition:
|
|
When returning this behavior, the state can now be altered by calling handle
with different value for the request map.
Note that the choice of using of a Map[MerchantId, HandlePayment]
is a pretty poor one which wouldn’t work in real life (and which I only took to keep the example simple): as soon as subsequent HandlePayment
messages with the same merchantId
are received, chances are that the first values would be overwritten. There are several correct solutions for this:
- using a multi-valued map
- extending the protocol so that each incoming
HandlePayment
message contains a unique request identifier, and modify theConfiguration
protocol to include that request identifier in order to be able to correlate the configuration responses - use the ask pattern which works well for this type of request-response situation where the protocol does not carry the required context to allow for correlation
Let’s have a look at the ask pattern in more detail which also allows us to map the response.
The ask pattern
The ask pattern allows two actors to interact in such a way that there’s a 1 to 1 mapping between the request and the response. Since there’s no guarantee that the responding actor will in fact ever respond, the ask pattern requires to define a timeout after which the interaction is considered to fail. What happens under the hood is that a TimeoutException
is thrown.
In the following implementation of the PaymentHandling
we no longer keep track of the in-flight payment requests as there’s now a direct mapping for the interactions with the configuration service. Instead we extend the internal protocol of the PaymentHandling
actor to carry the information we require:
|
|
Our actor implementation now becomes:
|
|
Again the really interesting bit of code here is the invocation of ask, which for the Scala API is a function with 4 parameter lists (3 explicit and one implicit):
|
|
- the first parameter list takes the target to which we want to send the request, in our case an
ActorRef[ConfigurationMessage]
. - the second parameter list takes a function that constructs the request to be sent given an actor reference to reply to. We could have defined the function inline as well, but for the clarity of the example it is defined separately first and then passed as a function reference
- the third parameter list takes a function that evaluates the result of a
Try
and turns it into a message that will be sent to the requesting actor. As such, it needs to be part of the protocol that the actor understands
Whilst this method signature might look a bit cumbersome at first, I think that it is a really good move on the part of the Akka team as it entirely eliminates a source of mistakes related to ask
returning a Future
in the classic Actor API, which made it possible to mistakenly close over mutable state of the actor.
As we now have retrieved the configuration we can proceed to contacting the right payment processor and ask it to perform the payment. For this purpose, let’s have a look at actor discovery.
Discovering actors with the Receptionist
With a name that could have been given to a Matrix character, The Receptionist allows you to get typed actor references given a key. If you’re familiar with the classic Actor API then this is what replaces the ActorSelection
.
The way in which the discovery mechanism works is pretty straight-forward — it simply acts as a registry:
- each actor that wants to become discoverable needs to register itself by sending a
Register
message to theReceptionist
actor and specifying aServiceKey
- any actor that requires a reference to a discoverable actor can query the
Receptionist
using aFind
message or it can subscribe to updates using aSubscribe
message
In our example, we don’t want to have to query the whereabouts of our processor actors for every request, therefore we’ll be using the subscription mechanism instead.
Let’s start by fleshing out a really simply processor and registering it with the Receptionist
when it starts up. To make the processors even more pluggable, they will share the same protocol:
|
|
We can now start with scaffolding the first payment processor for credit cards:
|
|
As described earlier on, this process is quite simple: whenever the CreditCardProcessor
is started it will register its actor reference with the Receptionist
.
Note that the ServiceKey
takes a type parameter, which is supposed to be the type of the protocol understood by the registered actor.
Next, we need to subscribe the PaymentHandling
actor to updates of the Receptionist
so that we are made aware of all the available processors:
|
|
Note that since the Receptionist
will return a Listing
message, we need to use a message adapter coupled with an internal protocol message (AddProcessorReference
) to be able to understand the update.
At this point we now have a set of Listing
’s at our disposal which we can use to send off the message to the right processor using the configuration (this step isn’t shown in the example, but you can imagine that given the right configuration this should be rather simple).
Concept comparison table
As usually in this series, here’s an attempt at comparing concepts in Akka Classic and Akka Typed (see also the official learning guide):
Akka Classic | Akka Typed |
---|---|
- | context.messageAdapter |
ask / ? |
context.ask |
ActorSelection |
receptionist |
And this is it for the second article of this series! You can find the source code of this article here. |