Return Address
When reasoning on Request-Reply (209), what if you want your request receiver to reply to an actor at an address other than the direct message sender? Well, that’s the idea behind Return Address, as shown in Figure 6.5, and one that you can implement in a few different ways.
Figure 6.5 A Requestor uses a Return Address to tell the Reactor to reply to a third party.
It’s interesting that the Actor model actually uses addresses to identify how to send messages to actors. You see, each actor has an address, and to send a given actor a message, you must know its address. One actor can know the address of another actor by a few different means.
- An actor creates another actor and thus knows the address of the actors it has created.
- An actor receives a message that has the address of one or more other actors that it will send messages to.
- In some cases, an actor may be able to look up the address of another actor by name, but this may create an unseemly binding to the definition and implementation of a given actor.
The Enterprise Integration Patterns [EIP] Return Address fits really well with the fundamental ideas behind the Actor model.
One obvious way to provide a Return Address in a given message is to put the address of the actor that you want to receive the reply right in the message that you send. Recall that you did something similar in the Request-Reply (209) example.
case class StartWith(server: ActorRef)
The first message that the client receives is StartWith, and that message must contain the ActorRef of the server that the client is to use. That way, the client will know how to make requests of some server. Okay, so that’s not really a Return Address, but you could send a Return Address as part of a message in the same way.
If the client chose to, it could also send messages to the server and provide the Return Address of the actor that should receive the reply. Of course, the request message itself would have to support that protocol and allow the ActorRef to be included in the message.
case class Request(what: String, replyTo: ActorRef)
That way, when the server is ready to send its reply to the request, it could send the reply to the replyTo actor, like so:
class Server extends Actor { def receive = { case Request(what, replyTo) => println("Server: received request value: " + what) replyTo ! Reply("RESP-1 for " + what) case _ => println("Server: received unexpected message") } }
That works, but it does require you to design the message protocol in a certain way. What if you have an existing message protocol and you later decide to redesign the existing receiving actor to delegate some message handling to one of its child actors? This might be the case if there is some complex processing to do for certain messages and you don’t want to heap too much responsibility on your original actor, for example the server. It would be nice if the server could create a child worker to handle a specific kind of complex message but design the worker to reply to the original client sender, not to the parent server. That would free the parent server to simply delegate to the child worker and allow the worker to react as if the server had done the work itself.
package co.vaughnvernon.reactiveenterprise.returnaddress import akka.actor._ import co.vaughnvernon.reactiveenterprise._ case class Request(what: String) case class RequestComplex(what: String) case class Reply(what: String) case class ReplyToComplex(what: String) case class StartWith(server: ActorRef) object ReturnAddress extends CompletableApp(2) { val client = system.actorOf(Props[Client], "client") val server = system.actorOf(Props[Server], "server") client ! StartWith(server) awaitCompletion println("ReturnAddress: is completed.") } class Client extends Actor { def receive = { case StartWith(server) => println("Client: is starting...") server ! Request("REQ-1") server ! RequestComplex("REQ-20") case Reply(what) => println("Client: received reply: " + what) ReturnAddress.completedStep() case ReplyToComplex(what) => println("Client: received reply to complex: " + what) ReturnAddress.completedStep() case _ => println("Client: received unexpected message") } } class Server extends Actor { val worker = context.actorOf(Props[Worker], "worker") def receive = { case request: Request => println("Server: received request value: " + request.what) sender ! Reply("RESP-1 for " + request.what) case request: RequestComplex => println("Server: received request value: " + request.what) worker forward request case _ => println("Server: received unexpected message") } } class Worker extends Actor { def receive = { case RequestComplex(what) => println("Worker: received complex request value: " + what) sender ! ReplyToComplex("RESP-2000 for " + what) case _ => println("Worker: received unexpected message") } }
This is the output produced by the Return Address example:
Client: is starting... Server: received request value: REQ-1 Server: received request value: REQ-20 Client: received reply: RESP-1 for REQ-1 Worker: received complex request value: REQ-20 Client: received reply to complex: RESP-2000 for REQ-20
Note that when the Server is created, it uses its context to create a single child Worker actor. This Worker is used by the Server only when it receives a RequestComplex message. Also note that there is no reason to design the RequestComplex message with a replyTo ActorRef. Thus, as far as the Client is concerned, it is the Server that handles the RequestComplex message.
Now notice that the Server doesn’t just tell the Worker what to do by sending it the RequestComplex message. Rather, the Server forwards the RequestComplex message to the Worker. By forwarding, the Worker receives the message as if it had been sent directly by the Client, which means that the special sender ActorRef has the address of the Client, not of the Server. Therefore, the Worker is able to act on behalf of the Server, as if the Server itself had done the work. Yet, the Server is freed from acting as a mediator between the Client and the Worker, not to mention that the Server is ready to process other messages while the Worker does its thing.