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// Test code asserts invariants directly; unwrap/expect keeps failures loud.
79#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
80
81pub mod error;
82mod executor;
83mod handle;
84mod mailbox;
85// Loom model double of the mailbox protocol — compiled only under
86// `--cfg loom` (see the module docs for what is modeled vs trusted).
87#[cfg(loom)]
88pub mod loom_mailbox;
89#[cfg(feature = "unsafe-pool")]
90pub mod memory_pool;
91pub mod policy;
92pub mod queue;
93mod registry;
94#[cfg(feature = "serde")]
95pub mod rpc;
96mod scheduler;
97// The deterministic sim module relies on invariants (unique ids, peeked
98// non-empty queues, uncontended Mutexes) where unwrap/expect is intentional.
99#[cfg_attr(feature = "sim", allow(clippy::unwrap_used))]
100#[cfg(feature = "sim")]
101pub mod sim;
102pub mod supervisor;
103#[cfg(feature = "zero-copy")]
104pub mod zero_copy;
105
106pub use error::{Error, Result};
107pub use executor::{ActorExecutor, ExecutionResult, NullExecutor};
108pub use handle::{ActorBuilder, ActorHandle};
109pub use mailbox::{Mailbox, MailboxConfig};
110pub use policy::{NoopPolicy, ResourcePolicy};
111pub use queue::{create_local_queue, PriorityQueue, WorkQueue, WorkStealer};
112pub use registry::{ActorRegistry, ActorState, RegistryStats};
113#[cfg(feature = "serde")]
114pub use rpc::{
115 process_rpc_message, RpcClient, RpcEnvelope, RpcError, RpcHandler, RpcMessage, RpcRegistry,
116 RpcRequest, RpcResponse,
117};
118pub use scheduler::{
119 ActorScheduler, SchedulerConfig, SchedulerStats, StealerRegistry, WorkerStatsInfo,
120};
121pub use supervisor::{
122 ActorConfig, ChildSpec, ChildState, DegradationController, DegradationDecision,
123 EscalationAction, ExitReason, FnResourceMonitor, PressureLevel, ResourceMonitor, RestartPolicy,
124 StaticResourceMonitor, SupervisedChild, SupervisionStrategy, Supervisor, SupervisorError,
125 SupervisorHandle, SupervisorStats, SupervisorTree, SupervisorTreeStats,
126};
127
128use std::sync::Arc;
129use uuid::Uuid;
130
131/// Unique identifier for an actor.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
133#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134pub struct ActorId(pub Uuid);
135
136impl ActorId {
137 /// Generate a new random actor ID.
138 pub fn new() -> Self {
139 Self(Uuid::new_v4())
140 }
141}
142
143impl Default for ActorId {
144 fn default() -> Self {
145 Self::new()
146 }
147}
148
149/// Priority level for actor messages.
150#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
151pub enum Priority {
152 /// Low priority (background tasks)
153 Low = 0,
154 /// Normal priority (default)
155 #[default]
156 Normal = 1,
157 /// High priority (time-sensitive)
158 High = 2,
159 /// Critical priority (system messages)
160 Critical = 3,
161}
162
163/// A message sent to an actor.
164#[derive(Debug, Clone)]
165pub struct Message {
166 /// Sender actor ID (None for system messages)
167 pub sender: Option<ActorId>,
168 /// Message type/payload
169 pub payload: MessagePayload,
170 /// Message priority
171 pub priority: Priority,
172}
173
174impl Default for Message {
175 fn default() -> Self {
176 Self {
177 sender: None,
178 payload: MessagePayload::Empty,
179 priority: Priority::Normal,
180 }
181 }
182}
183
184/// Payload of a message.
185#[derive(Debug, Clone, Default)]
186pub enum MessagePayload {
187 /// Start the actor
188 Start,
189 /// Stop the actor
190 Stop,
191 /// Custom binary payload
192 Custom(Vec<u8>),
193 /// System signal
194 Signal(Signal),
195 /// Default (empty)
196 #[default]
197 Empty,
198}
199
200/// System signals.
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum Signal {
203 /// Pause execution
204 Pause,
205 /// Resume execution
206 Resume,
207 /// Restart the actor
208 Restart,
209}
210
211/// Trait for actor behavior.
212pub trait Actor: Send + Sync + 'static {
213 /// Handle a message.
214 fn handle(
215 &mut self,
216 msg: Message,
217 ctx: &ActorContext,
218 ) -> impl std::future::Future<Output = Result<()>> + Send;
219}
220
221/// Context provided to actors during message handling.
222pub struct ActorContext {
223 /// Actor's own ID
224 pub id: ActorId,
225 /// Scheduler handle for spawning work
226 scheduler: Arc<ActorScheduler>,
227}
228
229impl ActorContext {
230 /// Send a message to another actor.
231 pub async fn send(&self, target: ActorId, payload: MessagePayload) -> Result<()> {
232 self.scheduler
233 .send(
234 target,
235 Message {
236 sender: Some(self.id),
237 payload,
238 priority: Priority::Normal,
239 },
240 )
241 .await
242 }
243
244 /// Send a high-priority message to another actor.
245 pub async fn send_high(&self, target: ActorId, payload: MessagePayload) -> Result<()> {
246 self.scheduler
247 .send(
248 target,
249 Message {
250 sender: Some(self.id),
251 payload,
252 priority: Priority::High,
253 },
254 )
255 .await
256 }
257}
258
259/// Internal helper: a minimal single-threaded runtime for doctests/examples.
260#[doc(hidden)]
261#[allow(clippy::expect_used)] // doctest support: failure here is a bug, not recoverable
262pub fn rt() -> tokio::runtime::Runtime {
263 tokio::runtime::Builder::new_current_thread()
264 .enable_all()
265 .build()
266 .expect("failed to build runtime")
267}