Skip to content

Commit 8d83891

Browse files
committed
[actor] Introduce Actor SDK
1 parent 785d338 commit 8d83891

9 files changed

Lines changed: 3341 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 15 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[workspace]
22
members = [
3+
"actor",
34
"broadcast",
45
"codec",
56
"coding",
@@ -85,6 +86,7 @@ chrono = "0.4.39"
8586
clap = "4.5.18"
8687
colored = "3.0.0"
8788
commonware-broadcast = { version = "2026.2.0", path = "broadcast" }
89+
commonware-actor = { version = "2026.2.0", path = "actor" }
8890
commonware-codec = { version = "2026.2.0", path = "codec", default-features = false }
8991
commonware-coding = { version = "2026.2.0", path = "coding" }
9092
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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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-macros = { workspace = true, features = ["std"] }
18+
commonware-runtime.workspace = true
19+
commonware-utils = { workspace = true, features = ["std"] }
20+
futures.workspace = true
21+
governor.workspace = true
22+
rand.workspace = true
23+
thiserror.workspace = true
24+
tracing.workspace = true
25+
26+
[dev-dependencies]
27+
tokio = { workspace = true, features = ["rt", "sync"] }
28+
29+
[lib]
30+
bench = false
31+
crate-type = ["rlib", "cdylib"]

actor/README.md

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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 protocol-driven ingress and lane-aware control loops.
7+
8+
`commonware-actor` is a small, static actor SDK for Commonware primitives.
9+
It emphasizes explicit protocols, 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 protocol types
16+
17+
This keeps protocols uniform without forcing one internal loop shape everywhere.
18+
19+
## What This Crate Provides
20+
21+
- Protocol declarations with generated ingress wrappers ([`protocol!`])
22+
- Explicit dispatch helper ([`dispatch!`]) preserving [`ControlFlow`] semantics
23+
- Bounded and unbounded mailbox APIs ([`ingress::Mailbox`], [`ingress::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+
## Quickstart (Single Lane)
35+
36+
```rust,no_run
37+
use commonware_actor::{dispatch, protocol, service::ServiceBuilder, Actor};
38+
use commonware_runtime::{deterministic, ContextCell, Metrics, Runner};
39+
use std::ops::ControlFlow;
40+
41+
protocol! {
42+
pub enum CounterIngress {
43+
tell Increment { amount: u64 };
44+
request Get -> u64;
45+
tell Stop;
46+
}
47+
}
48+
49+
#[derive(Default)]
50+
struct Counter {
51+
total: u64,
52+
}
53+
54+
impl Actor<ContextCell<deterministic::Context>> for Counter {
55+
type Ingress = CounterIngress;
56+
57+
async fn on_ingress(
58+
&mut self,
59+
_context: &ContextCell<deterministic::Context>,
60+
message: Self::Ingress,
61+
) -> ControlFlow<()> {
62+
dispatch!(message, {
63+
CounterIngress::Increment { amount } => {
64+
self.total += amount;
65+
},
66+
CounterIngress::Get { response } => {
67+
let _ = response.send(self.total);
68+
},
69+
CounterIngress::Stop => {
70+
ControlFlow::Break(())
71+
},
72+
})
73+
}
74+
}
75+
76+
let runner = deterministic::Runner::default();
77+
runner.start(|context| async move {
78+
let actor = Counter::default();
79+
let (mut mailbox, control) = ServiceBuilder::new(actor)
80+
.build(context.with_label("counter"))
81+
.unwrap();
82+
let handle = control.start();
83+
84+
mailbox.tell(Increment { amount: 5 }).await.unwrap();
85+
assert_eq!(mailbox.ask(Get).await.unwrap(), 5);
86+
87+
mailbox.tell(Stop).await.unwrap();
88+
let _ = handle.await;
89+
});
90+
```
91+
92+
## Priority Lanes
93+
94+
Use [`service::ServiceBuilder`] when you need multiple ingress lanes. Lane polling is
95+
declaration-order biased by `with_lane(...)`.
96+
97+
For simple one-lane actors, use `build(...)` or `build_with_capacity(...)`.
98+
Use `with_unbounded_lane(...)` for lanes that should never block on enqueue.
99+
100+
```rust,compile_fail
101+
let (lanes, control) = ServiceBuilder::new(actor)
102+
.with_lane(Lane::Control, 32)
103+
.with_lane(Lane::High, 256)
104+
.with_unbounded_lane(Lane::Low)
105+
.build(context.with_label("peer"));
106+
```
107+
108+
## Sources and Builder Poll Order
109+
110+
The driver polls branches in this order each iteration:
111+
112+
1. shutdown signal
113+
2. configured branches in builder declaration order
114+
115+
So `with_sources(...).with_lane(...)` prioritizes sources over lanes, while
116+
`with_lane(...).with_sources(...)` prioritizes lanes over sources.
117+
118+
```rust,compile_fail
119+
// Source branch before lane branch
120+
ServiceBuilder::new(actor)
121+
.with_sources(source)
122+
.with_lane(0usize, 128)
123+
.build(context);
124+
125+
// Lane branch before source branch
126+
ServiceBuilder::new(actor)
127+
.with_lane(0usize, 128)
128+
.with_sources(source)
129+
.build(context);
130+
```
131+
132+
Within each branch, polling is declaration-order biased:
133+
134+
- lanes: first declared lane first
135+
- sources: first declared source first (`sources!(a, b, c)` polls `a`, then `b`, then `c`)
136+
137+
## Built-in Source Adapters
138+
139+
- `recv(rx, map)`: maps `mpsc::Receiver<T>` messages into ingress
140+
- `deadline(arm, emit)`: dynamic timer source driven by actor state
141+
- `option_future(arm, map)`: polls one optional future in place
142+
- `pool_next(get_pool, map)`: polls next completion from [`commonware_utils::futures::AbortablePool`]
143+
- `handle(get_handle, map)`: polls an optional runtime task `Handle`
144+
- `poll_fn(f)`: custom adapter for unusual cases
145+
146+
## Writing Custom Sources
147+
148+
Most actors should start with built-in adapters. When those are not enough, you have two options:
149+
150+
1. Use [`source::poll_fn`] for local, one-off source behavior.
151+
2. Implement [`source::Source`] for reusable source types.
152+
153+
Custom source contract:
154+
155+
- return `Poll::Ready` with `Some(ingress)` to emit one event
156+
- return `Poll::Pending` when temporarily idle
157+
- return `Poll::Ready` with `None` only when permanently exhausted
158+
159+
Important: once a source returns `None`, the service stops polling that source branch.
160+
161+
```rust,compile_fail
162+
use commonware_actor::{source, sources, service::ServiceBuilder};
163+
use core::task::Poll;
164+
165+
let custom = source::poll_fn(|actor: &mut ActorState, _context: &Context, _cx| {
166+
if actor.ready {
167+
actor.ready = false;
168+
Poll::Ready(Some(Ingress::Tick))
169+
} else {
170+
Poll::Pending
171+
}
172+
});
173+
174+
let (_lanes, _service) = ServiceBuilder::new(actor)
175+
.with_sources(sources!(custom))
176+
.with_lane(0usize, 64)
177+
.build(context);
178+
```
179+
180+
## Manual Mode
181+
182+
Manual loops use the same protocol and mailbox types.
183+
184+
```rust,no_run
185+
use commonware_actor::{ingress::Mailbox, oneshot};
186+
use commonware_runtime::{deterministic, Runner, Spawner};
187+
use commonware_utils::channel::mpsc;
188+
189+
enum Ingress {
190+
TellVariant,
191+
RequestVariant { response: oneshot::Sender<u64> },
192+
Stop,
193+
}
194+
195+
let runner = deterministic::Runner::default();
196+
runner.start(|context| async move {
197+
let (tx, mut rx) = mpsc::channel::<Ingress>(128);
198+
let mailbox = Mailbox::new(tx);
199+
200+
let handle = context.spawn(move |_context| async move {
201+
while let Some(message) = rx.recv().await {
202+
match message {
203+
Ingress::TellVariant => {}
204+
Ingress::RequestVariant { response } => {
205+
let _ = response.send(7);
206+
}
207+
Ingress::Stop => break,
208+
}
209+
}
210+
});
211+
212+
drop(mailbox);
213+
let _ = handle.await;
214+
});
215+
```
216+
217+
## Status
218+
219+
Stability varies by primitive. See [README](https://github.com/commonwarexyz/monorepo#stability) for details.
220+
221+
[`ControlFlow`]: std::ops::ControlFlow

0 commit comments

Comments
 (0)