Skip to main content

actor_kit/
lib.rs

1//! # actor-kit
2//!
3//! A work-stealing actor runtime with OTP-style supervision, designed for
4//! hosting 100,000+ actors per node with efficient load balancing.
5//!
6//! ## How this differs from task-per-actor runtimes
7//!
8//! Most Rust actor frameworks (`ractor`, `kameo`) run each actor as its own
9//! tokio task: the async runtime multiplexes tasks onto its worker threads, and
10//! "scheduling" is just task polling. `actor-kit` takes a different approach:
11//!
12//! - **Actors are not tasks.** An actor is a registry entry (ID + state +
13//!   bounded mailbox). Work arrives as `Task` items on crossbeam deques.
14//! - **A fixed pool of OS worker threads** pulls work from (1) a priority
15//!   injector, (2) their local FIFO deque, (3) a global injector, then
16//!   (4) steals from other workers — in that order. Idle workers back off
17//!   (spin, then sleep) instead of burning a reactor tick per actor.
18//! - **Per-actor mailbox with hard backpressure.** Bounded lock-free
19//!   `ArrayQueue` + tokio `Semaphore`; senders block or fail once the
20//!   mailbox crosses its backpressure threshold (default 80% of capacity).
21//!   A hot actor cannot grow an unbounded task queue.
22//! - **Supervision is data, not tasks.** Erlang/OTP strategies
23//!   (OneForOne / OneForAll / RestForOne / SimpleOneForOne), restart
24//!   policies (Permanent / Transient / Temporary), max-restarts-within-window
25//!   rate limiting, escalation, and hierarchical [`SupervisorTree`]s are
26//!   plain data structures you drive explicitly — no hidden per-supervisor
27//!   tokio tasks, no implicit restart side effects.
28//!
29//! The trade-offs are honest: message handlers here are state-machine steps
30//! (state lives in the registry, not in an async frame), so actors that need
31//! to `.await` I/O mid-handler are a better fit for task-per-actor frameworks.
32//! This runtime shines when you have very many mostly-CPU-light actors,
33//! want predictable memory per actor, and need OTP-style fault tolerance
34//! semantics with real work stealing.
35//!
36//! ## Example
37//!
38//! ```no_run
39//! use actor_kit::{ActorBuilder, ActorScheduler, MessagePayload, SchedulerConfig};
40//! use std::sync::Arc;
41//!
42//! # fn main() -> actor_kit::Result<()> {
43//! // Create and start a 4-worker work-stealing scheduler.
44//! let scheduler = Arc::new(ActorScheduler::new(SchedulerConfig::new().workers(4)));
45//! scheduler.start()?;
46//!
47//! // Spawn an actor and start it.
48//! let handle = ActorBuilder::new().name("my-actor").spawn(&scheduler)?;
49//! # actor_kit::rt().block_on(async {
50//! handle.start().await?;
51//!
52//! // Send messages (backpressured, bounded mailbox).
53//! handle.send(MessagePayload::Custom(vec![1, 2, 3])).await?;
54//! assert!(handle.is_running());
55//! # Ok::<(), actor_kit::Error>(()) }).unwrap();
56//! # scheduler.stop();
57//! # Ok(())
58//! # }
59//! ```
60//!
61//! ## Feature flags
62//!
63//! - `std` *(default)* — passthrough; the runtime is std-based.
64//! - `serde` — typed RPC (`rpc` module), serde impls on public types.
65//! - `zero-copy` — rkyv-backed zero-copy message path (`zero_copy` module).
66//! - `unsafe-pool` — bump-allocator memory pool (`memory_pool` module).
67//!   **The only `unsafe` code in the crate lives behind this flag.**
68//! - `sim` — deterministic simulation testing (`sim` module): a seeded
69//!   single-threaded scheduler replacement, virtual clock, fault injection,
70//!   and seed-replay traces. TigerBeetle-style DST; see the `sim` module docs.
71//! - `full` — all of the above.
72//!
73//! The crate carries `#![deny(unsafe_code)]`; `memory_pool` (behind
74//! `unsafe-pool`) is the single, loudly documented exception.
75
76#![deny(unsafe_code)]
77#![warn(missing_docs)]
78
79pub mod error;
80mod executor;
81mod handle;
82mod mailbox;
83#[cfg(feature = "unsafe-pool")]
84pub mod memory_pool;
85pub mod policy;
86pub mod queue;
87mod registry;
88#[cfg(feature = "serde")]
89pub mod rpc;
90mod scheduler;
91#[cfg(feature = "sim")]
92pub mod sim;
93pub mod supervisor;
94#[cfg(feature = "zero-copy")]
95pub mod zero_copy;
96
97pub use error::{Error, Result};
98pub use executor::{ActorExecutor, ExecutionResult, NullExecutor};
99pub use handle::{ActorBuilder, ActorHandle};
100pub use mailbox::{Mailbox, MailboxConfig};
101pub use policy::{NoopPolicy, ResourcePolicy};
102pub use queue::{create_local_queue, PriorityQueue, WorkQueue, WorkStealer};
103pub use registry::{ActorRegistry, ActorState, RegistryStats};
104#[cfg(feature = "serde")]
105pub use rpc::{
106    process_rpc_message, RpcClient, RpcEnvelope, RpcError, RpcHandler, RpcMessage, RpcRegistry,
107    RpcRequest, RpcResponse,
108};
109pub use scheduler::{
110    ActorScheduler, SchedulerConfig, SchedulerStats, StealerRegistry, WorkerStatsInfo,
111};
112pub use supervisor::{
113    ActorConfig, ChildSpec, ChildState, DegradationController, DegradationDecision,
114    EscalationAction, ExitReason, FnResourceMonitor, PressureLevel, ResourceMonitor, RestartPolicy,
115    StaticResourceMonitor, SupervisedChild, SupervisionStrategy, Supervisor, SupervisorError,
116    SupervisorHandle, SupervisorStats, SupervisorTree, SupervisorTreeStats,
117};
118
119use std::sync::Arc;
120use uuid::Uuid;
121
122/// Unique identifier for an actor.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
124#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
125pub struct ActorId(pub Uuid);
126
127impl ActorId {
128    /// Generate a new random actor ID.
129    pub fn new() -> Self {
130        Self(Uuid::new_v4())
131    }
132}
133
134impl Default for ActorId {
135    fn default() -> Self {
136        Self::new()
137    }
138}
139
140/// Priority level for actor messages.
141#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
142pub enum Priority {
143    /// Low priority (background tasks)
144    Low = 0,
145    /// Normal priority (default)
146    #[default]
147    Normal = 1,
148    /// High priority (time-sensitive)
149    High = 2,
150    /// Critical priority (system messages)
151    Critical = 3,
152}
153
154/// A message sent to an actor.
155#[derive(Debug, Clone)]
156pub struct Message {
157    /// Sender actor ID (None for system messages)
158    pub sender: Option<ActorId>,
159    /// Message type/payload
160    pub payload: MessagePayload,
161    /// Message priority
162    pub priority: Priority,
163}
164
165impl Default for Message {
166    fn default() -> Self {
167        Self {
168            sender: None,
169            payload: MessagePayload::Empty,
170            priority: Priority::Normal,
171        }
172    }
173}
174
175/// Payload of a message.
176#[derive(Debug, Clone, Default)]
177pub enum MessagePayload {
178    /// Start the actor
179    Start,
180    /// Stop the actor
181    Stop,
182    /// Custom binary payload
183    Custom(Vec<u8>),
184    /// System signal
185    Signal(Signal),
186    /// Default (empty)
187    #[default]
188    Empty,
189}
190
191/// System signals.
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum Signal {
194    /// Pause execution
195    Pause,
196    /// Resume execution
197    Resume,
198    /// Restart the actor
199    Restart,
200}
201
202/// Trait for actor behavior.
203pub trait Actor: Send + Sync + 'static {
204    /// Handle a message.
205    fn handle(
206        &mut self,
207        msg: Message,
208        ctx: &ActorContext,
209    ) -> impl std::future::Future<Output = Result<()>> + Send;
210}
211
212/// Context provided to actors during message handling.
213pub struct ActorContext {
214    /// Actor's own ID
215    pub id: ActorId,
216    /// Scheduler handle for spawning work
217    scheduler: Arc<ActorScheduler>,
218}
219
220impl ActorContext {
221    /// Send a message to another actor.
222    pub async fn send(&self, target: ActorId, payload: MessagePayload) -> Result<()> {
223        self.scheduler
224            .send(
225                target,
226                Message {
227                    sender: Some(self.id),
228                    payload,
229                    priority: Priority::Normal,
230                },
231            )
232            .await
233    }
234
235    /// Send a high-priority message to another actor.
236    pub async fn send_high(&self, target: ActorId, payload: MessagePayload) -> Result<()> {
237        self.scheduler
238            .send(
239                target,
240                Message {
241                    sender: Some(self.id),
242                    payload,
243                    priority: Priority::High,
244                },
245            )
246            .await
247    }
248}
249
250/// Internal helper: a minimal single-threaded runtime for doctests/examples.
251#[doc(hidden)]
252pub fn rt() -> tokio::runtime::Runtime {
253    tokio::runtime::Builder::new_current_thread()
254        .enable_all()
255        .build()
256        .expect("failed to build runtime")
257}