Skip to content

Commit 9e27a8b

Browse files
committed
[actor] Introduce Actor SDK
1 parent 785d338 commit 9e27a8b

12 files changed

Lines changed: 3714 additions & 0 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
[workspace]
22
members = [
3+
"actor",
4+
"actor/macros",
35
"broadcast",
46
"codec",
57
"coding",
@@ -85,6 +87,8 @@ chrono = "0.4.39"
8587
clap = "4.5.18"
8688
colored = "3.0.0"
8789
commonware-broadcast = { version = "2026.2.0", path = "broadcast" }
90+
commonware-actor = { version = "2026.2.0", path = "actor" }
91+
commonware-actor-macros = { version = "2026.2.0", path = "actor/macros" }
8892
commonware-codec = { version = "2026.2.0", path = "codec", default-features = false }
8993
commonware-coding = { version = "2026.2.0", path = "coding" }
9094
commonware-collector = { version = "2026.2.0", path = "collector" }

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
_Primitives are designed for deployment in adversarial environments. If you find an exploit, please refer to our [security policy](./SECURITY.md) before disclosing it publicly (an exploit may equip a malicious party to attack users of a primitive)._
1414

15+
* [actor](./actor/README.md): Coordinate actors with protocol-driven ingress and lane-aware control loops.
1516
* [broadcast](./broadcast/README.md): Disseminate data over a wide-area network.
1617
* [codec](./codec/README.md): Serialize structured data.
1718
* [coding](./coding/README.md): Encode data to enable recovery from a subset of fragments.

actor/Cargo.toml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[package]
2+
name = "commonware-actor"
3+
edition.workspace = true
4+
publish = true
5+
version.workspace = true
6+
license.workspace = true
7+
description = "Coordinate actors with protocol-driven ingress and lane-aware control loops."
8+
readme = "README.md"
9+
homepage.workspace = true
10+
repository = "https://github.com/commonwarexyz/monorepo/tree/main/actor"
11+
documentation = "https://docs.rs/commonware-actor"
12+
13+
[lints]
14+
workspace = true
15+
16+
[dependencies]
17+
commonware-actor-macros.workspace = true
18+
commonware-macros = { workspace = true, features = ["std"] }
19+
commonware-runtime.workspace = true
20+
commonware-utils = { workspace = true, features = ["std"] }
21+
futures.workspace = true
22+
governor.workspace = true
23+
rand.workspace = true
24+
thiserror.workspace = true
25+
tracing.workspace = true
26+
27+
[dev-dependencies]
28+
tokio = { workspace = true, features = ["rt", "sync"] }
29+
30+
[lib]
31+
bench = false
32+
crate-type = ["rlib", "cdylib"]

actor/README.md

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
# commonware-actor
2+
3+
[![Crates.io](https://img.shields.io/crates/v/commonware-actor.svg)](https://crates.io/crates/commonware-actor)
4+
[![Docs.rs](https://docs.rs/commonware-actor/badge.svg)](https://docs.rs/commonware-actor)
5+
6+
Coordinate actors with explicit ingress types and lane-aware control loops.
7+
8+
`commonware-actor` is a small, static actor SDK for Commonware primitives.
9+
It emphasizes explicit ingress APIs, deterministic control loops, and no per-event
10+
dynamic dispatch on hot paths.
11+
12+
It is designed for two execution styles:
13+
14+
- driver mode with [`service::ServiceBuilder`] + [`service::ActorService`]
15+
- manual mode with the same [`Request`] / [`Tell`] / mailbox ingress types
16+
17+
This keeps ingress types uniform without forcing one internal loop shape everywhere.
18+
19+
## What This Crate Provides
20+
21+
- Ingress declarations with generated wrappers ([`ingress!`])
22+
- Explicit dispatch helper ([`dispatch!`]) preserving [`ControlFlow`] semantics
23+
- Bounded and unbounded mailbox APIs ([`mailbox::Mailbox`], [`mailbox::UnboundedMailbox`])
24+
- Single-lane and multi-lane drivers ([`service::ServiceBuilder`], [`service::ActorService`])
25+
- Static source registration ([`sources!`], tuple-based [`source::SourceSet`] impls)
26+
- Built-in source adapters: [`source::recv`], [`source::deadline`], [`source::option_future`], [`source::pool_next`], [`source::handle`], [`source::poll_fn`]
27+
28+
## What This Crate Does Not Provide
29+
30+
- Supervision trees, registries, or framework-managed actor discovery
31+
- Runtime-specific scheduling APIs outside the Commonware runtime traits
32+
- Dynamic source registries or per-event trait-object dispatch
33+
34+
## `ingress!` Macro
35+
36+
`ingress!` declares ingress, typed wrappers, and a typed mailbox wrapper.
37+
38+
- optional first token can be `MailboxName,` (or `MailboxName<...>,`) to set mailbox type name.
39+
- `tell` and `ask` define ingress items.
40+
- `pub tell` / `pub ask` expose generated convenience methods on the mailbox
41+
(`UpperCamelCase` variant names become `lower_snake_case` methods).
42+
`pub tell` items also generate `*_lossy` variants that return `bool`.
43+
44+
```rust
45+
commonware_actor::ingress! {
46+
CounterMailbox,
47+
48+
// Internal wrappers (no generated mailbox methods)
49+
tell LocalTick;
50+
51+
// Public API on CounterMailbox
52+
pub tell Increment { amount: u64 };
53+
pub ask Get -> u64;
54+
}
55+
56+
// Generated mailbox API includes:
57+
// - CounterMailbox::increment(amount)
58+
// - CounterMailbox::get()
59+
```
60+
61+
## Quickstart (Single Lane)
62+
63+
```rust,no_run
64+
use commonware_actor::{dispatch, service::ServiceBuilder, Actor};
65+
use commonware_runtime::{deterministic, ContextCell, Metrics, Runner};
66+
use std::ops::ControlFlow;
67+
68+
commonware_actor::ingress! {
69+
CounterMailbox,
70+
71+
pub tell Increment { amount: u64 };
72+
pub ask Get -> u64;
73+
pub tell Stop;
74+
}
75+
76+
#[derive(Default)]
77+
struct Counter {
78+
total: u64,
79+
}
80+
81+
impl Actor<ContextCell<deterministic::Context>> for Counter {
82+
type Ingress = CounterMailboxMessage;
83+
84+
async fn on_ingress(
85+
&mut self,
86+
_context: &ContextCell<deterministic::Context>,
87+
message: Self::Ingress,
88+
) -> ControlFlow<()> {
89+
dispatch!(message, {
90+
CounterMailboxMessage::Increment { amount } => {
91+
self.total += amount;
92+
},
93+
CounterMailboxMessage::Get { response } => {
94+
let _ = response.send(self.total);
95+
},
96+
CounterMailboxMessage::Stop => {
97+
ControlFlow::Break(())
98+
},
99+
})
100+
}
101+
}
102+
103+
let runner = deterministic::Runner::default();
104+
runner.start(|context| async move {
105+
let actor = Counter::default();
106+
let (mut mailbox, control) = ServiceBuilder::new(actor)
107+
.build(context.with_label("counter"));
108+
let handle = control.start();
109+
110+
mailbox.tell(Increment { amount: 5 }).await.unwrap();
111+
assert_eq!(mailbox.ask(Get).await.unwrap(), 5);
112+
113+
mailbox.tell(Stop).await.unwrap();
114+
let _ = handle.await;
115+
});
116+
```
117+
118+
## Priority Lanes
119+
120+
Use [`service::ServiceBuilder`] when you need multiple ingress lanes. Lane polling is
121+
declaration-order biased by `with_lane(...)`.
122+
123+
For simple one-lane actors, use `build(...)` or `build_with_capacity(...)`.
124+
Use `with_unbounded_lane(...)` for lanes that should never block on enqueue.
125+
126+
```rust,compile_fail
127+
let (lanes, control) = ServiceBuilder::new(actor)
128+
.with_lane(Lane::Control, 32)
129+
.with_lane(Lane::High, 256)
130+
.with_unbounded_lane(Lane::Low)
131+
.build(context.with_label("peer"));
132+
```
133+
134+
## Sources and Builder Poll Order
135+
136+
The driver polls branches in this order each iteration:
137+
138+
1. shutdown signal
139+
2. configured branches in builder declaration order
140+
141+
So `with_sources(...).with_lane(...)` prioritizes sources over lanes, while
142+
`with_lane(...).with_sources(...)` prioritizes lanes over sources.
143+
144+
```rust,compile_fail
145+
// Source branch before lane branch
146+
ServiceBuilder::new(actor)
147+
.with_sources(source)
148+
.with_lane(0usize, 128)
149+
.build(context);
150+
151+
// Lane branch before source branch
152+
ServiceBuilder::new(actor)
153+
.with_lane(0usize, 128)
154+
.with_sources(source)
155+
.build(context);
156+
```
157+
158+
Within each branch, polling is declaration-order biased:
159+
160+
- lanes: first declared lane first
161+
- sources: first declared source first (`sources!(a, b, c)` polls `a`, then `b`, then `c`)
162+
163+
## Built-in Source Adapters
164+
165+
- `recv(rx, map)`: maps `mpsc::Receiver<T>` messages into ingress
166+
- `deadline(arm, emit)`: dynamic timer source driven by actor state
167+
- `option_future(arm, map)`: polls one optional future in place
168+
- `pool_next(get_pool, map)`: polls next completion from [`commonware_utils::futures::AbortablePool`]
169+
- `handle(get_handle, map)`: polls an optional runtime task `Handle`
170+
- `poll_fn(f)`: custom adapter for unusual cases
171+
172+
## Writing Custom Sources
173+
174+
Most actors should start with built-in adapters. When those are not enough, you have two options:
175+
176+
1. Use [`source::poll_fn`] for local, one-off source behavior.
177+
2. Implement [`source::Source`] for reusable source types.
178+
179+
Custom source contract:
180+
181+
- return `Poll::Ready` with `Some(ingress)` to emit one event
182+
- return `Poll::Pending` when temporarily idle
183+
- return `Poll::Ready` with `None` only when permanently exhausted
184+
185+
Important: once a source returns `None`, the service stops polling that source branch.
186+
187+
```rust,compile_fail
188+
use commonware_actor::{source, sources, service::ServiceBuilder};
189+
use core::task::Poll;
190+
191+
let custom = source::poll_fn(|actor: &mut ActorState, _context: &Context, _cx| {
192+
if actor.ready {
193+
actor.ready = false;
194+
Poll::Ready(Some(Ingress::Tick))
195+
} else {
196+
Poll::Pending
197+
}
198+
});
199+
200+
let (_lanes, _service) = ServiceBuilder::new(actor)
201+
.with_sources(sources!(custom))
202+
.with_lane(0usize, 64)
203+
.build(context);
204+
```
205+
206+
## Manual Mode
207+
208+
Manual loops use the same ingress and mailbox types.
209+
210+
```rust,no_run
211+
use commonware_actor::{mailbox::Mailbox, oneshot};
212+
use commonware_runtime::{deterministic, Runner, Spawner};
213+
use commonware_utils::channel::mpsc;
214+
215+
enum Ingress {
216+
TellVariant,
217+
RequestVariant { response: oneshot::Sender<u64> },
218+
Stop,
219+
}
220+
221+
let runner = deterministic::Runner::default();
222+
runner.start(|context| async move {
223+
let (tx, mut rx) = mpsc::channel::<Ingress>(128);
224+
let mailbox = Mailbox::new(tx);
225+
226+
let handle = context.spawn(move |_context| async move {
227+
while let Some(message) = rx.recv().await {
228+
match message {
229+
Ingress::TellVariant => {}
230+
Ingress::RequestVariant { response } => {
231+
let _ = response.send(7);
232+
}
233+
Ingress::Stop => break,
234+
}
235+
}
236+
});
237+
238+
drop(mailbox);
239+
let _ = handle.await;
240+
});
241+
```
242+
243+
## Status
244+
245+
Stability varies by primitive. See [README](https://github.com/commonwarexyz/monorepo#stability) for details.
246+
247+
[`ControlFlow`]: std::ops::ControlFlow

actor/macros/Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "commonware-actor-macros"
3+
edition.workspace = true
4+
publish = true
5+
version.workspace = true
6+
license.workspace = true
7+
description = "Procedural macros for commonware-actor."
8+
readme = "README.md"
9+
homepage.workspace = true
10+
repository = "https://github.com/commonwarexyz/monorepo/tree/main/actor/macros"
11+
documentation = "https://docs.rs/commonware-actor-macros"
12+
13+
[lib]
14+
proc-macro = true
15+
16+
[dependencies]
17+
proc-macro-crate.workspace = true
18+
proc-macro2.workspace = true
19+
quote.workspace = true
20+
syn = { workspace = true, features = ["full", "parsing"] }
21+
22+
[lints]
23+
workspace = true

actor/macros/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# commonware-actor-macros
2+
3+
Procedural macros for `commonware-actor`.
4+
5+
This crate currently provides the `ingress!` macro used by
6+
`commonware-actor` to generate ingress enums, wrappers, and typed mailboxes.

0 commit comments

Comments
 (0)