actor12
A small, type-safe actor framework for Rust on top of Tokio — designed to get out
of your way. Each actor is a single async task that owns its state; you talk to it
through a cheap, cloneable Link. What makes actor12 different is how much of
the actor you get to choose: the message style, the run loop, the state it
exposes, and how errors flow back to callers.
[]
= "0.0.9"
= { = "1", = ["full"] }
= "1.0"
= "0.3"
Why actor12?
Most actor crates lock you into one messaging style and one fixed event loop.
actor12 instead gives you a handful of small, overridable knobs on the Actor
trait, with sensible defaults for all of them, so the simple case stays a few
lines while the hard case stays possible:
- Static or dynamic messages — pick one statically-typed request enum per
actor for maximum clarity and speed, or open the actor up to any number of
message types via the
Handlertrait. Same actor model, your choice per actor. - Custom run loop (
tick&cycle) — drop in periodic background work withtick, or overridecycleentirely to take full control of how the actor selects between messages, timers, and cancellation. - Custom state & props —
Specis the typed input you spawn an actor with;Stateis what the actor exposes back through itsLink, so callers can read a shared snapshot without sending a message. - First-class
anyhow— make your reply typeanyhow::Result<T>and transport failures (dead actor, dropped reply) fold intoErrautomatically, while handlers use?like any other async code. - Hierarchical cancellation — typed cancel reasons propagate to child tasks;
dropping the last
Linkshuts the actor down cleanly.
Quick start
Define an actor, implement a Handler per message type, and talk to it through
its Link:
use ;
use Future;
;
;
async
Static vs. dynamic messages
An actor's type Message decides how callers talk to it. actor12 supports two
styles and you choose per actor.
Static — one typed request type
Set Message = Envelope<Request, Reply>. The actor handles a single message type
and you match on it in Actor::handle. This is the leanest, most explicit
option: one enum in, one reply out, no dynamic dispatch.
use ;
use Future;
async
Dynamic — many message types via Handler
Set Message = Multi<Self> and implement Handler<T> once per message type.
Each message gets its own request and reply types, the actor stays open for
extension, and callers use ask_dyn / tell_dyn (see the Quick start
above). Reach for this when an actor naturally serves several distinct operations.
Custom state & props
Two associated types separate what you spawn an actor with from what it exposes back:
Spec— the props/configuration passed tospawnand forwarded toinit. Private to the actor.State— a value computed up front byActor::stateand stored in theLink. Any holder of the link can read it viaLink::statewithout sending a message — ideal for a shared counter, health flag, or config snapshot.
use ;
use Future;
use Arc;
use ;
;
async
Custom run loop
Every actor runs a cycle loop. By default each turn selects across three
things: a cancellation signal, a tick, and the next incoming message. You can
override either layer.
Periodic work with tick
Override tick to run background work on the actor's own task, interleaved with
message handling. Return ControlFlow::Continue(()) to keep going, or
ControlFlow::Break(reason) to stop the actor.
use ControlFlow;
use Duration;
Full control with cycle
For complete control over scheduling — priority channels, custom timeouts,
draining behavior — override cycle itself. The default is a tokio::select!
over cancellation, tick, and rx.recv(); yours can do whatever you need, as
long as it returns Continue to loop again or Break to shut down.
use ControlFlow;
use Duration;
use ;
use CancelReason;
Related lifecycle hooks: mailbox_capacity tunes the bounded mailbox (default
64), termination_strategy chooses whether to drain queued messages on shutdown,
and terminate runs custom cleanup with the cancel reason in hand.
Error handling with anyhow
Make a handler's Reply an anyhow::Result<T> and error handling becomes
uniform end to end:
- Inside the handler, use
?andanyhow!like in any async function. - At the call site, transport failures — the actor is dead, or its reply was
dropped — are converted into
Errfor you, soask_dyn/sendnever panic on a gone actor; you just get aResult.
use ;
async
Cancellation & lifecycle
A Link is a cloneable, reference-counted handle. The actor lives as long as
at least one link does:
link.cancel; // request shutdown with a typed reason
link.cancel_and_wait.await; // ...and wait until it has stopped
// Dropping the last `Link` cancels the actor automatically.
Cancellation is hierarchical: tasks spawned via the actor's context are cancelled
with it, and cancel reasons are typed (type Cancel) so shutdown can carry
meaning.
Examples
The examples/ directory has runnable programs for each pattern:
Full API documentation is on docs.rs.
Testing
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.