Actors using Scala & Akka — Part 2 : AskPattern and SpawnProtocol#
This is second post in a series of “Actors using Scala & Akka”. In Part 1, we saw how to create new actors and modify their state by sending messages.
In this articles we will take a look at how to get a reply from actors (i.e.Ask Pattern) and spawning user actors (Spawn protocol) instead of system actors.
Before we jump into ask pattern, we should have basic understanding of some scala concepts
Futures
Execution Context
Implicit parameters
If you are already familiar with scala, feel free to jump to “Ask Pattern” Section.
Futures in scala are similar to Task<T> in C#, a Promise in Javascript or a CompletableFuture<T> in Java. Their objective is to help developers write asynchronous code. A Future<Int> represents a value which will be ready in future when the future is completed. A future may complete with a successful value or fail with an exception. Futures are ideally returned from methods which perform an async operation such as network IO, file IO, etc. This is an evolved design compared to callbacks which were difficult to compose. Since futures are “return values” instead of method parameters, it is easy to chain multiple futures together (compose) and also to distribute across the application.
It is important to know about Future because when you send a message to an actor and expect a reply, even though actor sends a reply of type T, what you will get will always be a Future[T]. This is because message based communication is asynchronous & non-blocking.
Callbacks are a way to tell the program what you want to do after an async operation is complete. When you use a callback based api, you lose control over on which thread or thread-pool the callback will be executed.
When you use futures, you have this control. maping & flatMaping over a Future is like attaching a callback to a future and it requires providing an ExecutionContext. This execution context can be powered by a single thread or any of the various types of thread pools. In other words, whatever you write in the map or flatMap will be executed by the execution context provided by you.
Scala provides an out of the box ExecutionContext at scala.concurrent.ExecutionContext.Implicits.global and it does a good job at doing async work as well as blocking code when used appropriately. It is recommended to use different thread-pools (and execution contexts) for logically different areas of application. You can read about the global execution context in the official documentation.
Scala has this feature where you can define a parameter list as “implicit” and it will be passed by compiler automatically to the function, if an implicit variable of the required type is present in the caller’s scope.
Let’s see an example
sendData is a method which accepts a parameter data, of type Any. Any is a “super type” of all types in scala so you can pass anything to sendData. This method has one more parameter list which begins with implicit and requires a parameter of type Serializer. Looking at the body of method, we can see that method body can access serializer argument in the same way it can access data argument. When we call this method without serializer, it fails to compile as seen in the screenshot above.
We can open another pair of braces and pass in the serializer like any other parameter and it would compile and work just fine.
Here, I have declared a variable and marked it as implicit and now I don’t need to pass it anymore. Since it has the required type and is current scope, compiler does the job for me. If I enable the magic view in IntelliJ by pressing OptionCTRLShift++, I can see the implicit parameter being passed!
I talked about implicit parameters because .map and .flatMap requires passing an ExecutionContext implicitly. There is a lot more to implicits than this in Scala and you can learn more about them in the official documentation.
Ask pattern is used when you to send a message to an actor and also expect a reply. Since this is message driven and asynchronous communication, we get back a Future of reply. The future completes when the actors decides to send a reply and reply reaches the destination.
Do you remember the code of bank account actor where we ended part 1?
Here, to know the account balance, we make the actor print it on console. But what if we want to get the balance as a response? Let’s make that happen.
The first thing we will need to do is — change the protocol. Here’s our existing protocol
We need to add a “replyTo” parameter (name of parameter doesn’t matter) to the message which needs a reply. It should of type ActorRef[T] where T is the type of reply you want. Note that we changed the name from PrintBalance to GetBalance and also changed it from case object to case class since it now has parameters and can not be singleton anymore.
We are using akka version 2.6.5. You can find the latest version on github.
This was pretty straight forward (hopefully!). While handling the message in the behaviour we get hold of “replyTo” address and simply send (!) the balance.
Now let’s see how we can receive the reply outside of the actor world. This is where ask pattern comes to rescue.
First we need add an import statement to enable some extension methods
This provides with an ask extension method over account1 actorRef. Let’s observe this signature.
It requires one explicit parameter i.e. a function from ActorRef[Int] to BankAccountMessage. This is easy to construct.
We don’t have to worry about how we get this actorRef, we just need to be concerned with creating a new BankAccountMessage. In this is case it is GetBalance. GetBalance requires an ActorRef[Int] which is provided to us by ask method. Ask method internally spawns a temporary actor and provides its actor ref to us. But we don’t have to worry too much about it, it’s just good to know.
We can reduce this code. ask takes a function and GetBalance’s constructor is also function which takes actor ref. We can pass that directly and let the compiler expand it at compile time.
ask requires two more parameters which are marked as implicit. A timeout and a scheduler. Timeout is required to tell ask, how much time should it wait before failing the future with a timeout exception. A scheduler is a utility which, as the name suggests, is used to schedule function executions. In this case, ask requires scheduler to fire timeout exception in case actor does not send a reply or it does not reach in time. To pass these implicit values we just have to create their instances in the scope of ask and mark them as implicit as shown below.
We don’t have to create a new scheduler instance, we can just use scheduler of our actor system. For defining timeout, you need to pass an instance of FiniteDuration and it can be created using extension methods such as .seconds or.minutes. You need to import scala.concurrent.duration.DurationInt for these extensions.
Akka also provides an operator function for ask and it is ?. It is similar to ! which is an operator function for tell. Using this operator function, the code becomes as shown below
valbalanceFuture:Future[Int]=account1?GetBalance
Now that we have the future of balance, we can use .foreach to get access to actual balance and then print it.
This requires an implicit execution context. We can use the global execution context by importing scala.concurrent.ExecutionContext.Implicits.global or we can use the one in actor system.
Notice that for scheduler I have to create a new implicit variable, but for executionContext, I could just import it and it worked. That’s because actor system’s execution context is already marked implicit.
In part 1, we saw how to spawn a new “system” actor. But system actors are not what we are supposed to spawn for any normal (domain) entities. We need to spawn a user actor. A user actor can only be spawn by another actor. It can be a user actor or a system actor. The most common pattern to use is SpawnProtocol. SpawnProtocol is a behaviour, give out of the box by Akka. The pattern is
Use spawn protocol behaviour as guardian behaviour. This will create a system actor with this protocol.
Send spawn message to this actor with desired behaviour as a parameter
This actor will spawn the new actor and return actor ref of new actor
SpawnProtocol() returns Behavior[Command] . Command is a trait which has only one subtype — Spawn[T] . This means when you use this behavior for spawning an actor or actor system, the only message you can send to target actor is Spawn[T].
T represents the type of behavior you will spawn using spawn protocol. In other words, the “type of message” your spawned actor will accept after it is spawned.
Since we expect a reply (actor ref) from guardian actor, we need to use ask pattern here. Let’s look at the code.
For comprehensions solve a major problem by giving access to all the “resolved” variables to following expressions. For instance, in the previous code without for comprehension, we had to write bankAccountFuture.forEach twice and bankAccountFuture.flatMap once even after we know that future is complete because these operations are sequential. We could have carried the values manually, but that would have been super unreadable and boilerplate code. That’s why. for comprehensions are one of my favourite scala feature