Search code examples
scalaakkaakka-typed

Akka Typed: Replying to messages (tell) sent from different actors


With Akka classic, one can easily implement a service (as an actor: ServiceActor) that

  • receives requests from different actors (i.e., actors of different types: RequestorA, RequestorB, etc.),
  • processes the request,
  • sends back an acknowledgement to the requestor.

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?

  • RequestFromRequestorA( replyTo: ActorRef[RequestorA.Message] )
  • RequestFromRequestorB( replyTo: ActorRef[RequestorB.Message] )

Similarly, is there a way to avoid sending back a different type of acknowledgement to each type of requestor?


Solution

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