1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
//! `zestors` is an actor framework with Erlang/OTP-style supervision.
//!
//! This crate is a facade: it re-exports the workspace crates as modules and
//! collects the commonly used items in [`prelude`].
//!
//! # Reading Order
//!
//! | Module | What it provides |
//! |---|---|
//! | [`interface`] | The vocabulary: derive `Message` for each message type and `Interface` for the set of messages an actor accepts. |
//! | [`runtime`] | The delivery machinery: actors receive through `Inbox`, everyone else sends through `Address`/`StrongAddress`, and actors are looked up by `Pid` in `Registry`. |
//! | [`actor`] | The actors: implement `Handler` (per-message handlers) or `Actor` (the full event loop), then package either in an `Blueprint`. |
//! | [`supervision`] | The supervisor's vocabulary: `ChildSpec` pairs a blueprint with its `ChildConfig`, and `RestartIntensity` bounds how often a child may restart. |
//! | [`supervisor`] | The supervision actors: `Supervisor` starts, watches, and restarts children per a `SupervisionStrategy`; `Node` runs a root supervisor as a whole program. |
//! | [`api_server`] | HTTP introspection: `ApiServer` exposes `/processes`, `/snapshots`, and `/health` for a running tree. |
//! | `zestors_inspector` | The `zestors-inspector` GUI — a separate workspace crate, not re-exported here — visualizes the same tree data that [`api_server`] exposes. |
//!
//! # Guide: writing your first actor
//!
//! This walks through building a small actor step by step, then covers the
//! lifecycle behavior that governs it, a declarative alternative for
//! defining the actor body, and the supervision layer. Some of the
//! lifecycle behavior is easy to get wrong without knowing about it in
//! advance, so it's called out explicitly rather than left to be discovered
//! from a bug report.
//!
//! ## 1. Define your messages
//!
//! A message is a plain struct deriving [`Message`](interface::Message).
//! Messages come in two flavors: fire-and-forget, and request/reply.
//!
//! ```
//! use zestors::interface::Message;
//!
//! // Fire-and-forget: nothing is returned to the sender.
//! #[derive(Message, Debug)]
//! struct Increment;
//!
//! // Request/reply: `reply = u32` means calling this message returns a `u32`.
//! #[derive(Message, Debug)]
//! #[msg(reply = u32)]
//! struct GetCount;
//! ```
//!
//! ## 2. Group them into an `Interface`
//!
//! An actor doesn't accept a single message type - it accepts a set of
//! them, described by an [`Interface`](interface::Interface). Each variant
//! wraps one message type in an [`Envelope`](interface::Envelope):
//!
//! ```
//! use zestors::interface::{Envelope, Interface, Message};
//! #
//! # #[derive(Message, Debug)]
//! # struct Increment;
//! #
//! # #[derive(Message, Debug)]
//! # #[msg(reply = u32)]
//! # struct GetCount;
//!
//! #[derive(Interface, Debug)]
//! enum CounterInterface {
//! Increment(Envelope<Increment>),
//! GetCount(Envelope<GetCount>),
//! }
//! ```
//!
//! Only tuple variants with exactly one `Envelope<T>` field are allowed -
//! the derive macro needs that shape to generate the conversions that let a
//! channel accept and dispatch each message type.
//!
//! ## 3. Write the actor, spawn it, and send it messages
//!
//! The actor body is an `async` function (or closure) that owns an
//! [`Inbox`](runtime::Inbox) and loops over incoming messages until it
//! decides to stop. [`spawn_rand`](runtime::spawn_rand) starts it on a
//! fresh, randomly-generated [`Pid`](runtime::Pid), returning a
//! [`Child`](runtime::Child) that's both a handle for sending messages and
//! a future that resolves to the actor's own return value once it exits.
//!
//! ```
//! use zestors::interface::{Envelope, Interface, Message};
//! use zestors::runtime::prelude::*;
//! use zestors::runtime::spawn_rand;
//!
//! #[derive(Message, Debug)]
//! struct Increment;
//!
//! #[derive(Message, Debug)]
//! #[msg(reply = u32)]
//! struct GetCount;
//!
//! #[derive(Interface, Debug)]
//! enum CounterInterface {
//! Increment(Envelope<Increment>),
//! GetCount(Envelope<GetCount>),
//! }
//!
//! # #[tokio::main]
//! # async fn main() {
//! let child = spawn_rand(|mut inbox: Inbox<CounterInterface>| async move {
//! let mut count = 0;
//! while let Some(msg) = inbox.recv().await {
//! match msg {
//! CounterInterface::Increment(_) => count += 1,
//! CounterInterface::GetCount(envelope) => {
//! let _ = envelope.reply(count);
//! }
//! }
//! }
//! Ok(())
//! });
//!
//! // `cast` sends a message without waiting for it to be handled.
//! for _ in 0..5 {
//! child.cast(Increment).await.unwrap();
//! }
//!
//! // `call` sends a message and waits for its reply. Every message to one
//! // actor goes through the same queue, in order, so this only resolves
//! // once the 5 `Increment`s above have already been processed.
//! assert_eq!(child.call(GetCount).await.unwrap(), 5);
//!
//! child.signal_shutdown();
//! # }
//! ```
//!
//! `envelope.reply(value)` answers a request/reply message; a
//! fire-and-forget message like `Increment` has nothing to reply to, so its
//! envelope is just dropped once handled.
//!
//! Dropping a `Child` aborts the actor it belongs to, unless `.detach()` has
//! been called on it first (or it's been consumed via `.into_handle()`).
//! This applies even if the `Child` is bound to `_` or simply goes out of
//! scope.
//!
//! ## 4. The actor lifecycle
//!
//! Every actor moves through a small set of states, tracked as
//! [`ActorStatus`](runtime::ActorStatus): `Initializing`, then `Running`,
//! optionally toggling between `Running` and `Suspended`, then `Exiting`,
//! and finally `Exited` (with a reason: normal exit, panic, abort, or an
//! error returned from the actor's own code).
//!
//! An actor reaches `Initializing` the moment it's spawned, before its task
//! has run even once, and only becomes `Running` once it calls a receiving
//! method for the first time. Code that runs immediately after spawning
//! should not assume the actor is already `Running`; either wait for it
//! with `child.watch_init().await`, or accept `Initializing` as well.
//!
//! Two behaviors around signals (shutdown, suspend, resume) depart from
//! what the method names alone would suggest:
//!
//! - **Sending a signal does not take effect immediately.** Calling
//! `child.signal_shutdown()` adds "shutdown" to the actor's queue and
//! returns without waiting for it to be processed. Checking
//! `child.status()` on the next line can still show the previous status.
//! To observe the effect, await something that actually waits for it,
//! such as `child.watch_exit().await`.
//! - **Signals take priority over messages, except when the queue is
//! empty.** Signals are checked before regular messages, so a shutdown
//! request does not wait behind a backlog of messages. But once an
//! actor's message queue is empty, processing a shutdown signal makes it
//! exit immediately, without checking whether another signal is queued
//! behind it. This matters only in specific timing situations (for
//! example, pinging an actor in the same breath as shutting it down). An
//! actor that needs to keep responding to signals after `Shutdown` should
//! loop with `recv_event_always` and decide for itself when to stop,
//! rather than relying on `recv`/`recv_event` to do it.
//!
//! ## 5. Declarative actors with `Handler`
//!
//! Writing the event loop by hand, as in step 3, gives full control over
//! how messages, signals, and other futures are read and interleaved.
//! [`Handler`](actor::Handler) is a higher-level alternative for actors that
//! don't need that control: it provides the event loop, and asks only for
//! lifecycle hooks and one [`Handle<M>`](actor::Handle) implementation per
//! message type.
//!
//! ```
//! use zestors::interface::{Envelope, Interface, Message, Request};
//! use zestors::prelude::*;
//!
//! #[derive(Message, Debug)]
//! struct Increment;
//!
//! #[derive(Message, Debug)]
//! #[msg(reply = u32)]
//! struct GetCount;
//!
//! #[derive(Interface, HandlerInterface, Debug)]
//! enum CounterInterface {
//! Increment(Envelope<Increment>),
//! GetCount(Envelope<GetCount>),
//! }
//!
//! #[derive(Debug, Clone)]
//! struct Counter {
//! count: u32,
//! }
//!
//! impl Handler for Counter {
//! type Interface = CounterInterface;
//! }
//!
//! impl Handle<Increment> for Counter {
//! async fn handle(
//! &mut self,
//! _ctx: HandlerContext<'_, Self>,
//! _msg: Increment,
//! _req: (),
//! ) -> Result<(), rootcause::Report> {
//! self.count += 1;
//! Ok(())
//! }
//! }
//!
//! impl Handle<GetCount> for Counter {
//! async fn handle(
//! &mut self,
//! _ctx: HandlerContext<'_, Self>,
//! _msg: GetCount,
//! req: Request<u32>,
//! ) -> Result<(), rootcause::Report> {
//! let _ = req.reply(self.count);
//! Ok(())
//! }
//! }
//!
//! # #[tokio::main]
//! # async fn main() {
//! // `ActorExt::spawn_rand` (from the `Actor` implementation `Handler`
//! // provides automatically) plays the same role as `spawn_rand` did in
//! // step 3.
//! let child = Counter { count: 0 }.spawn_rand();
//!
//! for _ in 0..5 {
//! child.cast(Increment).await.unwrap();
//! }
//! assert_eq!(child.call(GetCount).await.unwrap(), 5);
//!
//! child.signal_shutdown();
//! # }
//! ```
//!
//! `#[derive(Interface, HandlerInterface)]` is what connects each message
//! variant to its `Handle<M>` implementation; both derives read the same
//! enum. A type implementing `Handler` implements [`Actor`](actor::Actor)
//! automatically, so it's spawned, sent messages, and awaited the same way
//! as any other actor.
//!
//! ## 6. Supervision
//!
//! Everything above is sufficient for actors that talk to each other
//! directly, without any restart policy. [`supervision`]/[`supervisor`] add
//! an optional layer for Erlang/OTP-style restart trees:
//! [`ChildSpec`](supervision::ChildSpec) pairs a blueprint with the
//! configuration a supervisor applies to it (restart mode, timeouts), and a
//! [`Supervisor`](supervisor::Supervisor) actor starts, watches, and
//! restarts a set of them.
//!
//! Any actor that is `Clone + Debug` - a [`Handler`](actor::Handler)
//! usually is - implements [`Blueprint`](actor::Blueprint)
//! automatically, so it can go directly into a `ChildSpec`, and several of
//! them into a [`SupervisorBlueprint`](supervisor::SupervisorBlueprint):
//!
//! [`Node`](supervisor::Node) covers the common case of running one root
//! supervisor as an entire program: starting it, restarting it on failure up
//! to a configured budget, and shutting it down gracefully on
//! Ctrl+C/SIGTERM.
//!
//! ```
//! use zestors::interface::{Envelope, Interface, Message};
//! use zestors::prelude::*;
//! use zestors::supervision::messages::GetChildren;
//! use zestors::supervisor::{Node, Supervisor};
//!
//! #[derive(Message, Debug)]
//! struct Ping;
//!
//! #[derive(Interface, HandlerInterface, Debug)]
//! enum WorkerInterface {
//! Ping(Envelope<Ping>),
//! }
//!
//! #[derive(Debug, Clone)]
//! struct Worker;
//!
//! impl Handler for Worker {
//! type Interface = WorkerInterface;
//! }
//!
//! impl Handle<Ping> for Worker {
//! async fn handle(
//! &mut self,
//! _ctx: HandlerContext<'_, Self>,
//! _msg: Ping,
//! _req: (),
//! ) -> Result<(), rootcause::Report> {
//! Ok(())
//! }
//! }
//!
//! # #[tokio::main]
//! # async fn main() {
//! let node = Node::new(
//! Supervisor::blueprint()
//! .child(Worker.pid("worker").unwrap())
//! .rand_pid(),
//! );
//!
//! // In a real program, `node.run()` is usually just awaited directly from
//! // `main`. It's spawned onto its own task here only so the rest of this
//! // example can also demonstrate signaling and observing its shutdown.
//! let root = node.root_supervisor().address().clone();
//! let node_task = tokio::spawn(node.run());
//!
//! root.watch_running().await;
//!
//! let children = root.call(GetChildren).await.unwrap();
//! assert_eq!(children.len(), 1);
//!
//! // The root supervisor exiting on its own is a normal, successful stop
//! // for the whole node - triggered here with an ordinary shutdown signal,
//! // in place of the Ctrl+C/SIGTERM a real deployment would send.
//! root.signal_shutdown();
//! assert!(node_task.await.unwrap().is_ok());
//! # }
//! ```
//!
//! ## Where to go next
//!
//! The [`runtime`], [`interface`], [`actor`], [`supervision`], and
//! [`supervisor`] crate docs each have further worked examples - a bare
//! `Inbox<()>` actor, looking an actor up by `Pid` from elsewhere in the
//! process, the lower-level `Envelope`/`Interface` machinery this guide
//! builds on, and more. Every example in this guide and in those crate docs
//! compiles and runs as part of this workspace's test suite, so they stay
//! in sync with the framework's actual behavior rather than drifting from
//! it over time.
/// The items you usually need, re-exported from each sub-crate plus the
/// codegen macros.
pub use zestors_api_server as api_server;
pub use zestors_actor as actor;
pub use zestors_interface as interface;
pub use zestors_runtime as runtime;
pub use zestors_supervision as supervision;
pub use zestors_supervisor as supervisor;