-
Notifications
You must be signed in to change notification settings - Fork 5
Open
Description
Problem
When two actors need to collaborate bidirectionally (each sending messages to the other), Spawned's strongly-typed ActorRef<T> creates a circular module dependency that Rust rejects.
// actors/actor_a.rs
use crate::actors::actor_b::ActorB; // ⚠️ imports actor_b
struct ActorA;
enum MsgA {
SetPeer(ActorRef<ActorB>), // Needs ActorB type
FromB(String),
}
impl Actor for ActorA { type Message = MsgA; ... }
// actors/actor_b.rs
use crate::actors::actor_a::ActorA; // ⚠️ imports actor_a - CIRCULAR!
struct ActorB;
enum MsgB {
SetPeer(ActorRef<ActorA>), // Needs ActorA type
FromA(String),
}
impl Actor for ActorB { type Message = MsgB; ... }Rust will reject this - circular module dependencies are not allowed.
Current Workarounds
1. Shared Message Type (what ping_pong example does)
// messages.rs - shared by both
enum Message {
Ping { from: Sender<Message> },
Pong,
}
// Both actors use the same Message type
impl Actor for ActorA { type Message = Message; ... }
impl Actor for ActorB { type Message = Message; ... }Problem: Loses type safety - ActorA can receive messages meant for ActorB.
2. Pass Raw Sender Instead of ActorRef
enum MsgA {
SetPeer(mpsc::Sender<MsgB>), // Raw sender, not ActorRef
}Problem: Loses ActorRef conveniences (cancellation token, join, etc.)
3. Put Both Actors in Same Module
Works but doesn't scale for large systems.
Proposed Solutions (Framework Changes)
Option A: Type-Erased Actor Handle
// A trait object that any ActorRef can become
trait AnyActor: Send {
fn send_boxed(&self, msg: Box<dyn Any + Send>) -> Result<(), Error>;
}
enum MsgA {
SetPeer(Arc<dyn AnyActor>), // No concrete type needed
}Option B: Generic Sender Trait (like Actix's Recipient)
trait MessageSender<M>: Send + Clone {
fn send(&self, msg: M) -> Result<(), Error>;
}
// ActorRef<T> implements MessageSender<T::Message>
// Now messages can hold Arc<dyn MessageSender<SomeMsg>>
enum MsgA {
SetPeer(Arc<dyn MessageSender<SharedMessage>>),
}This is similar to Actix's Recipient<M> pattern - a type-erased handle that can send a specific message type M to any actor that handles M.
Option C: PID-like Addressing (Erlang style)
// Global registry with string/id-based addressing
enum MsgA {
SetPeer(Pid), // Just an ID, resolved at runtime
}
// Sending:
registry.send(pid, message)?;This aligns with Phase 3 of the roadmap (Process Primitives with Registry).
Option D: Shared Protocol Trait
// Define a shared protocol both actors understand
trait PeerProtocol {
fn notify(&self, msg: String) -> Result<(), Error>;
}
// Both ActorRef<A> and ActorRef<B> implement PeerProtocol
enum MsgA {
SetPeer(Arc<dyn PeerProtocol + Send + Sync>),
}Comparison with Other Frameworks
| Framework | How It Solves This |
|---|---|
| Erlang | PIDs are untyped, messages are untyped - no compile-time deps |
| Akka | ActorRef[Any] or typed protocols with shared message traits |
| Actix | Addr<A> + Recipient<M> (type-erased for specific message) |
| Orleans | Interface-based grains, no circular deps by design |
Actix's Recipient Pattern (Worth Studying)
// Actix allows type-erasing an Addr to just the message it can receive
let recipient: Recipient<MyMessage> = addr.recipient();
// Any actor handling MyMessage can be stored as Recipient<MyMessage>
struct MsgA {
peer: Recipient<SharedMessage>, // Works for any actor handling SharedMessage
}Decision Criteria
- Preserve type safety where possible
- Minimize runtime overhead
- Keep API ergonomic
- Align with Erlang philosophy (or consciously diverge)
Related
- Phase 3 roadmap: Process Primitives (Pid, Registry) - v0.5 Roadmap Tracking Issue #138
- Type safety for request/reply - Improve type safety for request/reply message handling #144
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels