Akka Ask Antipattern
In this post, I’d like to raise a topic that relates to ask
pattern in Akka. How it works and what problems might it cause.
What is ask pattern?
The usual way of communication between actors in Akka is tell
pattern with a fire-and-forget semantic. It means that an actor fires a message to another actor and doesn’t wait for a response. To receive a response, the second actor should fire a message back.
Alternatively, ask
pattern provides a way to send a message to an actor and expect to receive a response from it. The result of ask
will be a Future
that represents a possible reply.
Under the hood, the ask
operation involves creating a listener actor for handling this reply. The listener actor is quite cheap, but not free. Also, it has a timeout to prevent leak of resources.
Now, suppose that we have a series of five actors. Each actor performs some small task and then passes the result to the next actor in a pipeline. In the end, we want the original actor to receive the result of the processing. This will look like:
Problem 1. Resources wasting
The flow looks more complex than we really need. In the chain of 5 actors, we have created 4 actor listeners and passed 8 messages. It happens behind the scenes, but it still consumes resources. Can we avoid that? Yes. Just use the following scenario:
As you may see, we replaced most of the ask
with forward
and ended up with 0 listeners and 5 message passes.
What forward
actually does? forward
is a special version of tell
, which keeps original message sender. Therefore Actor1
will be recognized as a message sender, as long as actors are passing the message using forward
. Thanks to that, Actor5
can send a reply directly to Actor1
, as otAer actors are not necessary anymore.
Problem 2. Handle total timeout
In a long chain of actors that are using ask
it is difficult to set the right value for a timeout so that various timeouts work together in a sensible way. What really necessary is that requester actor Actor1
succeeds within the specific timeout. All the other timeouts are irrelevant. But, because we have overused the ask
pattern, we are forced to define timeouts at each stage of the process.
Let’s assume that the entire request needs to happen within 30 seconds and we evenly distributed total timeout between actors. What will happen if:
- A new actor has been added to the chain of actors
- A requirement on timeout for an actor of the chain has changed
- A requirement on overall timeout has changed
The answer is pain will happen. You have to review and edit timeout for every actor in the chain.
Problem 3. Handle response
The main problem here is that ask
returns a reply as Future[Any]
, so you should handle the incoming message in future’s callback out of Receive
function. It can happen in parallel with running receive code, therefore there is lots of danger there. You can corrupt actor’s state or cause hard-to-reproduce bugs.
Conclusions
The point of the article is to realize that ask
pattern designed to be used only in very specific situations:
- You need to communicate with an actor from outside the system
- You need to use a request/response–style approach and a timeout is desirable
So, always prefer tell
, and only ask
if you must.