Skip to content

Commit b07bc3a

Browse files
am-kantoxwarp-agent
andcommitted
feat: add named process registry and scheduled messaging
- Add Registry for actor name registration (register, whereis, unregister) - Add Scheduler for delayed message delivery (send_after, cancel_timer) - Add ActorContext methods for registry and scheduler access - Add telemetry metrics for registry and scheduler operations - Add comprehensive integration tests and examples - Fix doctest compilation errors in scheduler.rs and system.rs This implements Erlang-style named processes and timer functionality, enabling location-transparent messaging and delayed message delivery. Co-Authored-By: Warp <agent@warp.dev>
1 parent 11bc2df commit b07bc3a

16 files changed

Lines changed: 2276 additions & 0 deletions

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

NAMED_PROCESSES.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Overview
2+
Implement Erlang-style named process registry and scheduled messaging to make joerl more feature-complete for Erlang developers. This includes:
3+
* `whereis/1` - Look up processes by registered name
4+
* `register/2` - Register a process with a name
5+
* `unregister/1` - Unregister a named process
6+
* `send/2` - Send to named process or Pid
7+
* `send_after/3` - Schedule a message to be sent after a delay
8+
# Current State
9+
joerl currently only supports addressing actors by Pid. The ActorSystem maintains a DashMap registry mapping Pid to ActorEntry (joerl/src/system.rs:279). Messages are sent via `ActorSystem::send()` and `ActorRef::send()` methods which only accept Pid as the recipient.
10+
There is no timer/scheduler subsystem, and all message sending is immediate.
11+
# Proposed Changes
12+
## 1. Create Named Registry Module
13+
Create joerl/src/registry.rs for name registration functionality.
14+
### Registry Structure
15+
* Use `DashMap<String, Pid>` for thread-safe name-to-Pid mapping
16+
* Use `DashMap<Pid, String>` for reverse lookup (Pid-to-name)
17+
* Support both local and distributed names (node-scoped)
18+
* Automatic cleanup when actors terminate
19+
### Key Types
20+
* `RegisteredName` - Newtype wrapper for registered names with validation
21+
* `RegistrationError` - Specific errors for name conflicts, invalid names
22+
### Core Functions
23+
* `register(name: String, pid: Pid) -> Result<()>` - Register name (error if already registered)
24+
* `unregister(name: &str) -> Result<Pid>` - Remove registration, return Pid
25+
* `whereis(name: &str) -> Option<Pid>` - Look up Pid by name
26+
* `registered() -> Vec<String>` - List all registered names
27+
* `cleanup_pid(pid: Pid)` - Internal cleanup when actor dies
28+
## 2. Integrate Registry into ActorSystem
29+
Add registry field to ActorSystem (joerl/src/system.rs:278).
30+
### Changes to ActorSystem
31+
* Add `registry: Arc<Registry>` field
32+
* Initialize registry in `new()` and `new_distributed()`
33+
* Expose public API methods that delegate to registry:
34+
* `register(&self, name: impl Into<String>, pid: Pid) -> Result<()>`
35+
* `unregister(&self, name: &str) -> Result<Pid>`
36+
* `whereis(&self, name: &str) -> Option<Pid>`
37+
* `registered(&self) -> Vec<String>`
38+
* Call `registry.cleanup_pid()` in `cleanup_actor()` (system.rs:717)
39+
### Named Send Support
40+
* Create `Destination` enum: `Pid(Pid) | Name(String)`
41+
* Add `send_to(&self, dest: Destination, msg: Message) -> Result<()>`
42+
* Modify existing `send()` to work through this abstraction
43+
* Update ActorRef to support named sends
44+
## 3. Create Scheduler Module
45+
Create joerl/src/scheduler.rs for delayed message scheduling.
46+
### Scheduler Structure
47+
* Use tokio's time utilities (`tokio::time::sleep`, `Instant`)
48+
* Store scheduled messages as heap-ordered by delivery time
49+
* Each scheduled message spawns a tokio task that sleeps then sends
50+
### Key Types
51+
* `ScheduledMessage` - Contains destination, message, delivery time
52+
* `SchedulerHandle` - Cancellable reference to scheduled message
53+
* `TimerRef` - Erlang-style timer reference for cancellation
54+
### Core Functions
55+
* `send_after(dest: Destination, msg: Message, duration: Duration) -> TimerRef`
56+
* `cancel_timer(timer_ref: TimerRef) -> Result<bool>` - Returns true if cancelled
57+
* `send_interval(dest: Destination, msg: Message, interval: Duration) -> TimerRef` (bonus)
58+
### Implementation Strategy
59+
* Each `send_after` spawns a detached tokio task
60+
* Task sleeps for duration, then calls `ActorSystem::send_to()`
61+
* Store cancellation tokens in `DashMap<TimerRef, CancellationToken>`
62+
* Use `tokio_util::sync::CancellationToken` for cancellation
63+
## 4. Update Message Types
64+
Add timer-related message types to joerl/src/message.rs if needed for timer expiry notifications.
65+
## 5. Extend ActorContext
66+
Add convenience methods to ActorContext (joerl/src/actor.rs:100):
67+
* `send_after(&self, dest: Destination, msg: Message, duration: Duration) -> Result<TimerRef>`
68+
* `whereis(&self, name: &str) -> Option<Pid>`
69+
* Allow actors to easily schedule messages from within their context
70+
## 6. Update Error Types
71+
Add new error variants to ActorError (joerl/src/error.rs):
72+
* `NameAlreadyRegistered(String)`
73+
* `NameNotRegistered(String)`
74+
* `InvalidName(String)`
75+
* `TimerNotFound(u64)`
76+
## 7. Telemetry Integration
77+
Add metrics for registry and scheduler (if telemetry feature enabled):
78+
* `joerl_registry_size` - Current number of registered names
79+
* `joerl_registry_lookups_total` - Registry lookup counter
80+
* `joerl_registry_conflicts_total` - Registration conflict counter
81+
* `joerl_scheduled_messages_total` - Messages scheduled
82+
* `joerl_scheduled_messages_cancelled_total` - Timers cancelled
83+
## 8. Documentation and Examples
84+
### Update README.md
85+
Add section on named processes and scheduled messaging with examples.
86+
### Create Examples
87+
* `examples/named_processes.rs` - Registry demonstration
88+
* `examples/scheduled_messaging.rs` - Timer demonstration
89+
* Update existing examples to show optional use of names
90+
### Update Erlang Terminology Mapping
91+
Add to README.md (line 301):
92+
* `register(Name, Pid)` -> `system.register(name, pid)`
93+
* `unregister(Name)` -> `system.unregister(name)`
94+
* `whereis(Name)` -> `system.whereis(name)`
95+
* `registered()` -> `system.registered()`
96+
* `erlang:send_after(Time, Pid, Msg)` -> `system.send_after(dest, msg, duration)`
97+
* `erlang:cancel_timer(TRef)` -> `system.cancel_timer(tref)`
98+
### Update WARP.md
99+
Document the new registry and scheduler modules in the Architecture section.
100+
## 9. Testing
101+
### Registry Tests
102+
* Test basic register/unregister/whereis
103+
* Test name conflicts (should error)
104+
* Test automatic cleanup on actor death
105+
* Test concurrent registration attempts
106+
* Test distributed name registration (node-scoped)
107+
* Property tests for registry consistency
108+
### Scheduler Tests
109+
* Test basic send_after delivery
110+
* Test timer cancellation before delivery
111+
* Test message ordering with multiple scheduled messages
112+
* Test scheduled message to named destination
113+
* Test scheduled messages survive actor restarts
114+
* Test timer behaviour when destination dies
115+
### Integration Tests
116+
* Named processes with supervision trees
117+
* Scheduled messages in distributed systems
118+
* Registry with monitors and links
119+
## 10. Export Public API
120+
Update joerl/src/lib.rs:
121+
* Add `pub mod registry;`
122+
* Add `pub mod scheduler;`
123+
* Re-export key types: `RegisteredName`, `Destination`, `TimerRef`
124+
* Update documentation with new capabilities

README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,87 @@ The macro generates:
278278
- `TransitionResult` enum for transition outcomes
279279
- Boilerplate Actor implementation with validation
280280

281+
### Named Processes
282+
283+
Like Erlang, joerl supports registering actors with names for easy lookup:
284+
285+
```rust
286+
let system = ActorSystem::new();
287+
let worker = system.spawn(Worker);
288+
289+
// Register with a name
290+
system.register("my_worker", worker.pid()).unwrap();
291+
292+
// Look up by name
293+
if let Some(pid) = system.whereis("my_worker") {
294+
system.send(pid, Box::new("Hello")).await?;
295+
}
296+
297+
// List all registered names
298+
let names = system.registered();
299+
300+
// Unregister
301+
system.unregister("my_worker").unwrap();
302+
```
303+
304+
Names are automatically cleaned up when actors terminate. Actors can look up names from within their context:
305+
306+
```rust
307+
#[async_trait]
308+
impl Actor for MyActor {
309+
async fn handle_message(&mut self, msg: Message, ctx: &mut ActorContext) {
310+
if let Some(manager_pid) = ctx.whereis("manager") {
311+
ctx.send(manager_pid, Box::new("status")).await.ok();
312+
}
313+
}
314+
}
315+
```
316+
317+
### Scheduled Messaging
318+
319+
joerl supports Erlang-style `send_after` for delayed message delivery:
320+
321+
```rust
322+
use joerl::scheduler::Destination;
323+
use std::time::Duration;
324+
325+
let system = ActorSystem::new();
326+
let actor = system.spawn(Worker);
327+
328+
// Schedule a message to be sent after 5 seconds
329+
let timer_ref = system.send_after(
330+
Destination::Pid(actor.pid()),
331+
Box::new("delayed message"),
332+
Duration::from_secs(5)
333+
).await;
334+
335+
// Cancel the timer if needed
336+
system.cancel_timer(timer_ref)?;
337+
338+
// Schedule to a named process
339+
system.send_after(
340+
Destination::Name("worker".to_string()),
341+
Box::new("task"),
342+
Duration::from_millis(100)
343+
).await;
344+
```
345+
346+
Actors can schedule messages from within their context:
347+
348+
```rust
349+
#[async_trait]
350+
impl Actor for MyActor {
351+
async fn started(&mut self, ctx: &mut ActorContext) {
352+
// Schedule a reminder to ourselves
353+
ctx.send_after(
354+
Destination::Pid(ctx.pid()),
355+
Box::new("timeout"),
356+
Duration::from_secs(10)
357+
);
358+
}
359+
}
360+
```
361+
281362
### Trapping Exits
282363

283364
Actors can trap exit signals to handle failures gracefully:
@@ -301,6 +382,12 @@ impl Actor for MyActor {
301382
|| Erlang | joerl | Description |
302383
|--------|-------|-------------|
303384
| `spawn/1` | `system.spawn(actor)` | Spawn a new actor |
385+
| `register(Name, Pid)` | `system.register(name, pid)` | Register a process with a name |
386+
| `unregister(Name)` | `system.unregister(name)` | Unregister a name |
387+
| `whereis(Name)` | `system.whereis(name)` | Look up a process by name |
388+
| `registered()` | `system.registered()` | List all registered names |
389+
| `erlang:send_after(Time, Dest, Msg)` | `system.send_after(dest, msg, duration)` | Schedule delayed message |
390+
| `erlang:cancel_timer(TRef)` | `system.cancel_timer(tref)` | Cancel a scheduled timer |
304391
| `gen_server:start_link/3` | `gen_server::spawn(&system, server)` | Spawn a gen_server |
305392
| `gen_server:call/2` | `server_ref.call(request)` | Synchronous call |
306393
| `gen_server:cast/2` | `server_ref.cast(message)` | Asynchronous cast |
@@ -325,6 +412,8 @@ See the [`examples/`](examples/) directory for more examples:
325412
- `ping_pong.rs` - Two actors communicating
326413
- `supervision_tree.rs` - Supervision tree example
327414
- `link_monitor.rs` - Links and monitors demonstration
415+
- `named_processes.rs` - Named process registry demonstration
416+
- `scheduled_messaging.rs` - Delayed message delivery with timers
328417
- **`panic_handling.rs` - Comprehensive panic handling demonstration (Erlang/OTP-style)**
329418
- **`telemetry_demo.rs` - Telemetry and metrics with Prometheus export**
330419
- `serialization_example.rs` - Trait-based message serialization

joerl/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ categories = ["asynchronous", "concurrency"]
1313

1414
[dependencies]
1515
tokio = { version = "1.48", features = ["full"] }
16+
tokio-util = "0.7"
1617
thiserror = "2.0"
1718
tracing = "0.1"
1819
async-trait = "0.1"

joerl/examples/msg_to_self.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use async_trait::async_trait;
2+
use joerl::{Actor, ActorContext, ActorSystem, ExitReason, Message};
3+
4+
struct SelfMessenger {
5+
count: u32,
6+
max: u32,
7+
}
8+
9+
#[async_trait]
10+
impl Actor for SelfMessenger {
11+
async fn started(&mut self, ctx: &mut ActorContext) {
12+
println!("[{}] Starting, sending first message to self", ctx.pid());
13+
// Send initial message to self
14+
ctx.send(ctx.pid(), Box::new("tick")).await.ok();
15+
}
16+
17+
async fn handle_message(&mut self, msg: Message, ctx: &mut ActorContext) {
18+
if let Some(cmd) = msg.downcast_ref::<&str>()
19+
&& *cmd == "tick"
20+
{
21+
self.count += 1;
22+
println!("[{}] Tick #{}", ctx.pid(), self.count);
23+
24+
if self.count < self.max {
25+
// Send next message to self
26+
ctx.send(ctx.pid(), Box::new("tick")).await.ok();
27+
} else {
28+
println!("[{}] Max reached, stopping", ctx.pid());
29+
ctx.stop(ExitReason::Normal);
30+
}
31+
}
32+
}
33+
}
34+
35+
#[tokio::main]
36+
async fn main() {
37+
let system = ActorSystem::new();
38+
let actor = system.spawn(SelfMessenger { count: 0, max: 5 });
39+
40+
// Wait for the actor to finish
41+
let exit_reason = actor.join().await;
42+
println!("Done! Actor exited with: {}", exit_reason);
43+
}

0 commit comments

Comments
 (0)