schub
schub

Reputation: 932

How to model a simple akka actor system?

this is a more general design question of how to model akka actors. The situation explained on a very simple example and I would like to get a more general answer on possibilities and approaches and their advantages and disadvantages.

There is a user object with which something should be done. Lets say: doSomethingWithUser(user: User). Let's say, the user has a property avatar: Option[String] which is an url. If it exists, the actual image behind the url should be grabbed before running the doSomethingWithUser method. Talking Akka, I would create an Actor DoSomethingWithUserActor which can receive two messages:

case class NewUser(user: User)
case class NewUserWithImage(user: User, imageData: Array[Byte])

Grabbing the image data is implemented as an Actor FetchImageActor which can handle one message:

case class FetchImage(url: String)

and produce one message:

case class GotImage(imageData: Array[Byte])

The MainActor is the root actor and only receives one message NewUser which is handled like this:

def receive {
  case newUser: NewUser => {
    newUser.avatar match {
      case Some(avatar) => {
        // here I would like to send a message to the FetchImageActor, 
        // wait for the response (GotImage) and once it's there send a
        // NewUserWithImage message to the DoSomethingWithUser actor.
        //
        // How can this be done?
        // Is it a good idea to use a Future here, and if so, how can this
        // be done?
        //
        // pseudocode:
        val gotImage: GotImage = // get it somehow
        doSomethingWithUserActor ! NewUserWithImage(newUser.user, gotImage.imageData)
      }
      case _ => doSomethingWithUserActor forward NewUser(newUser.user)
  }
}

The DoSomethingWithUserActor handles both messages NewUser and NewUserWithImage. Maybe like this:

def receive {
  case newUser: NewUser => doSomethingWithUser(newUser.user)
  case newUserWithImage: NewUserWithImage => {
    doSomethingWithImage(newUserWithImage.imageData)
    doSomethingWithUser(newUser.user)
  }
}

private def doSomethingWithUser(user: User) = { ... }
private def doSomethingWithImage(imageData: Array[Byte]) = { ... }

First I don't know, how to make the async call in case the user has an avatar and second, I don't know if it is in general a good approach to handle this problem this way.

Another approach could be, that the NewUser message is forwarded to the FetchImageActor. This actor then checks whether the user has the avatar property set and if so, it fetches the image and send a NewUserWithImage message back to the MainActor which forwards this message to the DoSomethingWithUserActor, which then actually does something with the contained user object and the image data. I thing, this would be bad, since the FetchImageActor needs knowledge about the user but it is only for fetching images. That are two different independent aspects, which should not be mixed together. In this case the FetchImage message object needs the user property as well (which I don't like as described before).

What would be the right or a 'good' strategy to solve this problem?

Upvotes: 0

Views: 412

Answers (2)

pushy
pushy

Reputation: 9645

One pattern which is very useful for your use-case, is using ask-pipeTo.

You use the ask pattern to get a future for your request, then (optionally) do some transformation on the result (the example below combines several responses, you can also call map on the future) and the use the pipeTo pattern to send the result of the future to a different actor. This is very similar to what Arseniy suggests in point 2, in fact pipeTo registers using onSuccess on the future.

Here is an example from the excellent akka docs:

import akka.pattern.{ ask, pipe }
import system.dispatcher // The ExecutionContext that will be used
case class Result(x: Int, s: String, d: Double)
case object Request

implicit val timeout = Timeout(5 seconds) // needed for `?` below

val f: Future[Result] =
  for {
    x ← ask(actorA, Request).mapTo[Int] // call pattern directly
    s ← (actorB ask Request).mapTo[String] // call by implicit conversion
    d ← (actorC ? Request).mapTo[Double] // call by symbolic name
  } yield Result(x, s, d)

f pipeTo actorD // .. or ..
pipe(f) to actorD

Another pattern I like to use in such circumstances is using temporary actors. Keep in mind that actors are unlike threads, there are very lightweight, and creating them does not cause much overhead.

You could do something like this:

val tempActor = context.actorOf(Props(classOf[DoSomethingWithUserActor], newUser.user)
fetchImageActor.tell(FetchImage(image), tempActor)

This way, the DoSomethingActor already has a reference to the user, and by setting it as the sender the FetchImageActor can just reply to the message, and your temporary actor will receive the image. Play around with that idea a bit, I find it very powerful when correctly used.

Upvotes: 1

Arseniy Zhizhelev
Arseniy Zhizhelev

Reputation: 2401

  1. If you need to interconnect actors you may find SynapseGrid library useful.

  2. For async calls Future is the usual approach.

    val gotImageFuture = new Future { fetchImageActor ? FetchImage(avatar) }
    gotImageFuture.onSuccess( (gotImage: GotImage) =>
      doSomethingWithUserActor ! NewUserWithImage(newUser.user, gotImage.imageData)
    )
    
  3. If you add a correlation token (either the User or at least the url) to fetching messages:

    GotImage(user:User, imageData: Array[Byte])
    FetchImage(url:String, user:User)
    

    then you may simply use fire-forget. The main actor will simply handle GotImage:

    ...
        case GotImage(user, imageData) => 
          doSomethingWithUserActor ! NewUserWithImage(user, imageData)
    

    This approach is unblocking.

P.S. A minor advice:

case class NewUser(user: User, imageData:Option[Array[Byte]])

can make handling a bit easier.

Upvotes: 1

Related Questions