With Akka classic, one can easily implement a service (as an actor: ServiceActor) that
RequestorA sends a RequestMessage to the ServiceActor ServiceActor sends back an Acknowledgement to RequestorA
Similarly, RequestorB sends a RequestMessage to the ServiceActor ServiceActor sends back an Acknowledgement to RequestorB
Whatever the Requestor, there are only one type of RequestMessage and Acknowledgement and all the RequestMessages are handled the same way by the ServiceActor.
What is the way to achieve something similar with Akka Typed? Now that the request message must contain an explicit replyTo: ActorRef[RequestorA.Message], is there a way to avoid implementing a different RequestMessage for each requestor?
Similarly, is there a way to avoid sending back a different type of acknowledgement to each type of requestor?
Typically in Akka Typed, the response message is defined by the protocol of the actor handling the request, for instance (in Scala):
object MyActor {
sealed trait Command
case class Request(replyTo: ActorRef[Response]) extends Command
sealed trait Response
// and so forth
}
It then becomes the responsibility of the actor sending the Request
to arrange to handle the Response
. If the requesting actor happens to only exist to send requests, it can define itself as a Behavior[Response]
, but in the more general case, there are a couple of tactics that can be used.
The actor can set up a message adapter:
val responseRef = context.messageAdapter { response =>
// convert to this actor's message protocol
???
}
otherActor ! MyActor.Request(responseRef)
If doing this, it's generally a good idea to set up the responseRef
only once (e.g. in Behaviors.setup
).
If the actor is just going to be performing the request once or only ever expects one response per request (and expects that response in a bounded amount of time), the "ask pattern" may be clearer:
implicit val timeout: Timeout = 15.seconds
context.ask(otherActor, MyActor.Request(_)) {
case Success(resp) =>
// convert to this actor's message protocol
???
case Failure(ex) =>
// convert to this actor's message protocol
???
This does require you to define an explicit timeout and handle the case where no response is received in time. The ActorRef
injected into the Request
message is ephemeral: it will not receive any message beyond the first message sent to it nor will it receive a message after the timeout.
It is also possible to define an actor's behavior as a Behavior[Any]
(which is like a "classic" untyped actor) but not expose that fact outside of the actor. In Scala, this is fairly straightforward and safe to do thanks to contravariance:
Behaviors.setup[Any] { context =>
// context.self is an ActorRef[Any], which by contravariance is usable as any type of ActorRef
otherActor ! MyActor.Request(context.self)
Behaviors.receiveMessage {
case resp: MyActor.Response =>
// handle response directly
???
case _ =>
Behaviors.unhandled
}
}.narrow[Command]
The spawned ActorRef
will be an ActorRef[Command]
(i.e. only promise to handle Command
), but the actor is able to decide to promise to handle any message it wants to handle (even if not a Command
). This does opt you out of compiler assistance when it comes to this actor sending messages to itself; additionally, this may be a bit of an obscure pattern, so its use should be well-commented.