Skip to content

feat: #[actor] macro, named registry, and Handler<M>/Recipient<M> API#149

Open
ElFantasma wants to merge 3 commits intomainfrom
feat/actor-macro-registry
Open

feat: #[actor] macro, named registry, and Handler<M>/Recipient<M> API#149
ElFantasma wants to merge 3 commits intomainfrom
feat/actor-macro-registry

Conversation

@ElFantasma
Copy link
Collaborator

Summary

Unified API redesign addressing #144, #145, and #129:

  • Handler<M> trait (Improve type safety for request/reply message handling #144): Per-message typed results via RPITIT. Each message type gets its own handler impl with M::Result return type. Replaces the monolithic handle_request/handle_message pattern.
  • Recipient<M> (Circular dependency problem with bidirectional actor communication #145): Type-erased cross-actor messaging via Receiver<M> (object-safe) + Recipient<M> = Arc<dyn Receiver<M>>. Actors reference each other by message type, not concrete type — breaks circular dependencies.
  • Named registry (Add Registry for name-based actor lookup #129): Erlang-style register/whereis/unregister with Any-based global store, keyed by name. Decouples actor discovery from type knowledge.
  • #[actor] proc macro: Generates Handler<M> impls from #[handler]-annotated methods. Supports both async (tasks) and sync (threads).
  • messages! macro: Declarative macro for defining message structs with associated result types.
  • Context<A>: Replaces ActorRef<A> in handler signatures. Provides send(), request(), stop().

New crates

  • spawned-macros — proc-macro crate with #[actor] + #[handler]

New examples

  • chat_room — demonstrates Recipient<M> breaking circular deps between Room and User
  • service_discovery — demonstrates register/whereis for named actor discovery

Migration

All existing examples migrated to the new API.

Test plan

  • cargo build --workspace — 17 crates compile
  • cargo test --workspace — 34 tests pass
  • cargo run -p bank — typed Handler returns work
  • cargo run -p chat_room — Recipient cross-actor messaging works
  • cargo run -p service_discovery — registry register/whereis works

Closes #144, closes #145, closes #129

… API

Introduces a unified API redesign addressing #144, #145, and #129:

- Handler<M> trait with per-message typed results (RPITIT, not object-safe)
- Receiver<M>/Recipient<M> for type-erased cross-actor messaging
- #[actor] proc macro that generates Handler<M> impls from #[handler] methods
- Any-based named registry (register/whereis/unregister)
- messages! declarative macro for defining message structs
- Context<A> replaces ActorRef<A> in handler signatures
- New examples: chat_room (Recipient), service_discovery (registry)
- All existing examples migrated to new API
@github-actions
Copy link

🤖 Codex Code Review

Review Summary
The refactor to message-based handlers and the new macros are generally clean and consistent across async/thread backends. I did find a couple of lifecycle/concurrency issues that can cause actors to hang or leak background threads, plus a macro edge case that can break user code.

Findings (Ordered by Severity)

  1. Actor stop does not wake idle actors (tasks + threads).

    • In both backends, Context::stop() only cancels the token; run_actor is blocked on rx.recv() with no cancellation select. If the actor is idle, ctx.stop() will not terminate it until a new message arrives or the channel closes.
    • This is a correctness/lifecycle bug that affects graceful shutdown and cancellation semantics.
    • Where: concurrency/src/tasks/actor.rs:356-386, concurrency/src/threads/actor.rs:332-362.
    • Suggested fix: In tasks backend, select between rx.recv() and cancellation_token.cancelled(). In threads backend, use recv_timeout with periodic cancellation checks, or use CancellationToken::on_cancel to wake the receiver similarly to the timer utilities.
  2. send_message_on in threads backend can leak threads after actor stop.

    • The thread version executes f() (often a blocking signal wait) before checking cancellation_token. If the actor stops, this background task still blocks indefinitely until f() returns.
    • This is especially problematic for ctrl_c()-style waits; these threads are effectively leaked.
    • Where: concurrency/src/threads/actor.rs:384-398.
    • Suggested fix: Provide a cancellation-aware wait (similar to the timer wake channel approach), or have send_message_on take a FnOnce(&CancellationToken) so the wait can short-circuit on cancellation.

Medium / API Footguns

  1. messages! macro always adds #[derive(Debug)], which can cause duplicate-derive errors.
    • If user supplies #[derive(Debug, ...)], the macro adds another #[derive(Debug)], which can lead to conflicting impls.
    • Where: concurrency/src/message.rs:22-38.
    • Suggested fix: Remove the forced #[derive(Debug)] and document a recommended derive list, or add an optional @derive variant to control it.

General Observations

  • The envelope + handler model is clean and idiomatic.
  • Error mapping to ActorStopped simplifies error types; just note that this loses granularity (previous Server/Callback/Init distinctions).

If you want, I can sketch a concrete patch for the cancellation-aware receive loop and the threads send_message_on wake mechanism.


Automated review by OpenAI Codex · custom prompt

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 11, 2026

Greptile Overview

Greptile Summary

This PR introduces a comprehensive API redesign for the spawned actor framework, replacing the monolithic message handling pattern with a typed, per-message Handler<M> trait system. The changes enable type-safe cross-actor messaging via Recipient<M> (solving circular dependency issues) and Erlang-style named actor discovery through a global registry.

Key Changes:

  • Handler trait: Replaced handle_request/handle_message with per-message trait implementations using RPITIT, enabling typed results (M::Result) for each message type
  • Recipient: Type-erased messaging abstraction (Arc<dyn Receiver<M>>) that allows actors to reference each other by message type rather than concrete actor type, breaking circular dependencies
  • Named registry: Global Any-based registry with register/whereis/unregister for dynamic actor discovery without compile-time type knowledge
  • #[actor] macro: Proc macro that generates Handler<M> implementations from #[handler]-annotated methods, reducing boilerplate
  • messages! macro: Declarative macro for defining message structs with associated Result types

Migration Quality:
All 17 crates in the workspace successfully compile, and all 34 tests pass. The migration is consistent across both tasks (async) and threads (sync) modules. Examples effectively demonstrate the new patterns (chat_room for Recipient<M>, service_discovery for registry).

Architecture:
The new design uses type erasure at different levels: Envelope<A> for per-actor message handling, and Receiver<M> for cross-actor references. This provides both type safety (via Handler<M> bounds) and flexibility (via Recipient<M> indirection).

Confidence Score: 5/5

  • This PR is safe to merge with high confidence
  • The refactor is well-executed with comprehensive test coverage (34 passing tests), consistent implementation across async/sync modules, clear examples demonstrating new patterns, and successful compilation of all 17 workspace crates. The API design is sound, using established patterns (type erasure, trait objects, proc macros). No critical issues found.
  • No files require special attention

Important Files Changed

Filename Overview
macros/src/lib.rs Implements #[actor] proc macro for generating Handler trait implementations from #[handler] methods
concurrency/src/registry.rs New global registry using Any-based type erasure for named actor discovery; has good test coverage
concurrency/src/message.rs Defines core Message trait and messages! macro for typed message declarations with associated result types
concurrency/src/tasks/actor.rs Major refactor: replaced monolithic message handling with Handler trait, added Recipient type for type-erased messaging, improved panic handling
concurrency/src/threads/actor.rs Parallel refactor for sync handlers; mirrors tasks module design with synchronous Handler trait
examples/chat_room/src/main.rs New example demonstrating Recipient solving circular dependency between Room and User actors
examples/service_discovery/src/main.rs New example demonstrating registry-based named actor discovery with register/whereis/unregister
examples/bank/src/server.rs Successfully migrated to Handler trait pattern; each message type has individual handler implementation

Sequence Diagram

sequenceDiagram
    participant User as User Actor
    participant Room as ChatRoom Actor
    participant Recipient as Recipient<M>
    participant Handler as Handler<M>
    
    Note over User,Room: Actor Initialization
    User->>Room: room.recipient::<Say>()
    Room-->>User: Recipient<Say>
    
    Note over User,Room: Cross-Actor Messaging via Recipient
    User->>Recipient: send(Say { from, text })
    Recipient->>Room: Envelope<Say>
    Room->>Handler: handle(Say, ctx)
    Handler-->>Room: ()
    
    Note over Room: Broadcast to Members
    loop For each member
        Room->>Recipient: member.send(Deliver)
        Recipient->>User: Envelope<Deliver>
        User->>Handler: handle(Deliver, ctx)
        Handler-->>User: ()
    end
    
    Note over User,Room: Registry-Based Discovery
    User->>Registry: register("room", recipient)
    Registry-->>User: Ok(())
    User->>Registry: whereis::<Recipient<M>>("room")
    Registry-->>User: Some(recipient)
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

47 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 52 to 69
let msg_ty = match method.sig.inputs.iter().nth(1) {
Some(FnArg::Typed(pat_type)) => {
if let Pat::Ident(pat_ident) = &*pat_type.pat {
if pat_ident.ident == "_" || pat_ident.ident.to_string().starts_with('_') {
// Still use the type
}
}
&*pat_type.ty
}
_ => {
return syn::Error::new_spanned(
&method.sig,
"#[handler] method must have signature: fn(&mut self, msg: M, ctx: &Context<Self>) -> R",
)
.to_compile_error()
.into();
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider validating that the 3rd parameter is ctx: &Context<Self>. Currently, if a user writes a handler with the wrong context type or missing context parameter, they'll get a confusing compiler error instead of a clear macro error.

Prompt To Fix With AI
This is a comment left during a code review.
Path: macros/src/lib.rs
Line: 52:69

Comment:
Consider validating that the 3rd parameter is `ctx: &Context<Self>`. Currently, if a user writes a handler with the wrong context type or missing context parameter, they'll get a confusing compiler error instead of a clear macro error.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant