Zero-cost, platform-dual, supervision-aware actors for Rust statecharts
The lit-bit actor system brings mature actor model patterns to Rust statecharts with a unique focus on:
- Zero-cost StateMachine integration - Your statecharts become actors with no runtime overhead
- Platform-dual design - Same code runs on embedded (
no_std+ Embassy) and cloud (std+ Tokio) - Supervision-aware lifecycle - OTP-inspired restart strategies prevent cascading failures
- Research-validated performance - <200ns message latency, >100k messages/sec/core targets
Unlike traditional actor frameworks that require boxing and dynamic dispatch, our system leverages Rust's type system for compile-time guarantees and zero-cost abstractions.
pub trait Actor {
type Message: Send + 'static;
// Core message handler - your business logic lives here
async fn on_event(&mut self, msg: Self::Message);
// Lifecycle hooks for supervision
fn on_start(&mut self) -> Result<(), ActorError> { Ok(()) }
fn on_stop(self) -> Result<(), ActorError> { Ok(()) }
fn on_panic(&self, _info: &PanicInfo) -> RestartStrategy {
RestartStrategy::OneForOne
}
}The real power comes from blanket implementation - every StateMachine automatically becomes an Actor:
// Your existing statechart
statechart! {
name: TrafficLight,
event: TrafficEvent,
initial: Red,
// ... states and transitions
}
// Automatically becomes an actor - no boilerplate!
impl Actor for TrafficLight {
type Message = TrafficEvent;
async fn on_event(&mut self, event: TrafficEvent) {
// Direct forwarding - zero overhead
let _ = self.send(&event);
}
}
// Spawn it like any actor
let addr = spawn_actor_tokio::<TrafficLight, 32>(traffic_light);
addr.send(TrafficEvent::TimerExpired).await?;#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RestartStrategy {
OneForOne, // Restart only this actor (default - most common)
OneForAll, // Restart all sibling actors (shared state scenarios)
RestForOne, // Restart this + all actors started after it (dependency chains)
}When to use each:
- OneForOne: Independent actors (sensors, API handlers, state machines)
- OneForAll: Actors sharing critical state (database connections, caches)
- RestForOne: Dependency chains (auth → session → user-data actors)
// Compile-time type safety - can only send valid events
let traffic_addr: Address<TrafficEvent, 32> = spawn_actor_tokio(traffic_light);
let sensor_addr: Address<SensorReading, 16> = spawn_actor_tokio(sensor);
// ✅ This compiles
traffic_addr.send(TrafficEvent::TimerExpired).await?;
// ❌ This fails at compile time
traffic_addr.send(SensorReading::Temperature(25.0)).await?; // Type error!The same Address<Event, N> type provides different semantics based on your target:
// Bounded queue with immediate feedback
let addr = spawn_actor_embassy::<SensorActor, 8>(spawner, sensor);
// Fails immediately if queue is full - no blocking
match addr.try_send(SensorReading::Temperature(25.0)) {
Ok(()) => { /* Message queued */ },
Err(SendError::Full(msg)) => { /* Handle overflow */ },
}// Bounded channel with natural flow control
let addr = spawn_actor_tokio::<ApiHandler, 1000>(handler);
// Awaits if queue is full - natural back-pressure
addr.send(HttpRequest::new("/api/users")).await?;| Use Case | Recommended N | Reasoning |
|---|---|---|
| State machines | 4-16 | Low latency, predictable load |
| Sensors (embedded) | 8-32 | Burst tolerance, memory constraints |
| API handlers | 100-1000 | High throughput, variable load |
| Background workers | 32-128 | Batch processing, moderate latency |
Understanding the actor lifecycle is crucial for effective supervision and debugging. Here's the complete flow from spawn to restart:
flowchart TD
A[🟢 Spawn Actor] --> B[🔧 on_start]
B --> |Success| C[🔄 Event Loop]
B --> |Error| D[💀 Actor Dies]
C --> E{📨 Receive Message}
E --> F[⚡ on_event]
F --> |Success| C
F --> |Panic!| G[⚠️ Panic Detected]
G --> H[🛡️ on_panic]
H --> I{🎯 Restart Strategy?}
I --> |OneForOne| J[♻️ Restart This Actor]
I --> |OneForAll| K[♻️ Restart All Siblings]
I --> |RestForOne| L[♻️ Restart This + Newer]
J --> M[🧹 on_stop]
K --> N[🧹 Stop All Siblings]
L --> O[🧹 Stop Dependency Chain]
M --> B
N --> P[🔧 Restart All Siblings]
O --> Q[🔧 Restart Chain]
P --> B
Q --> B
C --> |Shutdown Signal| R[🛑 Graceful Shutdown]
R --> S[🧹 on_stop]
S --> D
style A fill:#90EE90
style D fill:#FFB6C1
style G fill:#FFD700
style C fill:#87CEEB
| Phase | Purpose | Key Actions | Failure Handling |
|---|---|---|---|
| 🟢 Spawn | Create actor instance | Allocate mailbox, setup supervision | Actor creation fails |
| 🔧 on_start | Initialize state, resources | Load config, connect to services | Return Err → actor dies |
| 🔄 Event Loop | Process messages continuously | Dequeue → process → repeat | Cooperative yielding |
| ⚡ on_event | Handle business logic | State transitions, side effects | Panic → supervision |
| Capture failure information | Log error, determine strategy | Supervisor decides fate | |
| 🛡️ on_panic | Choose restart scope | OneForOne/OneForAll/RestForOne | Strategy guides recovery |
| ♻️ Restart | Clean slate recovery | Stop → start cycle | Fresh state, clean mailbox |
| 🧹 on_stop | Cleanup resources | Close connections, save state | Best-effort cleanup |
| 🛑 Shutdown | Graceful termination | Drain mailbox, notify dependents | Orderly system shutdown |
- Single-threaded processing: Each
on_event()completes before the next message is dequeued - Panic isolation: Actor panics don't crash siblings (unless supervision strategy dictates restart)
- Mailbox preservation: Messages sent during restart are queued and processed after
on_start()succeeds - Supervision timing: Restart decisions happen immediately after panic detection
- Resource cleanup:
on_stop()is always called before actor destruction (best-effort)
┌─────────────────┐
│ Supervisor │
│ (OneForAll) │
└─────────┬───────┘
│
┌────────────┼────────────┐
│ │ │
┌───────▼──────┐ ┌───▼────┐ ┌─────▼─────┐
│ Database │ │ Cache │ │ Session │
│ Manager │ │Manager │ │ Manager │
│ (OneForOne) │ │ │ │ │
└──────────────┘ └────────┘ └─────┬─────┘
│
┌──────▼──────┐
│ User │
│ Actors │
│ (RestForOne)│
└─────────────┘
struct SensorActor {
readings: Vec<f32>,
calibration: Option<CalibrationData>,
}
impl Actor for SensorActor {
type Message = SensorCommand;
async fn on_event(&mut self, cmd: SensorCommand) {
match cmd {
SensorCommand::Read => {
// This might panic if calibration is None
let calibrated = self.calibration.unwrap().apply(raw_reading);
self.readings.push(calibrated);
},
SensorCommand::Calibrate(data) => {
self.calibration = Some(data);
}
}
}
fn on_panic(&self, info: &PanicInfo) -> RestartStrategy {
// Log the panic for debugging
log::error!("Sensor actor panicked: {}", info);
// Restart just this sensor - don't affect siblings
RestartStrategy::OneForOne
}
fn on_start(&mut self) -> Result<(), ActorError> {
// Reset state on restart
self.readings.clear();
self.calibration = None;
log::info!("Sensor actor restarted");
Ok(())
}
}// Supervisor manages a fleet of sensor actors
struct SensorSupervisor {
sensor_addresses: HashMap<SensorId, Address<SensorCommand, 16>>,
}
impl Actor for SensorSupervisor {
type Message = SupervisorCommand;
async fn on_event(&mut self, cmd: SupervisorCommand) {
match cmd {
SupervisorCommand::SensorPanicked { id, strategy } => {
match strategy {
RestartStrategy::OneForOne => {
// Restart just the failed sensor
self.restart_sensor(id).await;
},
RestartStrategy::OneForAll => {
// Restart all sensors (shared calibration corrupted)
self.restart_all_sensors().await;
},
RestartStrategy::RestForOne => {
// Restart this sensor and all newer ones
self.restart_sensor_chain(id).await;
}
}
},
SupervisorCommand::AddSensor(config) => {
let addr = spawn_actor_embassy(spawner, SensorActor::new(config));
self.sensor_addresses.insert(config.id, addr);
}
}
}
}| Aspect | Tokio (std) | Embassy (no_std) |
|---|---|---|
| Throughput | >1M msg/sec | >500k msg/sec |
| Latency | <100ns | <200ns |
| Memory/Actor | ~1KB | ~512B |
| Spawn Cost | ~50ns | ~100ns |
| Back-pressure | Async await | Fail-fast |
| Use Cases | APIs, services, high-throughput | IoT, real-time, resource-constrained |
// ✅ Good: Small, bounded mailboxes
spawn_actor_embassy::<SensorActor, 8>(spawner, sensor);
// ✅ Good: Value types for messages
#[derive(Clone)]
enum SensorMessage {
Reading(f32),
Calibrate { offset: f32, scale: f32 },
}
// ❌ Avoid: Large mailboxes on embedded
spawn_actor_embassy::<SensorActor, 1000>(spawner, sensor); // Too big!
// ❌ Avoid: Heap allocations in messages
enum BadMessage {
Data(Vec<u8>), // Heap allocation!
Text(String), // Heap allocation!
}// ✅ Good: Larger mailboxes for burst handling
spawn_actor_tokio::<ApiHandler, 1000>(handler);
// ✅ Good: Batch processing for throughput
impl Actor for BatchProcessor {
async fn on_event(&mut self, item: WorkItem) {
self.batch.push(item);
if self.batch.len() >= BATCH_SIZE {
self.process_batch().await;
self.batch.clear();
}
}
}// Actual measurements from benchmarks:
// Minimal actor (no_std)
struct MinimalActor;
// Memory: ~64B (just the actor task overhead)
// StateMachine actor (no_std)
struct TrafficLight { state: TrafficState, context: TrafficContext }
// Memory: ~128B (state + context + task overhead)
// Complex actor with mailbox (std)
struct ApiHandler {
db_pool: DbPool,
cache: LruCache<String, Response>,
metrics: Metrics
}
// Memory: ~2KB (includes Tokio task + mailbox + actor state)// DON'T: Shared Arc<Mutex<T>> between actors
struct BadActor {
shared_state: Arc<Mutex<HashMap<String, Value>>>, // Defeats actor isolation!
}
// DO: Message passing for coordination
enum CoordinatorMessage {
Get { key: String, reply_to: Address<GetResponse, 8> },
Set { key: String, value: Value },
}// DON'T: Spawn and forget
let addr = spawn_actor_tokio(risky_actor);
// If this actor panics, no one knows or cares
// DO: Proper supervision
struct Supervisor {
children: Vec<Address<ChildMessage, 16>>,
}
impl Actor for Supervisor {
async fn on_event(&mut self, msg: SupervisorMessage) {
match msg {
SupervisorMessage::ChildPanicked { id } => {
// Restart the child, update monitoring, etc.
self.restart_child(id).await;
}
}
}
}// DON'T: Massive mailboxes hide back-pressure
spawn_actor_tokio::<SlowProcessor, 100_000>(processor); // Memory bomb!
// DO: Right-sized for your use case
spawn_actor_tokio::<SlowProcessor, 32>(processor); // Natural back-pressure// DON'T: Block the executor
impl Actor for BadActor {
async fn on_event(&mut self, msg: Message) {
std::thread::sleep(Duration::from_secs(1)); // Blocks executor!
let result = reqwest::blocking::get("http://api.example.com"); // Blocking!
}
}
// DO: Use async APIs
impl Actor for GoodActor {
async fn on_event(&mut self, msg: Message) {
tokio::time::sleep(Duration::from_secs(1)).await; // Yields properly
let result = reqwest::get("http://api.example.com").await; // Async!
}
}use lit_bit_core::actor::{Actor, spawn_actor_tokio};
#[derive(Debug)]
struct Counter {
value: u32,
}
#[derive(Debug, Clone)]
enum CounterMessage {
Increment,
Decrement,
GetValue { reply_to: tokio::sync::oneshot::Sender<u32> },
}
impl Actor for Counter {
type Message = CounterMessage;
async fn on_event(&mut self, msg: CounterMessage) {
match msg {
CounterMessage::Increment => self.value += 1,
CounterMessage::Decrement => self.value = self.value.saturating_sub(1),
CounterMessage::GetValue { reply_to } => {
let _ = reply_to.send(self.value);
}
}
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let counter = Counter { value: 0 };
let addr = spawn_actor_tokio::<Counter, 16>(counter);
// Send some messages
addr.send(CounterMessage::Increment).await?;
addr.send(CounterMessage::Increment).await?;
// Get the value
let (tx, rx) = tokio::sync::oneshot::channel();
addr.send(CounterMessage::GetValue { reply_to: tx }).await?;
let value = rx.await?;
println!("Counter value: {}", value); // Prints: Counter value: 2
Ok(())
}#![no_std]
#![no_main]
use embassy_executor::Spawner;
use lit_bit_core::actor::{Actor, spawn_actor_embassy};
#[derive(Debug)]
struct LedController {
is_on: bool,
}
#[derive(Debug, Clone)]
enum LedMessage {
TurnOn,
TurnOff,
Toggle,
}
impl Actor for LedController {
type Message = LedMessage;
async fn on_event(&mut self, msg: LedMessage) {
match msg {
LedMessage::TurnOn => {
self.is_on = true;
// gpio.set_high(); // Actual GPIO control
},
LedMessage::TurnOff => {
self.is_on = false;
// gpio.set_low();
},
LedMessage::Toggle => {
self.is_on = !self.is_on;
// if self.is_on { gpio.set_high() } else { gpio.set_low() }
}
}
}
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let led = LedController { is_on: false };
let addr = spawn_actor_embassy::<LedController, 8>(spawner, led);
// Blink pattern
loop {
addr.try_send(LedMessage::Toggle).ok();
embassy_time::Timer::after(embassy_time::Duration::from_millis(500)).await;
}
}use lit_bit_core::actor::{Actor, RestartStrategy, spawn_actor_tokio};
use std::collections::HashMap;
struct WorkerSupervisor {
workers: HashMap<u32, Address<WorkerMessage, 16>>,
next_worker_id: u32,
}
#[derive(Debug, Clone)]
enum SupervisorMessage {
SpawnWorker,
WorkerPanicked { id: u32 },
DistributeWork { task: WorkTask },
}
impl Actor for WorkerSupervisor {
type Message = SupervisorMessage;
async fn on_event(&mut self, msg: SupervisorMessage) {
match msg {
SupervisorMessage::SpawnWorker => {
let worker = Worker::new(self.next_worker_id);
let addr = spawn_actor_tokio::<Worker, 16>(worker);
self.workers.insert(self.next_worker_id, addr);
self.next_worker_id += 1;
},
SupervisorMessage::WorkerPanicked { id } => {
// Remove the failed worker
self.workers.remove(&id);
// Spawn a replacement
let worker = Worker::new(id);
let addr = spawn_actor_tokio::<Worker, 16>(worker);
self.workers.insert(id, addr);
log::info!("Restarted worker {}", id);
},
SupervisorMessage::DistributeWork { task } => {
// Round-robin work distribution
if let Some(addr) = self.workers.values().next() {
let _ = addr.send(WorkerMessage::ProcessTask(task)).await;
}
}
}
}
}
struct Worker {
id: u32,
processed_count: u32,
}
impl Worker {
fn new(id: u32) -> Self {
Self { id, processed_count: 0 }
}
}
impl Actor for Worker {
type Message = WorkerMessage;
async fn on_event(&mut self, msg: WorkerMessage) {
match msg {
WorkerMessage::ProcessTask(task) => {
// Simulate work that might panic
if task.is_poison() {
panic!("Worker {} encountered poison task!", self.id);
}
// Normal processing
self.processed_count += 1;
log::info!("Worker {} processed task (total: {})", self.id, self.processed_count);
}
}
}
fn on_panic(&self, _info: &std::panic::PanicInfo) -> RestartStrategy {
// Notify supervisor of panic
// (In real implementation, supervisor would be notified automatically)
RestartStrategy::OneForOne
}
}- Testing Guide - Learn to test actors, supervision, and back-pressure scenarios
- Examples Repository - Real-world actor patterns and use cases
- Performance Guide - Benchmarking, profiling, and optimization techniques
- TestKit utilities - Mock actors, message capturing, deterministic execution
- Actor visualization - Generate supervision tree diagrams
- Performance profiling - Message latency and throughput analysis
- Monitoring integration - Metrics, tracing, and health checks
- Graceful shutdown - Clean actor termination patterns
- Error recovery - Advanced supervision strategies and circuit breakers
Built with ❤️🦀 for the Rust community. Questions? Open an issue or start a discussion!