Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct pattern for accumulating state in an Akka actor

Tags:

scala

akka

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?

like image 898
T. Stone Avatar asked Jul 11 '14 23:07

T. Stone


2 Answers

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".

like image 142
Vladimir Matveev Avatar answered Oct 14 '22 06:10

Vladimir Matveev


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.

like image 35
mikea Avatar answered Oct 14 '22 06:10

mikea