Reputation: 932
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
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
Reputation: 2401
If you need to interconnect actors you may find SynapseGrid library useful.
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)
)
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