Question:
What is the correct pattern for accumulating state in an Akka actor?
Context:
Let's say I have a few services that all return data.
class ServiceA extends Actor {
def receive = {
case _ => sender ! AResponse(100)
}
}
class ServiceB extends Actor {
def receive = {
case _ => sender ! BResponse("n")
}
}
// ...
I want to have one controlling/supervising actor that coordinates talking to all these services and keeping track of their responses, then sending a response with all the data back to the original sender.
class Supervisor extends Actor {
def receive = {
case "begin" => begin
case AResponse(id) => ???
case BResponse(letter) => ???
}
// end goal:
def gotEverything(id: Int, letter: String) =
originalSender ! (id, letter)
def begin = {
ServiceA ! "a"
ServiceB ! "b"
}
}
As service responses come in, how do I keep all of that state associated together? As I understand it, if I was to assign the value of AResponse to, say, var aResponse: Int
, that var is constantly changing as different messages are being received and it's not possible for me to count on that var
staying while I wait for the BResponse
message.
I realize I could use ask
and just nest/flatMap Future
's, but from what I've read that is a bad pattern. Is there a way to achieve all this without Future's?
Because actors are never accessed from multiple threads simultaneously, you can easily store and mutate any state in them you want. For example, you can do this:
class Supervisor extends Actor {
private var originalSender: Option[ActorRef] = None
private var id: Option[WhateverId] = None
private var letter: Option[WhateverLetter] = None
def everythingReceived = id.isDefined && letter.isDefined
def receive = {
case "begin" =>
this.originalSender = Some(sender)
begin()
case AResponse(id) =>
this.id = Some(id)
if (everythingReceived) gotEverything()
case BResponse(letter) =>
this.letter = Some(letter)
if (everythingReceived) gotEverything()
}
// end goal:
def gotEverything(): Unit = {
originalSender.foreach(_ ! (id.get, letter.get))
originalSender = None
id = None
letter = None
}
def begin(): Unit = {
ServiceA ! "a"
ServiceB ! "b"
}
}
There is a better way, however. You can emulate some kind of state machine with actors without explicit state variables. This is done using become()
mechanism.
class Supervisor extends Actor {
def receive = empty
def empty: Receive = {
case "begin" =>
AService ! "a"
BService ! "b"
context become noResponses(sender)
}
def noResponses(originalSender: ActorRef): Receive = {
case AResponse(id) => context become receivedId(originalSender, id)
case BResponse(letter) => context become receivedLetter(originalSender, letter)
}
def receivedId(originalSender: ActorRef, id: WhateverId): Receive = {
case AResponse(id) => context become receivedId(originalSender, id)
case BResponse(letter) => gotEverything(originalSender, id, letter)
}
def receivedLetter(originalSender: ActorRef, letter: WhateverLetter): Receive = {
case AResponse(id) => gotEverything(originalSender, id, letter)
case BResponse(letter) => context become receivedLetter(originalSender, letter)
}
// end goal:
def gotEverything(originalSender: ActorRef, id: Int, letter: String): Unit = {
originalSender ! (id, letter)
context become empty
}
}
This may be slightly more verbose, but it does not contain explicit variables; all state is implicitly contained in parameters of Receive
methods, and when this state needs to be updated, actor's receive function is just switched to reflect this new state.
Note that the above code is very simple and it won't work properly when there can be many "original senders". In that case you'll have to add an id to all messages and use them to determine which responses belong to which "original sender" state or you can create multiple actors, each for every one of the "original senders".
I believe Akka way is to use actor-per-request pattern. This way, instead figuring out which responds corresponds to what, you create a new actor every time you get a request. This is very cheap and, in fact, happens every time you do ask().
These request processors (that's how I call them) usually have simple fields for responses. And it is only a matter of simple null comparison to see if request has arrived.
Retries/failures also get much easier with this scheme. Timeouts as well.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With