joerl 🦀
An Erlang-inspired actor model library for Rust, named in tribute to Joe Armstrong, the creator of Erlang.
Features
- 🎭 Actor Model: Lightweight actors that communicate via message passing
- 🤖 GenServer: Erlang's gen_server behavior with call/cast semantics
- 🔄 GenStatem DSL: Mermaid-based state machine definition with compile-time validation
- 🌳 Supervision Trees: Robust error handling with configurable restart strategies
- 🔗 Links & Monitors: Actor relationships for failure detection and propagation
- 📬 Bounded Mailboxes: Backpressure support to prevent resource exhaustion
- 🌐 Distributed Messaging: Full location-transparent remote messaging with EPMD discovery
- ⚡ Async/Await: Built on tokio for excellent performance
- 📊 Telemetry & Observability: Comprehensive metrics and tracing with Prometheus/OpenTelemetry support
- 🦀 Erlang Conventions: Familiar API for Erlang/OTP developers
Installation
Add this to your Cargo.toml:
[]
= "0.4"
= { = "1", = ["full"] }
= "0.1"
Quick Start
use ;
use async_trait;
// Define your actor
async
Core Concepts
Actors
Actors are the fundamental unit of computation. Each actor:
- Has a unique
Pid(Process ID) - Processes messages sequentially from its mailbox
- Can spawn other actors
- Can send messages to other actors
- Can link to and monitor other actors
Message Passing
Actors communicate by sending messages. Messages are type-erased using Box<dyn Any>:
actor_ref.send.await?;
actor_ref.send.await?;
Links and Monitors
Links create bidirectional relationships between actors. If one fails, both fail:
system.link?;
Monitors create unidirectional observation. The monitoring actor receives a DOWN signal when the monitored actor terminates:
let monitor_ref = actor_ref.monitor?;
Supervision Trees
Supervisors monitor child actors and restart them according to strategies:
use ;
let spec = new
.child
.child;
let supervisor = spawn_supervisor;
Restart Strategies:
OneForOne: Restart only the failed childOneForAll: Restart all children when one failsRestForOne: Restart the failed child and all children started after it
GenServer (Generic Server Behavior)
For structured stateful actors with synchronous call/reply and asynchronous cast semantics:
use ;
;
// Usage
let counter = spawn;
let value = counter.call.await?; // Synchronous
counter.cast.await?; // Asynchronous
GenStatem (Generic State Machine with DSL)
For finite state machines, joerl provides a powerful DSL using Mermaid state diagrams with compile-time validation:
use ;
use Arc;
// Usage
let system = new;
let initial_data = Turnstile ;
let turnstile = Turnstile;
turnstile.send.await.unwrap;
turnstile.send.await.unwrap;
Features:
- Mermaid Syntax: Define FSM using standard Mermaid state diagram syntax
- Compile-Time Validation: FSM structure validated at compile time
- Auto-Generated Types: State and Event enums generated from the diagram
- Transition Validation: Invalid transitions detected and logged at runtime
- Terminal States: Automatic termination when reaching
[*]end state - Callbacks:
on_transition,on_enter, andon_terminatehooks
The macro generates:
{Name}Stateenum with all states{Name}Eventenum with all eventsTransitionResultenum for transition outcomes- Boilerplate Actor implementation with validation
Named Processes
Like Erlang, joerl supports registering actors with names for easy lookup:
let system = new;
let worker = system.spawn;
// Register with a name
system.register.unwrap;
// Look up by name
if let Some = system.whereis
// List all registered names
let names = system.registered;
// Unregister
system.unregister.unwrap;
Names are automatically cleaned up when actors terminate. Actors can look up names from within their context:
Scheduled Messaging
joerl supports Erlang-style send_after for delayed message delivery:
use Destination;
use Duration;
let system = new;
let actor = system.spawn;
// Schedule a message to be sent after 5 seconds
let timer_ref = system.send_after.await;
// Cancel the timer if needed
system.cancel_timer?;
// Schedule to a named process
system.send_after.await;
Actors can schedule messages from within their context:
Trapping Exits
Actors can trap exit signals to handle failures gracefully:
Erlang Terminology Mapping
|| Erlang | joerl | Description |
|--------|-------|-------------|
| spawn/1 | system.spawn(actor) | Spawn a new actor |
| register(Name, Pid) | system.register(name, pid) | Register a process with a name |
| unregister(Name) | system.unregister(name) | Unregister a name |
| whereis(Name) | system.whereis(name) | Look up a process by name |
| registered() | system.registered() | List all registered names |
| erlang:send_after(Time, Dest, Msg) | system.send_after(dest, msg, duration) | Schedule delayed message |
| erlang:cancel_timer(TRef) | system.cancel_timer(tref) | Cancel a scheduled timer |
| gen_server:start_link/3 | gen_server::spawn(&system, server) | Spawn a gen_server |
| gen_server:call/2 | server_ref.call(request) | Synchronous call |
| gen_server:cast/2 | server_ref.cast(message) | Asynchronous cast |
| gen_statem:start_link/3 | #[gen_statem(fsm = "...")] | Define state machine with DSL |
| Pid | Pid | Process identifier |
| ! (send) | actor_ref.send(msg) | Send a message |
| link/1 | system.link(pid1, pid2) | Link two actors |
| monitor/2 | actor_ref.monitor(from) | Monitor an actor |
| process_flag(trap_exit, true) | ctx.trap_exit(true) | Trap exit signals |
| {'EXIT', Pid, Reason} | Signal::Exit { from, reason } | Exit signal |
| {'DOWN', Ref, process, Pid, Reason} | Signal::Down { reference, pid, reason } | Down signal |
Examples
See the examples/ directory for more examples:
counter.rs- Simple counter actorgen_server_counter.rs- GenServer (gen_server behavior) exampleturnstile.rs- GenStatem DSL with Mermaid state diagramgen_statem_turnstile.rs- Alternative GenStatem exampledocument_workflow.rs- Complex FSM with approval workflow and revision cycleping_pong.rs- Two actors communicatingsupervision_tree.rs- Supervision tree examplelink_monitor.rs- Links and monitors demonstrationnamed_processes.rs- Named process registry demonstrationscheduled_messaging.rs- Delayed message delivery with timerspanic_handling.rs- Comprehensive panic handling demonstration (Erlang/OTP-style)telemetry_demo.rs- Telemetry and metrics with Prometheus exportserialization_example.rs- Trait-based message serializationremote_actors.rs- Distributed actors conceptual foundationremote_ping_pong.rs- Remote messaging between nodesdistributed_chat.rs- Multi-node chat system over TCPdistributed_cluster.rs- Multi-node cluster with EPMD discoverydistributed_system_example.rs- Distributed system example (uses deprecated API, see remote_ping_pong.rs for current API)epmd_server.rs- Standalone EPMD server
Run examples with:
Distributed Actors Examples
joerl now features a unified ActorSystem with true location transparency! The same API works for both local and distributed scenarios - just like Erlang/OTP.
Quick Start with EPMD:
# Terminal 1: Start EPMD server
# Terminal 2: Start first node
# Terminal 3: Start second node
# Nodes automatically discover and connect!
Remote Ping-Pong Example:
# Terminal 1: Start server node
# Terminal 2: Start client node
This demonstrates:
- Unified ActorSystem:
ActorSystem::new_distributed()- same API as local! - Location Transparency:
ctx.send()works for both local and remote actors - Erlang-Style Helpers:
nodes(),node(),is_process_alive(),connect_to_node() - Bidirectional Connections: Handshake protocol establishes auto-bidirectional links
- Ping/Pong RPC: Remote process liveness checking
Conceptual Examples:
The remote_actors example demonstrates distributed concepts using multiple local systems:
The distributed_chat example shows a real TCP-based distributed chat:
# Terminal 1
# Terminal 2
For detailed documentation on building distributed systems with joerl, see DISTRIBUTED.md.
Telemetry and Observability
joerl provides comprehensive telemetry support for production monitoring:
[]
= { = "0.4", = ["telemetry"] }
use telemetry;
use PrometheusBuilder;
async
Available Metrics:
joerl_actors_spawned_total- Total actors spawnedjoerl_actors_active- Current active actorsjoerl_messages_sent_total- Message throughputjoerl_message_processing_duration_seconds- Processing latencyjoerl_supervisor_restarts_total- Supervisor restart events- And more...
See TELEMETRY.md for comprehensive documentation and integration examples with Prometheus, Grafana, OpenTelemetry, and Datadog.
Architecture
The library is organized into several modules:
actor- Core actor trait and contextsystem- Actor system runtime and registrymessage- Message types and signalsmailbox- Bounded mailbox implementationsupervisor- Supervision trees and restart strategiestelemetry- Metrics and observability (optional)error- Error types and resultspid- Process identifier
Testing
Run the test suite:
Check code coverage:
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
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.
Acknowledgments
This library is dedicated to the memory of Joe Armstrong (1950-2019), whose work on Erlang has inspired generations of developers to build robust, concurrent systems.