Sender Propagation
Overview
The actor system now supports consistent sender context propagation through actor hierarchies, making it work seamlessly with the ask pattern.
Two Approaches
1. Using tell() - No Sender Propagation (Default)
When you use tell(), the sender context is NOT propagated:
// Grandparent asks Parent
Pid response = grandparent.ask(parentPid, new Request());
// Inside Parent's receive method:
@Override
public void receive(Message msg, ActorContext context) {
// Sender is available via context
// Send to child using tell - sender context is lost
childPid.tell(new ChildMessage());
// Inside Child's receive method, no sender context available
}
2. Using forward() - Preserves Original Sender (New!)
When you use forward(), the original sender context is preserved:
// Grandparent asks Parent
Pid response = grandparent.ask(parentPid, new Request());
// Inside Parent's receive method:
@Override
public void receive(Message msg, ActorContext context) {
// Sender context available
// Forward to child - preserves grandparent as sender
context.forward(childPid, new ChildMessage());
// Inside Child's receive method:
// Sender context preserved - child can reply to grandparent
}
Use Cases
Use Case 1: Request Forwarding
When an actor acts as a router/proxy and wants the final handler to reply to the original requester:
public class RouterHandler implements Handler<Message> {
@Override
public void receive(Message msg, ActorContext context) {
if (msg instanceof RoutableRequest req) {
Pid handler = selectHandler(req);
// Forward preserves original sender for reply
context.forward(handler, req);
}
}
}
public class HandlerActor implements Handler<Message> {
@Override
public void receive(Message msg, ActorContext context) {
if (msg instanceof RoutableRequest req) {
// Process request
Response response = process(req);
// Reply directly to original requester, not router
context.reply(response);
}
}
}
Use Case 2: Multi-Level Processing Pipeline
// Grandparent initiates request
Pid result = grandparent.ask(parentPid, new ProcessRequest());
// Parent forwards to child
public class ParentHandler implements Handler<Message> {
@Override
public void receive(Message msg, ActorContext context) {
if (msg instanceof ProcessRequest req) {
// Do some preprocessing
ProcessRequest enhanced = preprocess(req);
// Forward to child - grandparent remains the sender
context.forward(childPid, enhanced);
}
}
}
// Child processes and replies to grandparent
public class ChildHandler implements Handler<Message> {
@Override
public void receive(Message msg, ActorContext context) {
if (msg instanceof ProcessRequest req) {
ProcessResult result = process(req);
// Reply goes to grandparent (original requester)
context.reply(result);
}
}
}
Use Case 3: Handler-Based API
The forward() method is also available in the handler-based API through ActorContext:
public class MyHandler implements Handler<Message> {
@Override
public void receive(Message msg, ActorContext context) {
if (msg instanceof ForwardableRequest req) {
Pid nextActor = selectNextActor(req);
// Forward preserves sender context
context.forward(nextActor, req);
}
}
}
When to Use Each
| Method | Use When | Sender Context |
|---|---|---|
tell() | Normal message passing, no reply expected | Lost (Optional.empty()) |
forward() | Acting as intermediary, want final actor to reply to original sender | Preserved |
ask() | Request-response pattern, you are the requester | You become the sender |
Implementation Details
- Sender context is stored in a
ThreadLocalvariable forward()wraps the message withMessageWithSenderto preserve context- Context is automatically cleared after message processing
- Works seamlessly with the ask pattern
- Compatible with both traditional Actor subclassing and Handler-based API
Best Practices
- Use
forward()for request routing: When your actor is a proxy/router - Use
tell()for fire-and-forget: When no reply is needed - Use
ask()for request-response: When you need a reply - Use Optional methods:
getSender()returnsOptional<Pid>- useifPresent(),map(), ororElse()for clean handling