feat: transport protocol (#27)

Address actors via ID, send messages over transport (TCP, QUIC, etc)
This commit is contained in:
zacheryasc 2026-02-09 19:05:37 +00:00
parent bc70d5fd89
commit ef08d3e7a5
33 changed files with 3789 additions and 2690 deletions

2
.gitignore vendored
View file

@ -9,3 +9,5 @@ corpus
# Analysis artifacts (depgraph + spectral)
**/deps.dot
**/deps.html
docs/architecture.dot
docs/architecture.html

View file

@ -21,6 +21,7 @@ getrandom = ["dep:getrandom"]
serde = ["dep:serde"]
tracing = ["dep:tracing"]
no_random = [] # compile without access to a source of randomness
transport = [] # transport-agnostic messaging (no mandatory deps; codec is user-provided)
[dependencies]
getrandom = { version = "0.2", optional = true }

278
README.md
View file

@ -1,9 +1,9 @@
# swactor
Small, WASM-compatible actor runtime for Rust, with Python and WebAssembly
bindings.
Minimal actor runtime for Rust. Single-threaded or multi-threaded, with
Python and WebAssembly bindings.
## Quick Start (Rust)
## Quick Start
```rust
use swactor::actor::{ActorAddress, ActorInterface};
@ -39,221 +39,92 @@ fn main() {
}
```
## Quick Start (Python)
## Features
### Actor Model
Actors implement one trait (`ActorInterface`), receive one message type, and
hold mutable state. No lifecycle hooks, no supervision trees, no async.
Every actor gets a 32-byte globally unique `ActorAddress`. The same
`ctx.send(addr, msg)` call works whether the target is on the same worker,
a different worker thread, an external inbox, or a remote process.
Single-threaded mode (`rt.tick()`) gives deterministic frame-level control.
Multi-threaded mode (`rt.run()`) spawns OS threads with adaptive backoff.
See [docs/actor-model.md](docs/actor-model.md) and
[docs/runtime.md](docs/runtime.md) for the full model.
### Transport
Pluggable cross-process messaging. User-provided codecs handle serialization
(gRPC/protobuf, bincode, hand-rolled — no serde bounds imposed) and
user-provided transports handle delivery (TCP, in-memory, gRPC channel).
```bash
uv pip install . # builds the Rust extension automatically
cargo build --features transport
cargo run --example tcp_ping_pong --features transport -- receiver # terminal 1
cargo run --example tcp_ping_pong --features transport -- sender # terminal 2
```
Single-threaded — caller drives each tick:
See [docs/transport.md](docs/transport.md) for the routing chain, codec
registry, and address resolution.
```python
from swactor import Runtime
### Runtime Dashboard
def echo(ctx, msg):
ctx.send(msg["reply_to"], f"hello, {msg['name']}!")
Live web dashboard for monitoring actors, message throughput, and mailbox
depths. Supports trace recording and replay at configurable speed.
rt = Runtime()
addr = rt.spawn(echo)
inbox = rt.inbox()
rt.send(addr, {"name": "world", "reply_to": inbox.addr})
rt.tick()
print(inbox.try_recv()) # "hello, world!"
```
Includes hand-authored SVG diagrams (actor lifecycle, message lifecycle,
tick cycle, transport routing) and generated diagrams from DOT sources
(architecture, dataflow, type erasure).
Multi-threaded — workers run on background threads:
See [crates/runtime-dashboard/](crates/runtime-dashboard/README.md).
```python
import asyncio
from swactor import Runtime, RuntimeConfig
### Language Bindings
async def main():
rt = Runtime(RuntimeConfig(num_threads=2))
def echo(ctx, msg):
ctx.send(msg["reply_to"], f"hello, {msg['name']}!")
addr = rt.spawn(echo)
inbox = rt.inbox()
handle = rt.run() # spawns worker threads, consumes rt
for name in ["alice", "bob", "charlie"]:
handle.send(addr, {"name": name, "reply_to": inbox.addr})
while (reply := inbox.try_recv()) is None:
await asyncio.sleep(0.01)
print(reply)
handle.shutdown()
handle.join()
asyncio.run(main())
```
## Quick Start (WASM)
The `wasm/` crate wraps swactor for use from JavaScript via `wasm-bindgen`.
It runs single-threaded with the caller driving `tick()` — a natural fit
for game loops, simulations, or any frame-based update cycle.
**Python** — PyO3 via Maturin. Spawn actors from Python callables, pass
dicts as messages, single-threaded or multi-threaded.
```bash
cd wasm && wasm-pack build --target nodejs # or --target web
cd crates/swactor-python && maturin develop
```
```javascript
import { SwactorRuntime } from "./wasm/pkg/swactor_wasm.js";
Examples in `examples/python/` (single-thread, async, Jupyter notebook).
const rt = new SwactorRuntime();
// spawn a counter actor — accumulates values sent to it
const counter = rt.spawn_counter();
// spawn a relay that forwards messages to the counter
const relay = rt.spawn_relay(counter);
// send through the relay
rt.send(relay, 5);
rt.send(relay, 7);
rt.tick(); // relay receives and forwards
rt.tick(); // counter receives forwarded messages
// drain results from the inbox
let v;
while ((v = rt.try_recv()) !== undefined) {
console.log(v); // 5, then 12
}
rt.free();
```
The WASM crate uses the `no_random` feature (deterministic address
generation) so there's no dependency on system RNG.
## Running the Examples
**WASM** — wasm-bindgen. Runs single-threaded with deterministic addressing
(`no_random` feature).
```bash
cargo run --example hello # single actor, request/response
cargo run --example ring # 500 actors in a ring topology
cd crates/swactor-wasm && wasm-pack build --target nodejs
```
## Multi-threaded Mode
### Connectome Analysis
Pass `num_threads` in the config. The runtime spawns OS threads and runs
workers autonomously — no `tick()` calls needed.
Structural analysis of the internal dependency graph.
```rust
let mut config = RuntimeConfig::default();
config.num_threads = 4;
let rt = Runtime::new(config);
- **depgraph** (`tools/depgraph/`) — AST-based extraction of module
dependencies, outputs GraphViz DOT
- **spectral** (`tools/spectral/`) — Laplacian eigenvalue analysis,
Connectome Complexity Index (CCI), coupling heatmaps, interactive HTML
dashboard
let addr = rt.spawn(MyActor::default()).unwrap();
let handle = rt.run().unwrap(); // consumes rt, spawns 4 threads
// use handle.runtime to spawn/send while workers run
handle.runtime.send_to(addr, MyMsg).unwrap();
handle.shutdown();
handle.join();
```bash
cargo run --manifest-path tools/depgraph/Cargo.toml -- --src-dir src/ --output deps
python tools/spectral/spectral_analysis.py deps.dot
```
## Architecture
The runtime is layered: **Runtime** → **Workers** → **ActorPool** → **Actors**.
```
┌─ Runtime (Arc, shared) ──────────────────────────────────────┐
│ │
│ AddressMap Placement InboxRegistry is_running │
│ (addr→worker) (round-robin) (external inboxes) (AtomicBool) │
│ │
│ transfer_txs[] spawn_txs[] │
│ (one Sender per worker) (one Sender per worker) │
│ │
└───────┬───────────────┬───────────────┬───────────────────────┘
│ │ │
v v v
┌─ Worker 0 ──┐ ┌─ Worker 1 ──┐ ┌─ Worker 2 ──┐
│ ActorPool │ │ ActorPool │ │ ActorPool │
│ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │
│ │mailbox │ │ │ │mailbox │ │ │ │mailbox │ │
│ │ actor │ │ │ │ actor │ │ │ │ actor │ │
│ └────────┘ │ │ └────────┘ │ │ └────────┘ │
│ ┌────────┐ │ │ ┌────────┐ │ │ │
│ │mailbox │ │ │ │mailbox │ │ └──────────────┘
│ │ actor │ │ │ │ actor │ │
│ └────────┘ │ │ └────────┘ │
└──────────────┘ └──────────────┘
```
Each worker runs a **four-phase tick loop**:
1. **Drain spawn queue** — add newly spawned actors to the pool
2. **Drain transfer queue** — deliver cross-worker messages to mailboxes
3. **Tick all actors** — pop messages, call handlers, buffer outgoing sends
4. **Drain pending local** — deliver same-worker messages for the next tick
Messages are type-erased (`Box<dyn Any + Send>`) in transit and downcast
back to the concrete type at delivery. Mismatched types are silently dropped.
Detailed architecture docs live in `docs/`:
| Document | Covers |
|----------|--------|
| [Worker Thread](docs/worker-thread.md) | Tick phases, backoff, message routing, full system topology |
| [Runtime](docs/runtime.md) | Runtime, Ctx, Inbox, RuntimeHandle, stats |
| [Actor Model](docs/actor-model.md) | Traits, type erasure, addresses |
| [Channels & Shared State](docs/channels.md) | HybridChannel, AddressMap, Placement |
## Source Layout
```
src/
├── lib.rs module root, feature gates, get_random()
├── actor.rs Message, ActorInterface, ActorAddress, type erasure
├── runtime.rs Runtime, Ctx, Inbox, RuntimeHandle, InboxRegistry
├── worker/
│ ├── mod.rs Worker, WorkerContext, ActorPool, tick loop
│ └── tests.rs worker unit tests with step-based DSL
├── channel.rs HybridChannel (ArrayQueue + SegQueue), Sender/Receiver
├── config.rs RuntimeConfig, BackoffPolicy
├── address_map.rs AddressMap (RwLock<HashMap>), Placement (round-robin)
├── error.rs Error type
└── python.rs PyO3 bindings (feature = "python")
wasm/
├── Cargo.toml separate crate, depends on swactor with no_random
├── src/lib.rs wasm-bindgen wrapper (SwactorRuntime)
└── test.mjs Node.js test suite
examples/
├── hello.rs echo actor
├── ring.rs ring topology
└── python/
├── hello_single_thread.py minimal Python example
├── hello_async.py multi-threaded + asyncio
└── getting_started.ipynb Jupyter notebook
tests/
├── runtime_api.rs single + multi-thread integration tests
├── stats_demo.rs stats snapshot tests
└── test_python.py Python binding tests
```
See [docs/connectome.md](docs/connectome.md) for metric interpretation.
## Building & Testing
```bash
# Rust
cargo test # run all tests
cargo run --example hello # run an example
cargo test # all tests
cargo test --features transport # include transport tests
cargo run --example hello # single actor example
cargo run --example ring # 500-actor ring topology
cargo bench # benchmarks (criterion)
# Python bindings (requires Rust toolchain on PATH)
uv pip install . # build + install
uv run python3 tests/test_python.py # run Python tests
# WASM bindings
cd wasm && wasm-pack build --target nodejs
node test.mjs # run WASM tests
```
## Feature Flags
@ -261,19 +132,20 @@ node test.mjs # run WASM tests
| Flag | Default | What it does |
|------|---------|--------------|
| `getrandom` | yes | System RNG for actor addresses |
| `no_random` | no | Deterministic counter (for WASM / reproducible tests) |
| `python` | no | PyO3 bindings, builds cdylib wheel |
## Connectome analysis
| `no_random` | no | Deterministic counter (WASM / reproducible tests) |
| `transport` | no | Pluggable remote messaging (codec + transport) |
| `tracing` | no | `tracing` instrumentation for runtime internals |
| `serde` | no | Serde derives for stats types |
| `python` | no | PyO3 bindings (cdylib wheel) |
Spectral analysis of the internal dependency graph, producing a Connectome Complexity Index (CCI) and visual dashboards.
## Documentation
```sh
# Generate the dependency DAG
cargo run --manifest-path tools/depgraph/Cargo.toml -- --src-dir src/ --output deps
# Run spectral analysis (outputs to docs/connectome/)
source .venv/bin/activate
python tools/spectral/spectral_analysis.py deps.dot
```
This produces a text report, an interactive HTML dashboard, and a static PNG dashboard in `docs/connectome/`. See [docs/connectome.md](docs/connectome.md) for details on the metrics and interpretation.
| Document | Covers |
|----------|--------|
| [Actor Model](docs/actor-model.md) | Traits, type erasure, addresses |
| [Runtime](docs/runtime.md) | Runtime, Ctx, Inbox, RuntimeHandle, stats |
| [Worker Thread](docs/worker-thread.md) | Tick phases, backoff, routing, full system topology |
| [Channels](docs/channels.md) | HybridChannel, AddressMap, Placement |
| [Transport](docs/transport.md) | Codec, Transport, remote messaging, address resolution |
| [Connectome](docs/connectome.md) | CCI metrics, spectral analysis interpretation |
| [Dashboard](crates/runtime-dashboard/README.md) | Live web UI, trace recording, diagram index |

View file

@ -1,36 +1,3 @@
# runtime-dashboard
Visual dashboard and architectural diagrams for the swactor runtime.
## Generating Diagrams
Render all `.dot` sources into SVGs:
```bash
./render_docs.sh
```
**Prerequisites** (one of):
- [Graphviz](https://graphviz.org/) — `apt install graphviz` / `brew install graphviz`
- [Node.js](https://nodejs.org/) — the script auto-installs `@viz-js/viz` into `tools/`
Generated SVGs are written to `docs/generated/` (gitignored).
## Diagram Index
### DOT sources (`docs/*.dot` → `docs/generated/*.svg`)
| Diagram | Description |
|---------|-------------|
| `architecture.dot` | Structural map of all structs/traits, grouped by module, with ownership/Arc/borrow/trait-impl edges |
| `dataflow.dot` | 5 behavioral flows: cross-worker send, same-worker send, actor spawn, external inbox, 6-phase tick cycle |
| `tick_cycle.dot` | Focused view of the `Worker::tick_once` pipeline and its 6 phases |
| `type_erasure.dot` | How generic message/actor types are erased via `Box<dyn Any>` and `Box<dyn AnyActor>` |
### Hand-authored SVGs (`docs/*.svg`, committed)
| Diagram | Description |
|---------|-------------|
| `actor_lifecycle.svg` | Lifecycle states of an actor from spawn to shutdown |
| `message_lifecycle.svg` | Path of a message from send through inbox to handler |
| `runtime_lifecycle.svg` | Runtime startup, worker creation, and shutdown sequence |
Visual dashboard for the swactor runtime.

View file

@ -1,692 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 14.1.1 (20251213.1925)
-->
<!-- Title: architecture Pages: 1 -->
<svg width="2903pt" height="1196pt"
viewBox="0.00 0.00 2903.00 1196.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1191.85)">
<title>architecture</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-1191.85 2899,-1191.85 2899,4 -4,4"/>
<g id="clust1" class="cluster">
<title>cluster_legend</title>
<path fill="none" stroke="#888888" d="M20,-1048.15C20,-1048.15 234,-1048.15 234,-1048.15 240,-1048.15 246,-1054.15 246,-1060.15 246,-1060.15 246,-1165.35 246,-1165.35 246,-1171.35 240,-1177.35 234,-1177.35 234,-1177.35 20,-1177.35 20,-1177.35 14,-1177.35 8,-1171.35 8,-1165.35 8,-1165.35 8,-1060.15 8,-1060.15 8,-1054.15 14,-1048.15 20,-1048.15"/>
<text xml:space="preserve" text-anchor="middle" x="34.35" y="-1163.45" font-family="Helvetica,sans-Serif" font-size="11.00">Legend</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_runtime</title>
<path fill="#d6eaf8" stroke="#1a5276" d="M748,-560.25C748,-560.25 1014,-560.25 1014,-560.25 1020,-560.25 1026,-566.25 1026,-572.25 1026,-572.25 1026,-1156.85 1026,-1156.85 1026,-1162.85 1020,-1168.85 1014,-1168.85 1014,-1168.85 748,-1168.85 748,-1168.85 742,-1168.85 736,-1162.85 736,-1156.85 736,-1156.85 736,-572.25 736,-572.25 736,-566.25 742,-560.25 748,-560.25"/>
<text xml:space="preserve" text-anchor="middle" x="768.45" y="-1154.95" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#1a5276">runtime.rs</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_actor</title>
<path fill="#d5f5e3" stroke="#1e8449" d="M268,-8C268,-8 716,-8 716,-8 722,-8 728,-14 728,-20 728,-20 728,-1149.61 728,-1149.61 728,-1155.61 722,-1161.61 716,-1161.61 716,-1161.61 268,-1161.61 268,-1161.61 262,-1161.61 256,-1155.61 256,-1149.61 256,-1149.61 256,-20 256,-20 256,-14 262,-8 268,-8"/>
<text xml:space="preserve" text-anchor="middle" x="282.34" y="-1147.71" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#1e8449">actor.rs</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_delivery</title>
<path fill="#e8daef" stroke="#6c3483" d="M1046,-357.88C1046,-357.88 1947,-357.88 1947,-357.88 1953,-357.88 1959,-363.88 1959,-369.88 1959,-369.88 1959,-969.05 1959,-969.05 1959,-975.05 1953,-981.05 1947,-981.05 1947,-981.05 1046,-981.05 1046,-981.05 1040,-981.05 1034,-975.05 1034,-969.05 1034,-969.05 1034,-369.88 1034,-369.88 1034,-363.88 1040,-357.88 1046,-357.88"/>
<text xml:space="preserve" text-anchor="middle" x="1067.06" y="-967.15" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#6c3483">delivery.rs</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_worker</title>
<path fill="#fadbd8" stroke="#922b21" d="M1983,-184.85C1983,-184.85 2223,-184.85 2223,-184.85 2229,-184.85 2235,-190.85 2235,-196.85 2235,-196.85 2235,-1167.85 2235,-1167.85 2235,-1173.85 2229,-1179.85 2223,-1179.85 2223,-1179.85 1983,-1179.85 1983,-1179.85 1977,-1179.85 1971,-1173.85 1971,-1167.85 1971,-1167.85 1971,-196.85 1971,-196.85 1971,-190.85 1977,-184.85 1983,-184.85"/>
<text xml:space="preserve" text-anchor="middle" x="2001.61" y="-1165.95" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#922b21">worker.rs</text>
</g>
<g id="clust6" class="cluster">
<title>cluster_channel</title>
<path fill="#d1f2eb" stroke="#117864" d="M2255,-184.85C2255,-184.85 2489,-184.85 2489,-184.85 2495,-184.85 2501,-190.85 2501,-196.85 2501,-196.85 2501,-649.45 2501,-649.45 2501,-655.45 2495,-661.45 2489,-661.45 2489,-661.45 2255,-661.45 2255,-661.45 2249,-661.45 2243,-655.45 2243,-649.45 2243,-649.45 2243,-196.85 2243,-196.85 2243,-190.85 2249,-184.85 2255,-184.85"/>
<text xml:space="preserve" text-anchor="middle" x="2276.38" y="-647.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#117864">channel.rs</text>
</g>
<g id="clust7" class="cluster">
<title>cluster_stats</title>
<path fill="#d1c4e9" stroke="#283593" d="M2699,-339.65C2699,-339.65 2875,-339.65 2875,-339.65 2881,-339.65 2887,-345.65 2887,-351.65 2887,-351.65 2887,-1167.85 2887,-1167.85 2887,-1173.85 2881,-1179.85 2875,-1179.85 2875,-1179.85 2699,-1179.85 2699,-1179.85 2693,-1179.85 2687,-1173.85 2687,-1167.85 2687,-1167.85 2687,-351.65 2687,-351.65 2687,-345.65 2693,-339.65 2699,-339.65"/>
<text xml:space="preserve" text-anchor="middle" x="2712.72" y="-1165.95" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#283593">stats.rs</text>
</g>
<g id="clust8" class="cluster">
<title>cluster_config</title>
<path fill="#fef9e7" stroke="#b7950b" d="M2521,-328.65C2521,-328.65 2667,-328.65 2667,-328.65 2673,-328.65 2679,-334.65 2679,-340.65 2679,-340.65 2679,-682.45 2679,-682.45 2679,-688.45 2673,-694.45 2667,-694.45 2667,-694.45 2521,-694.45 2521,-694.45 2515,-694.45 2509,-688.45 2509,-682.45 2509,-682.45 2509,-340.65 2509,-340.65 2509,-334.65 2515,-328.65 2521,-328.65"/>
<text xml:space="preserve" text-anchor="middle" x="2537.79" y="-680.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#b7950b">config.rs</text>
</g>
<!-- legend -->
<g id="node1" class="node">
<title>legend</title>
<polygon fill="#f8f8f8" stroke="none" points="237.66,-1148.15 16.34,-1148.15 16.34,-1056.15 237.66,-1056.15 237.66,-1148.15"/>
<text xml:space="preserve" text-anchor="start" x="30.34" y="-1129.15" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#333333">───────</text>
<text xml:space="preserve" text-anchor="start" x="96.69" y="-1129.15" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#333333">Ownership (solid)</text>
<text xml:space="preserve" text-anchor="start" x="30.34" y="-1110.15" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="#e67300">━━━━━━━</text>
<text xml:space="preserve" text-anchor="start" x="96.69" y="-1109.15" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#e67300">Arc&#45;shared (bold orange)</text>
<text xml:space="preserve" text-anchor="start" x="30.34" y="-1089.15" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#7b2d8b">&#45; &#45; &#45; &#45; &#45; &#45;</text>
<text xml:space="preserve" text-anchor="start" x="96.69" y="-1089.15" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#7b2d8b">Borrow / &amp;&#39;a (dashed purple)</text>
<text xml:space="preserve" text-anchor="start" x="30.34" y="-1069.15" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#2e8b57">· · · · · ·▷</text>
<text xml:space="preserve" text-anchor="start" x="96.69" y="-1069.15" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#2e8b57">Trait impl (dotted green)</text>
</g>
<!-- Runtime -->
<g id="node2" class="node">
<title>Runtime</title>
<polygon fill="white" stroke="black" points="744.32,-756.35 744.32,-984.35 1017.68,-984.35 1017.68,-756.35 744.32,-756.35"/>
<polygon fill="#2980b9" stroke="none" points="752.32,-958.35 752.32,-980.35 1009.68,-980.35 1009.68,-958.35 752.32,-958.35"/>
<polygon fill="none" stroke="black" points="752.32,-958.35 752.32,-980.35 1009.68,-980.35 1009.68,-958.35 752.32,-958.35"/>
<text xml:space="preserve" text-anchor="start" x="861" y="-967.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">Runtime</text>
<polygon fill="none" stroke="black" points="752.32,-936.35 752.32,-958.35 1009.68,-958.35 1009.68,-936.35 752.32,-936.35"/>
<text xml:space="preserve" text-anchor="start" x="757.32" y="-944.35" font-family="Helvetica,sans-Serif" font-size="10.00">config: RuntimeConfig</text>
<polygon fill="none" stroke="black" points="752.32,-914.35 752.32,-936.35 1009.68,-936.35 1009.68,-914.35 752.32,-914.35"/>
<text xml:space="preserve" text-anchor="start" x="757.32" y="-922.35" font-family="Helvetica,sans-Serif" font-size="10.00">address_map: Arc&lt;AddressMap&gt;</text>
<polygon fill="none" stroke="black" points="752.32,-892.35 752.32,-914.35 1009.68,-914.35 1009.68,-892.35 752.32,-892.35"/>
<text xml:space="preserve" text-anchor="start" x="757.32" y="-900.35" font-family="Helvetica,sans-Serif" font-size="10.00">inbox_registry: Arc&lt;InboxRegistry&gt;</text>
<polygon fill="none" stroke="black" points="752.32,-870.35 752.32,-892.35 1009.68,-892.35 1009.68,-870.35 752.32,-870.35"/>
<text xml:space="preserve" text-anchor="start" x="757.32" y="-878.35" font-family="Helvetica,sans-Serif" font-size="10.00">transfer_txs: Vec&lt;Sender&lt;Envelope&gt;&gt;</text>
<polygon fill="none" stroke="black" points="752.32,-848.35 752.32,-870.35 1009.68,-870.35 1009.68,-848.35 752.32,-848.35"/>
<text xml:space="preserve" text-anchor="start" x="757.32" y="-856.35" font-family="Helvetica,sans-Serif" font-size="10.00">spawn_txs: Vec&lt;Sender&lt;(Addr, Box&lt;dyn AnyActor&gt;)&gt;&gt;</text>
<polygon fill="none" stroke="black" points="752.32,-826.35 752.32,-848.35 1009.68,-848.35 1009.68,-826.35 752.32,-826.35"/>
<text xml:space="preserve" text-anchor="start" x="757.32" y="-834.35" font-family="Helvetica,sans-Serif" font-size="10.00">placement: Placement</text>
<polygon fill="none" stroke="black" points="752.32,-804.35 752.32,-826.35 1009.68,-826.35 1009.68,-804.35 752.32,-804.35"/>
<text xml:space="preserve" text-anchor="start" x="757.32" y="-812.35" font-family="Helvetica,sans-Serif" font-size="10.00">is_running: AtomicBool</text>
<polygon fill="none" stroke="black" points="752.32,-782.35 752.32,-804.35 1009.68,-804.35 1009.68,-782.35 752.32,-782.35"/>
<text xml:space="preserve" text-anchor="start" x="757.32" y="-790.35" font-family="Helvetica,sans-Serif" font-size="10.00">worker_stats: Vec&lt;Arc&lt;WorkerStats&gt;&gt;</text>
<polygon fill="none" stroke="black" points="752.32,-760.35 752.32,-782.35 1009.68,-782.35 1009.68,-760.35 752.32,-760.35"/>
<text xml:space="preserve" text-anchor="start" x="757.32" y="-768.35" font-family="Helvetica,sans-Serif" font-size="10.00">tick_workers: RefCell&lt;Vec&lt;Worker&gt;&gt;</text>
</g>
<!-- ContextInner -->
<g id="node10" class="node">
<title>ContextInner</title>
<ellipse fill="#a9dfbf" stroke="black" stroke-width="2" cx="631" cy="-605.75" rx="89.49" ry="42.14"/>
<text xml:space="preserve" text-anchor="start" x="583.77" y="-620.55" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00">«trait» ContextInner</text>
<text xml:space="preserve" text-anchor="start" x="578.72" y="-602.45" font-family="Helvetica,sans-Serif" font-size="9.00">fn send_any(addr, msg)</text>
<text xml:space="preserve" text-anchor="start" x="578.72" y="-585.65" font-family="Helvetica,sans-Serif" font-size="9.00">fn spawn_any(addr, actor)</text>
</g>
<!-- Runtime&#45;&gt;ContextInner -->
<g id="edge35" class="edge">
<title>Runtime&#45;&gt;ContextInner</title>
<path fill="none" stroke="#2e8b57" stroke-width="1.5" stroke-dasharray="1,5" d="M784.34,-755.94C784.34,-684.49 784.34,-606 784.34,-606 784.34,-606 732.91,-606 732.91,-606"/>
<polygon fill="none" stroke="#2e8b57" stroke-width="1.5" points="732.91,-602.5 722.91,-606 732.91,-609.5 732.91,-602.5"/>
<text xml:space="preserve" text-anchor="middle" x="794.25" y="-713.35" font-family="Helvetica,sans-Serif" font-size="9.00">impl</text>
</g>
<!-- AddressMap -->
<g id="node14" class="node">
<title>AddressMap</title>
<polygon fill="white" stroke="black" points="1237.17,-579.75 1237.17,-631.75 1498.83,-631.75 1498.83,-579.75 1237.17,-579.75"/>
<polygon fill="#8e44ad" stroke="none" points="1245.17,-605.75 1245.17,-627.75 1490.83,-627.75 1490.83,-605.75 1245.17,-605.75"/>
<polygon fill="none" stroke="black" points="1245.17,-605.75 1245.17,-627.75 1490.83,-627.75 1490.83,-605.75 1245.17,-605.75"/>
<text xml:space="preserve" text-anchor="start" x="1337.99" y="-614.75" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">AddressMap</text>
<polygon fill="none" stroke="black" points="1245.17,-583.75 1245.17,-605.75 1490.83,-605.75 1490.83,-583.75 1245.17,-583.75"/>
<text xml:space="preserve" text-anchor="start" x="1250.17" y="-591.75" font-family="Helvetica,sans-Serif" font-size="10.00">inner: RwLock&lt;HashMap&lt;ActorAddress, WorkerId&gt;&gt;</text>
</g>
<!-- Runtime&#45;&gt;AddressMap -->
<g id="edge19" class="edge">
<title>Runtime&#45;&gt;AddressMap</title>
<path fill="none" stroke="#e67300" stroke-width="2.5" d="M1017.95,-761C1148.2,-761 1324.39,-761 1324.39,-761 1324.39,-761 1324.39,-645.87 1324.39,-645.87"/>
<polygon fill="#e67300" stroke="#e67300" stroke-width="2.5" points="1327.89,-645.87 1324.39,-635.87 1320.89,-645.87 1327.89,-645.87"/>
<text xml:space="preserve" text-anchor="middle" x="982.75" y="-713.35" font-family="Helvetica,sans-Serif" font-size="9.00">Arc</text>
</g>
<!-- Placement -->
<g id="node15" class="node">
<title>Placement</title>
<polygon fill="white" stroke="black" points="1527.54,-568.75 1527.54,-642.75 1642.46,-642.75 1642.46,-568.75 1527.54,-568.75"/>
<polygon fill="#8e44ad" stroke="none" points="1535.54,-616.75 1535.54,-638.75 1634.46,-638.75 1634.46,-616.75 1535.54,-616.75"/>
<polygon fill="none" stroke="black" points="1535.54,-616.75 1535.54,-638.75 1634.46,-638.75 1634.46,-616.75 1535.54,-616.75"/>
<text xml:space="preserve" text-anchor="start" x="1559.99" y="-625.75" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">Placement</text>
<polygon fill="none" stroke="black" points="1535.54,-594.75 1535.54,-616.75 1634.46,-616.75 1634.46,-594.75 1535.54,-594.75"/>
<text xml:space="preserve" text-anchor="start" x="1540.54" y="-602.75" font-family="Helvetica,sans-Serif" font-size="10.00">next: AtomicUsize</text>
<polygon fill="none" stroke="black" points="1535.54,-572.75 1535.54,-594.75 1634.46,-594.75 1634.46,-572.75 1535.54,-572.75"/>
<text xml:space="preserve" text-anchor="start" x="1540.54" y="-580.75" font-family="Helvetica,sans-Serif" font-size="10.00">num_workers: usize</text>
</g>
<!-- Runtime&#45;&gt;Placement -->
<g id="edge1" class="edge">
<title>Runtime&#45;&gt;Placement</title>
<path fill="none" stroke="#333333" d="M1017.99,-765C1222.79,-765 1585,-765 1585,-765 1585,-765 1585,-654.6 1585,-654.6"/>
<polygon fill="#333333" stroke="#333333" points="1588.5,-654.6 1585,-644.6 1581.5,-654.6 1588.5,-654.6"/>
<text xml:space="preserve" text-anchor="middle" x="1202.51" y="-713.35" font-family="Helvetica,sans-Serif" font-size="9.00">owns</text>
</g>
<!-- InboxRegistry -->
<g id="node16" class="node">
<title>InboxRegistry</title>
<polygon fill="white" stroke="black" points="1671.26,-579.75 1671.26,-631.75 1950.74,-631.75 1950.74,-579.75 1671.26,-579.75"/>
<polygon fill="#8e44ad" stroke="none" points="1679.26,-605.75 1679.26,-627.75 1942.74,-627.75 1942.74,-605.75 1679.26,-605.75"/>
<polygon fill="none" stroke="black" points="1679.26,-605.75 1679.26,-627.75 1942.74,-627.75 1942.74,-605.75 1679.26,-605.75"/>
<text xml:space="preserve" text-anchor="start" x="1777.66" y="-614.75" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">InboxRegistry</text>
<polygon fill="none" stroke="black" points="1679.26,-583.75 1679.26,-605.75 1942.74,-605.75 1942.74,-583.75 1679.26,-583.75"/>
<text xml:space="preserve" text-anchor="start" x="1684.26" y="-591.75" font-family="Helvetica,sans-Serif" font-size="10.00">senders: RwLock&lt;HashMap&lt;Addr, Arc&lt;dyn SenderT&gt;&gt;&gt;</text>
</g>
<!-- Runtime&#45;&gt;InboxRegistry -->
<g id="edge20" class="edge">
<title>Runtime&#45;&gt;InboxRegistry</title>
<path fill="none" stroke="#e67300" stroke-width="2.5" d="M1017.84,-770C1253.2,-770 1709.66,-770 1709.66,-770 1709.66,-770 1709.66,-645.92 1709.66,-645.92"/>
<polygon fill="#e67300" stroke="#e67300" stroke-width="2.5" points="1713.16,-645.92 1709.66,-635.92 1706.16,-645.92 1713.16,-645.92"/>
<text xml:space="preserve" text-anchor="middle" x="1337.75" y="-713.35" font-family="Helvetica,sans-Serif" font-size="9.00">Arc</text>
</g>
<!-- Worker -->
<g id="node19" class="node">
<title>Worker</title>
<polygon fill="white" stroke="black" points="1979.39,-535.75 1979.39,-675.75 2226.61,-675.75 2226.61,-535.75 1979.39,-535.75"/>
<polygon fill="#c0392b" stroke="none" points="1987.39,-649.75 1987.39,-671.75 2218.61,-671.75 2218.61,-649.75 1987.39,-649.75"/>
<polygon fill="none" stroke="black" points="1987.39,-649.75 1987.39,-671.75 2218.61,-671.75 2218.61,-649.75 1987.39,-649.75"/>
<text xml:space="preserve" text-anchor="start" x="2085.77" y="-658.75" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">Worker</text>
<polygon fill="none" stroke="black" points="1987.39,-627.75 1987.39,-649.75 2218.61,-649.75 2218.61,-627.75 1987.39,-627.75"/>
<text xml:space="preserve" text-anchor="start" x="1992.39" y="-635.75" font-family="Helvetica,sans-Serif" font-size="10.00">id: WorkerId</text>
<polygon fill="none" stroke="black" points="1987.39,-605.75 1987.39,-627.75 2218.61,-627.75 2218.61,-605.75 1987.39,-605.75"/>
<text xml:space="preserve" text-anchor="start" x="1992.39" y="-613.75" font-family="Helvetica,sans-Serif" font-size="10.00">pool: ActorPool</text>
<polygon fill="none" stroke="black" points="1987.39,-583.75 1987.39,-605.75 2218.61,-605.75 2218.61,-583.75 1987.39,-583.75"/>
<text xml:space="preserve" text-anchor="start" x="1992.39" y="-591.75" font-family="Helvetica,sans-Serif" font-size="10.00">transfer_rx: Receiver&lt;Envelope&gt;</text>
<polygon fill="none" stroke="black" points="1987.39,-561.75 1987.39,-583.75 2218.61,-583.75 2218.61,-561.75 1987.39,-561.75"/>
<text xml:space="preserve" text-anchor="start" x="1992.39" y="-569.75" font-family="Helvetica,sans-Serif" font-size="10.00">spawn_rx: Receiver&lt;(Addr, Box&lt;dyn AnyActor&gt;)&gt;</text>
<polygon fill="none" stroke="black" points="1987.39,-539.75 1987.39,-561.75 2218.61,-561.75 2218.61,-539.75 1987.39,-539.75"/>
<text xml:space="preserve" text-anchor="start" x="1992.39" y="-547.75" font-family="Helvetica,sans-Serif" font-size="10.00">stats: Arc&lt;WorkerStats&gt;</text>
</g>
<!-- Runtime&#45;&gt;Worker -->
<g id="edge38" class="edge">
<title>Runtime&#45;&gt;Worker</title>
<path fill="none" stroke="#333333" d="M977.66,-756.12C977.66,-707.59 977.66,-662 977.66,-662 977.66,-662 1967.57,-662 1967.57,-662"/>
<polygon fill="#333333" stroke="#333333" points="1967.57,-665.5 1977.57,-662 1967.57,-658.5 1967.57,-665.5"/>
<text xml:space="preserve" text-anchor="middle" x="2082.02" y="-713.35" font-family="Helvetica,sans-Serif" font-size="9.00">RefCell&lt;Vec&lt;&gt;&gt;</text>
</g>
<!-- SenderCh -->
<g id="node24" class="node">
<title>SenderCh</title>
<polygon fill="white" stroke="black" points="2266.01,-579.75 2266.01,-631.75 2435.99,-631.75 2435.99,-579.75 2266.01,-579.75"/>
<polygon fill="#148f77" stroke="none" points="2274.01,-605.75 2274.01,-627.75 2427.99,-627.75 2427.99,-605.75 2274.01,-605.75"/>
<polygon fill="none" stroke="black" points="2274.01,-605.75 2274.01,-627.75 2427.99,-627.75 2427.99,-605.75 2274.01,-605.75"/>
<text xml:space="preserve" text-anchor="start" x="2325.16" y="-614.75" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">Sender&lt;T&gt;</text>
<polygon fill="none" stroke="black" points="2274.01,-583.75 2274.01,-605.75 2427.99,-605.75 2427.99,-583.75 2274.01,-583.75"/>
<text xml:space="preserve" text-anchor="start" x="2279.01" y="-591.75" font-family="Helvetica,sans-Serif" font-size="10.00">queue: Arc&lt;HybridChannel&lt;T&gt;&gt;</text>
</g>
<!-- Runtime&#45;&gt;SenderCh -->
<g id="edge15" class="edge">
<title>Runtime&#45;&gt;SenderCh</title>
<path fill="none" stroke="#333333" d="M1017.93,-775C1377.79,-775 2322.67,-775 2322.67,-775 2322.67,-775 2322.67,-643.71 2322.67,-643.71"/>
<polygon fill="#333333" stroke="#333333" points="2326.17,-643.71 2322.67,-633.71 2319.17,-643.71 2326.17,-643.71"/>
<text xml:space="preserve" text-anchor="middle" x="2299.39" y="-718.75" font-family="Helvetica,sans-Serif" font-size="9.00">transfer_txs +</text>
<text xml:space="preserve" text-anchor="middle" x="2299.39" y="-707.95" font-family="Helvetica,sans-Serif" font-size="9.00">spawn_txs</text>
</g>
<!-- WorkerStats -->
<g id="node26" class="node">
<title>WorkerStats</title>
<polygon fill="white" stroke="black" points="2695.13,-348.15 2695.13,-444.15 2872.87,-444.15 2872.87,-348.15 2695.13,-348.15"/>
<polygon fill="#3949ab" stroke="none" points="2703.13,-418.15 2703.13,-440.15 2864.87,-440.15 2864.87,-418.15 2703.13,-418.15"/>
<polygon fill="none" stroke="black" points="2703.13,-418.15 2703.13,-440.15 2864.87,-440.15 2864.87,-418.15 2703.13,-418.15"/>
<text xml:space="preserve" text-anchor="start" x="2754.55" y="-427.15" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">WorkerStats</text>
<polygon fill="none" stroke="black" points="2703.13,-396.15 2703.13,-418.15 2864.87,-418.15 2864.87,-396.15 2703.13,-396.15"/>
<text xml:space="preserve" text-anchor="start" x="2708.13" y="-404.15" font-family="Helvetica,sans-Serif" font-size="10.00">num_actors: AtomicUsize</text>
<polygon fill="none" stroke="black" points="2703.13,-374.15 2703.13,-396.15 2864.87,-396.15 2864.87,-374.15 2703.13,-374.15"/>
<text xml:space="preserve" text-anchor="start" x="2708.13" y="-382.15" font-family="Helvetica,sans-Serif" font-size="10.00">total_mailbox_depth: AtomicUsize</text>
<polygon fill="none" stroke="black" points="2703.13,-352.15 2703.13,-374.15 2864.87,-374.15 2864.87,-352.15 2703.13,-352.15"/>
<text xml:space="preserve" text-anchor="start" x="2708.13" y="-360.15" font-family="Helvetica,sans-Serif" font-size="10.00">messages_processed: AtomicU64</text>
</g>
<!-- Runtime&#45;&gt;WorkerStats -->
<g id="edge21" class="edge">
<title>Runtime&#45;&gt;WorkerStats</title>
<path fill="none" stroke="#e67300" stroke-width="2.5" d="M1017.94,-784C1442.45,-784 2707.95,-784 2707.95,-784 2707.95,-784 2707.95,-458.27 2707.95,-458.27"/>
<polygon fill="#e67300" stroke="#e67300" stroke-width="2.5" points="2711.45,-458.27 2707.95,-448.27 2704.45,-458.27 2711.45,-458.27"/>
<text xml:space="preserve" text-anchor="middle" x="2797.76" y="-603.05" font-family="Helvetica,sans-Serif" font-size="9.00">Vec&lt;Arc&gt;</text>
</g>
<!-- RuntimeConfig -->
<g id="node29" class="node">
<title>RuntimeConfig</title>
<polygon fill="white" stroke="black" points="2516.52,-546.75 2516.52,-664.75 2671.48,-664.75 2671.48,-546.75 2516.52,-546.75"/>
<polygon fill="#d4ac0d" stroke="none" points="2524.52,-638.75 2524.52,-660.75 2663.48,-660.75 2663.48,-638.75 2524.52,-638.75"/>
<polygon fill="none" stroke="black" points="2524.52,-638.75 2524.52,-660.75 2663.48,-660.75 2663.48,-638.75 2524.52,-638.75"/>
<text xml:space="preserve" text-anchor="start" x="2558.17" y="-647.75" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">RuntimeConfig</text>
<polygon fill="none" stroke="black" points="2524.52,-616.75 2524.52,-638.75 2663.48,-638.75 2663.48,-616.75 2524.52,-616.75"/>
<text xml:space="preserve" text-anchor="start" x="2529.52" y="-624.75" font-family="Helvetica,sans-Serif" font-size="10.00">max_actors: usize</text>
<polygon fill="none" stroke="black" points="2524.52,-594.75 2524.52,-616.75 2663.48,-616.75 2663.48,-594.75 2524.52,-594.75"/>
<text xml:space="preserve" text-anchor="start" x="2529.52" y="-602.75" font-family="Helvetica,sans-Serif" font-size="10.00">actor_max_messages: usize</text>
<polygon fill="none" stroke="black" points="2524.52,-572.75 2524.52,-594.75 2663.48,-594.75 2663.48,-572.75 2524.52,-572.75"/>
<text xml:space="preserve" text-anchor="start" x="2529.52" y="-580.75" font-family="Helvetica,sans-Serif" font-size="10.00">num_threads: usize</text>
<polygon fill="none" stroke="black" points="2524.52,-550.75 2524.52,-572.75 2663.48,-572.75 2663.48,-550.75 2524.52,-550.75"/>
<text xml:space="preserve" text-anchor="start" x="2529.52" y="-558.75" font-family="Helvetica,sans-Serif" font-size="10.00">backoff_policy: BackoffPolicy</text>
</g>
<!-- Runtime&#45;&gt;RuntimeConfig -->
<g id="edge2" class="edge">
<title>Runtime&#45;&gt;RuntimeConfig</title>
<path fill="none" stroke="#333333" d="M1018,-779C1419.98,-779 2568.17,-779 2568.17,-779 2568.17,-779 2568.17,-736.04 2568.17,-705.72"/>
<polygon fill="#333333" stroke="#333333" points="2571.68,-705.96 2568.17,-695.96 2564.68,-705.96 2571.68,-705.96"/>
<text xml:space="preserve" text-anchor="middle" x="2487.51" y="-713.35" font-family="Helvetica,sans-Serif" font-size="9.00">owns</text>
</g>
<!-- RuntimeHandle -->
<g id="node3" class="node">
<title>RuntimeHandle</title>
<polygon fill="white" stroke="black" points="799.9,-1065.15 799.9,-1139.15 962.1,-1139.15 962.1,-1065.15 799.9,-1065.15"/>
<polygon fill="#2980b9" stroke="none" points="807.9,-1113.15 807.9,-1135.15 954.1,-1135.15 954.1,-1113.15 807.9,-1113.15"/>
<polygon fill="none" stroke="black" points="807.9,-1113.15 807.9,-1135.15 954.1,-1135.15 954.1,-1113.15 807.9,-1113.15"/>
<text xml:space="preserve" text-anchor="start" x="844.33" y="-1122.15" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">RuntimeHandle</text>
<polygon fill="none" stroke="black" points="807.9,-1091.15 807.9,-1113.15 954.1,-1113.15 954.1,-1091.15 807.9,-1091.15"/>
<text xml:space="preserve" text-anchor="start" x="812.9" y="-1099.15" font-family="Helvetica,sans-Serif" font-size="10.00">runtime: Arc&lt;Runtime&gt;</text>
<polygon fill="none" stroke="black" points="807.9,-1069.15 807.9,-1091.15 954.1,-1091.15 954.1,-1069.15 807.9,-1069.15"/>
<text xml:space="preserve" text-anchor="start" x="812.9" y="-1077.15" font-family="Helvetica,sans-Serif" font-size="10.00">threads: Vec&lt;JoinHandle&lt;()&gt;&gt;</text>
</g>
<!-- RuntimeHandle&#45;&gt;Runtime -->
<g id="edge18" class="edge">
<title>RuntimeHandle&#45;&gt;Runtime</title>
<path fill="none" stroke="#e67300" stroke-width="2.5" d="M881,-1064.9C881,-1064.9 881,-998.45 881,-998.45"/>
<polygon fill="#e67300" stroke="#e67300" stroke-width="2.5" points="884.5,-998.45 881,-988.45 877.5,-998.45 884.5,-998.45"/>
<text xml:space="preserve" text-anchor="middle" x="887.75" y="-1016.55" font-family="Helvetica,sans-Serif" font-size="9.00">Arc</text>
</g>
<!-- Inbox -->
<g id="node4" class="node">
<title>Inbox</title>
<polygon fill="white" stroke="black" points="824.37,-568.75 824.37,-642.75 937.63,-642.75 937.63,-568.75 824.37,-568.75"/>
<polygon fill="#2980b9" stroke="none" points="832.37,-616.75 832.37,-638.75 929.63,-638.75 929.63,-616.75 832.37,-616.75"/>
<polygon fill="none" stroke="black" points="832.37,-616.75 832.37,-638.75 929.63,-638.75 929.63,-616.75 832.37,-616.75"/>
<text xml:space="preserve" text-anchor="start" x="857.66" y="-625.75" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">Inbox&lt;M&gt;</text>
<polygon fill="none" stroke="black" points="832.37,-594.75 832.37,-616.75 929.63,-616.75 929.63,-594.75 832.37,-594.75"/>
<text xml:space="preserve" text-anchor="start" x="837.37" y="-602.75" font-family="Helvetica,sans-Serif" font-size="10.00">addr: ActorAddress</text>
<polygon fill="none" stroke="black" points="832.37,-572.75 832.37,-594.75 929.63,-594.75 929.63,-572.75 832.37,-572.75"/>
<text xml:space="preserve" text-anchor="start" x="837.37" y="-580.75" font-family="Helvetica,sans-Serif" font-size="10.00">inner: Receiver&lt;M&gt;</text>
</g>
<!-- ActorAddress -->
<g id="node5" class="node">
<title>ActorAddress</title>
<polygon fill="white" stroke="black" points="623.98,-359.15 623.98,-433.15 720.02,-433.15 720.02,-359.15 623.98,-359.15"/>
<polygon fill="#27ae60" stroke="none" points="631.98,-407.15 631.98,-429.15 712.02,-429.15 712.02,-407.15 631.98,-407.15"/>
<polygon fill="none" stroke="black" points="631.98,-407.15 631.98,-429.15 712.02,-429.15 712.02,-407.15 631.98,-407.15"/>
<text xml:space="preserve" text-anchor="start" x="638.94" y="-416.15" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">ActorAddress</text>
<polygon fill="none" stroke="black" points="631.98,-385.15 631.98,-407.15 712.02,-407.15 712.02,-385.15 631.98,-385.15"/>
<text xml:space="preserve" text-anchor="start" x="636.98" y="-393.15" font-family="Helvetica,sans-Serif" font-size="10.00">0: [u8; 32]</text>
<polygon fill="none" stroke="black" points="631.98,-363.15 631.98,-385.15 712.02,-385.15 712.02,-363.15 631.98,-363.15"/>
<text xml:space="preserve" text-anchor="start" x="636.98" y="-372.15" font-family="Helvetica,sans-Serif" font-style="italic" font-size="10.00">Copy, Eq, Hash</text>
</g>
<!-- Inbox&#45;&gt;ActorAddress -->
<g id="edge10" class="edge">
<title>Inbox&#45;&gt;ActorAddress</title>
<path fill="none" stroke="#333333" d="M862.12,-568.47C862.12,-517.42 862.12,-432 862.12,-432 862.12,-432 731.98,-432 731.98,-432"/>
<polygon fill="#333333" stroke="#333333" points="731.98,-428.5 721.98,-432 731.98,-435.5 731.98,-428.5"/>
<text xml:space="preserve" text-anchor="middle" x="781.01" y="-492.75" font-family="Helvetica,sans-Serif" font-size="9.00">addr</text>
</g>
<!-- ReceiverCh -->
<g id="node25" class="node">
<title>ReceiverCh</title>
<polygon fill="white" stroke="black" points="2251.01,-370.15 2251.01,-422.15 2420.99,-422.15 2420.99,-370.15 2251.01,-370.15"/>
<polygon fill="#148f77" stroke="none" points="2259.01,-396.15 2259.01,-418.15 2412.99,-418.15 2412.99,-396.15 2259.01,-396.15"/>
<polygon fill="none" stroke="black" points="2259.01,-396.15 2259.01,-418.15 2412.99,-418.15 2412.99,-396.15 2259.01,-396.15"/>
<text xml:space="preserve" text-anchor="start" x="2306.26" y="-405.15" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">Receiver&lt;T&gt;</text>
<polygon fill="none" stroke="black" points="2259.01,-374.15 2259.01,-396.15 2412.99,-396.15 2412.99,-374.15 2259.01,-374.15"/>
<text xml:space="preserve" text-anchor="start" x="2264.01" y="-382.15" font-family="Helvetica,sans-Serif" font-size="10.00">queue: Arc&lt;HybridChannel&lt;T&gt;&gt;</text>
</g>
<!-- Inbox&#45;&gt;ReceiverCh -->
<g id="edge9" class="edge">
<title>Inbox&#45;&gt;ReceiverCh</title>
<path fill="none" stroke="#333333" d="M899.88,-568.46C899.88,-520.98 899.88,-445 899.88,-445 899.88,-445 2256.01,-445 2256.01,-445 2256.01,-445 2256.01,-434.09 2256.01,-434.09"/>
<polygon fill="#333333" stroke="#333333" points="2259.51,-434.09 2256.01,-424.09 2252.51,-434.09 2259.51,-434.09"/>
<text xml:space="preserve" text-anchor="middle" x="2363.51" y="-492.75" font-family="Helvetica,sans-Serif" font-size="9.00">owns</text>
</g>
<!-- Actor -->
<g id="node6" class="node">
<title>Actor</title>
<polygon fill="white" stroke="black" points="402.75,-204.35 402.75,-256.35 563.25,-256.35 563.25,-204.35 402.75,-204.35"/>
<polygon fill="#27ae60" stroke="none" points="410.75,-230.35 410.75,-252.35 555.25,-252.35 555.25,-230.35 410.75,-230.35"/>
<polygon fill="none" stroke="black" points="410.75,-230.35 410.75,-252.35 555.25,-252.35 555.25,-230.35 410.75,-230.35"/>
<text xml:space="preserve" text-anchor="start" x="460.49" y="-239.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">Actor&lt;A&gt;</text>
<polygon fill="none" stroke="black" points="410.75,-208.35 410.75,-230.35 555.25,-230.35 555.25,-208.35 410.75,-208.35"/>
<text xml:space="preserve" text-anchor="start" x="415.75" y="-217.35" font-family="Helvetica,sans-Serif" font-size="10.00">0: A &#160;</text>
<text xml:space="preserve" text-anchor="start" x="439.1" y="-217.35" font-family="Helvetica,sans-Serif" font-style="italic" font-size="10.00">(where A: ActorInterface)</text>
</g>
<!-- ActorInterface -->
<g id="node8" class="node">
<title>ActorInterface</title>
<ellipse fill="#a9dfbf" stroke="black" stroke-width="2" cx="364" cy="-70.02" rx="99.73" ry="54.02"/>
<text xml:space="preserve" text-anchor="start" x="313.71" y="-93.22" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00">«trait» ActorInterface</text>
<text xml:space="preserve" text-anchor="start" x="304.48" y="-75.12" font-family="Helvetica,sans-Serif" font-size="9.00">type Incoming: Message</text>
<text xml:space="preserve" text-anchor="start" x="304.48" y="-58.32" font-family="Helvetica,sans-Serif" font-size="9.00">type Response: Message</text>
<text xml:space="preserve" text-anchor="start" x="304.48" y="-41.52" font-family="Helvetica,sans-Serif" font-size="9.00">fn handle(&amp;mut self, ctx, msg)</text>
</g>
<!-- Actor&#45;&gt;ActorInterface -->
<g id="edge34" class="edge">
<title>Actor&#45;&gt;ActorInterface</title>
<path fill="none" stroke="#2e8b57" stroke-width="1.5" stroke-dasharray="1,5" d="M433.24,-203.87C433.24,-203.87 433.24,-121.42 433.24,-121.42"/>
<polygon fill="none" stroke="#2e8b57" stroke-width="1.5" points="436.74,-121.42 433.24,-111.42 429.74,-121.42 436.74,-121.42"/>
<text xml:space="preserve" text-anchor="middle" x="415.01" y="-155.75" font-family="Helvetica,sans-Serif" font-size="9.00">wraps A: impl</text>
</g>
<!-- AnyActor -->
<g id="node9" class="node">
<title>AnyActor</title>
<ellipse fill="#a9dfbf" stroke="black" stroke-width="2" cx="606" cy="-70.02" rx="113.53" ry="30.26"/>
<text xml:space="preserve" text-anchor="start" x="567.11" y="-76.42" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00">«trait» AnyActor</text>
<text xml:space="preserve" text-anchor="start" x="536.72" y="-58.32" font-family="Helvetica,sans-Serif" font-size="9.00">fn handle_any(&amp;mut self, ctx, msg)</text>
</g>
<!-- Actor&#45;&gt;AnyActor -->
<g id="edge33" class="edge">
<title>Actor&#45;&gt;AnyActor</title>
<path fill="none" stroke="#2e8b57" stroke-width="1.5" stroke-dasharray="1,5" d="M527.86,-203.87C527.86,-203.87 527.86,-104.66 527.86,-104.66"/>
<polygon fill="none" stroke="#2e8b57" stroke-width="1.5" points="531.36,-104.66 527.86,-94.66 524.36,-104.66 531.36,-104.66"/>
<text xml:space="preserve" text-anchor="middle" x="564.25" y="-155.75" font-family="Helvetica,sans-Serif" font-size="9.00">impl</text>
</g>
<!-- Ctx -->
<g id="node7" class="node">
<title>Ctx</title>
<polygon fill="white" stroke="black" points="447.57,-833.35 447.57,-907.35 594.43,-907.35 594.43,-833.35 447.57,-833.35"/>
<polygon fill="#27ae60" stroke="none" points="455.57,-881.35 455.57,-903.35 586.43,-903.35 586.43,-881.35 455.57,-881.35"/>
<polygon fill="none" stroke="black" points="455.57,-881.35 455.57,-903.35 586.43,-903.35 586.43,-881.35 455.57,-881.35"/>
<text xml:space="preserve" text-anchor="start" x="503.13" y="-890.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">Ctx&lt;&#39;a&gt;</text>
<polygon fill="none" stroke="black" points="455.57,-859.35 455.57,-881.35 586.43,-881.35 586.43,-859.35 455.57,-859.35"/>
<text xml:space="preserve" text-anchor="start" x="460.57" y="-867.35" font-family="Helvetica,sans-Serif" font-size="10.00">inner: &amp;&#39;a dyn ContextInner</text>
<polygon fill="none" stroke="black" points="455.57,-837.35 455.57,-859.35 586.43,-859.35 586.43,-837.35 455.57,-837.35"/>
<text xml:space="preserve" text-anchor="start" x="460.57" y="-845.35" font-family="Helvetica,sans-Serif" font-size="10.00">self_addr: ActorAddress</text>
</g>
<!-- Ctx&#45;&gt;ActorAddress -->
<g id="edge11" class="edge">
<title>Ctx&#45;&gt;ActorAddress</title>
<path fill="none" stroke="#333333" d="M494.54,-833.12C494.54,-721.3 494.54,-396 494.54,-396 494.54,-396 611.99,-396 611.99,-396"/>
<polygon fill="#333333" stroke="#333333" points="611.99,-399.5 621.99,-396 611.99,-392.5 611.99,-399.5"/>
<text xml:space="preserve" text-anchor="middle" x="493.51" y="-603.05" font-family="Helvetica,sans-Serif" font-size="9.00">self_addr</text>
</g>
<!-- Ctx&#45;&gt;ContextInner -->
<g id="edge26" class="edge">
<title>Ctx&#45;&gt;ContextInner</title>
<path fill="none" stroke="#7b2d8b" stroke-width="1.5" stroke-dasharray="5,2" d="M567.97,-833C567.97,-833 567.97,-648.2 567.97,-648.2"/>
<polygon fill="#7b2d8b" stroke="#7b2d8b" stroke-width="1.5" points="571.47,-648.2 567.97,-638.2 564.47,-648.2 571.47,-648.2"/>
<text xml:space="preserve" text-anchor="middle" x="590.87" y="-713.35" font-family="Helvetica,sans-Serif" font-size="9.00">&amp;&#39;a dyn</text>
</g>
<!-- MessageTrait -->
<g id="node11" class="node">
<title>MessageTrait</title>
<ellipse fill="#a9dfbf" stroke="black" stroke-width="2" cx="493" cy="-1102.15" rx="99.02" ry="30.26"/>
<text xml:space="preserve" text-anchor="start" x="455.49" y="-1108.55" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00">«trait» Message</text>
<text xml:space="preserve" text-anchor="start" x="433.98" y="-1090.45" font-family="Helvetica,sans-Serif" font-size="9.00">&#39;static + Clone + Send + Sync</text>
</g>
<!-- Envelope -->
<g id="node12" class="node">
<title>Envelope</title>
<polygon fill="white" stroke="black" points="1041.81,-568.75 1041.81,-642.75 1208.19,-642.75 1208.19,-568.75 1041.81,-568.75"/>
<polygon fill="#8e44ad" stroke="none" points="1049.81,-616.75 1049.81,-638.75 1200.19,-638.75 1200.19,-616.75 1049.81,-616.75"/>
<polygon fill="none" stroke="black" points="1049.81,-616.75 1049.81,-638.75 1200.19,-638.75 1200.19,-616.75 1049.81,-616.75"/>
<text xml:space="preserve" text-anchor="start" x="1102.77" y="-625.75" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">Envelope</text>
<polygon fill="none" stroke="black" points="1049.81,-594.75 1049.81,-616.75 1200.19,-616.75 1200.19,-594.75 1049.81,-594.75"/>
<text xml:space="preserve" text-anchor="start" x="1054.81" y="-602.75" font-family="Helvetica,sans-Serif" font-size="10.00">dest: ActorAddress</text>
<polygon fill="none" stroke="black" points="1049.81,-572.75 1049.81,-594.75 1200.19,-594.75 1200.19,-572.75 1049.81,-572.75"/>
<text xml:space="preserve" text-anchor="start" x="1054.81" y="-580.75" font-family="Helvetica,sans-Serif" font-size="10.00">payload: Box&lt;dyn Any + Send&gt;</text>
</g>
<!-- Envelope&#45;&gt;ActorAddress -->
<g id="edge12" class="edge">
<title>Envelope&#45;&gt;ActorAddress</title>
<path fill="none" stroke="#333333" d="M1125,-568.47C1125,-516.89 1125,-430 1125,-430 1125,-430 731.99,-430 731.99,-430"/>
<polygon fill="#333333" stroke="#333333" points="731.99,-426.5 721.99,-430 731.99,-433.5 731.99,-426.5"/>
<text xml:space="preserve" text-anchor="middle" x="1032.51" y="-492.75" font-family="Helvetica,sans-Serif" font-size="9.00">dest</text>
</g>
<!-- WorkerId -->
<g id="node13" class="node">
<title>WorkerId</title>
<polygon fill="white" stroke="black" points="1470.33,-370.15 1470.33,-422.15 1539.67,-422.15 1539.67,-370.15 1470.33,-370.15"/>
<polygon fill="#8e44ad" stroke="none" points="1478.33,-396.15 1478.33,-418.15 1531.67,-418.15 1531.67,-396.15 1478.33,-396.15"/>
<polygon fill="none" stroke="black" points="1478.33,-396.15 1478.33,-418.15 1531.67,-418.15 1531.67,-396.15 1478.33,-396.15"/>
<text xml:space="preserve" text-anchor="start" x="1483.33" y="-405.15" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">WorkerId</text>
<polygon fill="none" stroke="black" points="1478.33,-374.15 1478.33,-396.15 1531.67,-396.15 1531.67,-374.15 1478.33,-374.15"/>
<text xml:space="preserve" text-anchor="start" x="1483.33" y="-382.15" font-family="Helvetica,sans-Serif" font-size="10.00">0: usize</text>
</g>
<!-- AddressMap&#45;&gt;ActorAddress -->
<g id="edge14" class="edge">
<title>AddressMap&#45;&gt;ActorAddress</title>
<path fill="none" stroke="#333333" d="M1353.75,-579.52C1353.75,-530.08 1353.75,-428 1353.75,-428 1353.75,-428 732,-428 732,-428"/>
<polygon fill="#333333" stroke="#333333" points="732,-424.5 722,-428 732,-431.5 732,-424.5"/>
<text xml:space="preserve" text-anchor="middle" x="1254.51" y="-492.75" font-family="Helvetica,sans-Serif" font-size="9.00">HashMap key</text>
</g>
<!-- AddressMap&#45;&gt;WorkerId -->
<g id="edge13" class="edge">
<title>AddressMap&#45;&gt;WorkerId</title>
<path fill="none" stroke="#333333" d="M1484.58,-579.62C1484.58,-579.62 1484.58,-434.11 1484.58,-434.11"/>
<polygon fill="#333333" stroke="#333333" points="1488.08,-434.11 1484.58,-424.11 1481.08,-434.11 1488.08,-434.11"/>
<text xml:space="preserve" text-anchor="middle" x="1462.26" y="-492.75" font-family="Helvetica,sans-Serif" font-size="9.00">HashMap val</text>
</g>
<!-- SenderT -->
<g id="node18" class="node">
<title>SenderT</title>
<ellipse fill="#d2b4de" stroke="black" stroke-width="2" cx="1824" cy="-396.15" rx="122.03" ry="30.26"/>
<text xml:space="preserve" text-anchor="start" x="1787.6" y="-402.55" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00">«trait» SenderT</text>
<text xml:space="preserve" text-anchor="start" x="1748.71" y="-384.45" font-family="Helvetica,sans-Serif" font-size="9.00">fn try_send_any(msg: Box&lt;dyn Any&gt;)</text>
</g>
<!-- InboxRegistry&#45;&gt;SenderT -->
<g id="edge25" class="edge">
<title>InboxRegistry&#45;&gt;SenderT</title>
<path fill="none" stroke="#e67300" stroke-width="2.5" d="M1783.32,-579.62C1783.32,-579.62 1783.32,-438.74 1783.32,-438.74"/>
<polygon fill="#e67300" stroke="#e67300" stroke-width="2.5" points="1786.82,-438.74 1783.32,-428.74 1779.82,-438.74 1786.82,-438.74"/>
<text xml:space="preserve" text-anchor="middle" x="1834.26" y="-492.75" font-family="Helvetica,sans-Serif" font-size="9.00">Arc&lt;dyn&gt;</text>
</g>
<!-- TickContext -->
<g id="node17" class="node">
<title>TickContext</title>
<polygon fill="white" stroke="black" points="1748.05,-789.35 1748.05,-951.35 1939.95,-951.35 1939.95,-789.35 1748.05,-789.35"/>
<polygon fill="#8e44ad" stroke="none" points="1756.05,-925.35 1756.05,-947.35 1931.95,-947.35 1931.95,-925.35 1756.05,-925.35"/>
<polygon fill="none" stroke="black" points="1756.05,-925.35 1756.05,-947.35 1931.95,-947.35 1931.95,-925.35 1756.05,-925.35"/>
<text xml:space="preserve" text-anchor="start" x="1805.57" y="-934.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">TickContext&lt;&#39;a&gt;</text>
<polygon fill="none" stroke="black" points="1756.05,-903.35 1756.05,-925.35 1931.95,-925.35 1931.95,-903.35 1756.05,-903.35"/>
<text xml:space="preserve" text-anchor="start" x="1761.05" y="-911.35" font-family="Helvetica,sans-Serif" font-size="10.00">address_map: &amp;&#39;a AddressMap</text>
<polygon fill="none" stroke="black" points="1756.05,-881.35 1756.05,-903.35 1931.95,-903.35 1931.95,-881.35 1756.05,-881.35"/>
<text xml:space="preserve" text-anchor="start" x="1761.05" y="-889.35" font-family="Helvetica,sans-Serif" font-size="10.00">transfer_txs: &amp;&#39;a [Sender&lt;Envelope&gt;]</text>
<polygon fill="none" stroke="black" points="1756.05,-859.35 1756.05,-881.35 1931.95,-881.35 1931.95,-859.35 1756.05,-859.35"/>
<text xml:space="preserve" text-anchor="start" x="1761.05" y="-867.35" font-family="Helvetica,sans-Serif" font-size="10.00">spawn_txs: &amp;&#39;a [Sender&lt;...&gt;]</text>
<polygon fill="none" stroke="black" points="1756.05,-837.35 1756.05,-859.35 1931.95,-859.35 1931.95,-837.35 1756.05,-837.35"/>
<text xml:space="preserve" text-anchor="start" x="1761.05" y="-845.35" font-family="Helvetica,sans-Serif" font-size="10.00">placement: &amp;&#39;a Placement</text>
<polygon fill="none" stroke="black" points="1756.05,-815.35 1756.05,-837.35 1931.95,-837.35 1931.95,-815.35 1756.05,-815.35"/>
<text xml:space="preserve" text-anchor="start" x="1761.05" y="-823.35" font-family="Helvetica,sans-Serif" font-size="10.00">inbox_registry: &amp;&#39;a InboxRegistry</text>
<polygon fill="none" stroke="black" points="1756.05,-793.35 1756.05,-815.35 1931.95,-815.35 1931.95,-793.35 1756.05,-793.35"/>
<text xml:space="preserve" text-anchor="start" x="1761.05" y="-801.35" font-family="Helvetica,sans-Serif" font-size="10.00">config: &amp;&#39;a RuntimeConfig</text>
</g>
<!-- TickContext&#45;&gt;AddressMap -->
<g id="edge27" class="edge">
<title>TickContext&#45;&gt;AddressMap</title>
<path fill="none" stroke="#7b2d8b" stroke-width="1.5" stroke-dasharray="5,2" d="M1747.77,-870C1620.36,-870 1411.61,-870 1411.61,-870 1411.61,-870 1411.61,-644.32 1411.61,-644.32"/>
<polygon fill="#7b2d8b" stroke="#7b2d8b" stroke-width="1.5" points="1415.11,-644.32 1411.61,-634.32 1408.11,-644.32 1415.11,-644.32"/>
<text xml:space="preserve" text-anchor="middle" x="1509.36" y="-713.35" font-family="Helvetica,sans-Serif" font-size="9.00">&amp;&#39;a</text>
</g>
<!-- TickContext&#45;&gt;Placement -->
<g id="edge29" class="edge">
<title>TickContext&#45;&gt;Placement</title>
<path fill="none" stroke="#7b2d8b" stroke-width="1.5" stroke-dasharray="5,2" d="M1812.02,-789.08C1812.02,-722.08 1812.02,-638 1812.02,-638 1812.02,-638 1655.14,-638 1655.14,-638"/>
<polygon fill="#7b2d8b" stroke="#7b2d8b" stroke-width="1.5" points="1655.14,-634.5 1645.14,-638 1655.14,-641.5 1655.14,-634.5"/>
<text xml:space="preserve" text-anchor="middle" x="1691.36" y="-713.35" font-family="Helvetica,sans-Serif" font-size="9.00">&amp;&#39;a</text>
</g>
<!-- TickContext&#45;&gt;InboxRegistry -->
<g id="edge30" class="edge">
<title>TickContext&#45;&gt;InboxRegistry</title>
<path fill="none" stroke="#7b2d8b" stroke-width="1.5" stroke-dasharray="5,2" d="M1875.98,-789.16C1875.98,-789.16 1875.98,-644.27 1875.98,-644.27"/>
<polygon fill="#7b2d8b" stroke="#7b2d8b" stroke-width="1.5" points="1879.48,-644.27 1875.98,-634.27 1872.48,-644.27 1879.48,-644.27"/>
<text xml:space="preserve" text-anchor="middle" x="1839.36" y="-713.35" font-family="Helvetica,sans-Serif" font-size="9.00">&amp;&#39;a</text>
</g>
<!-- TickContext&#45;&gt;SenderCh -->
<g id="edge28" class="edge">
<title>TickContext&#45;&gt;SenderCh</title>
<path fill="none" stroke="#7b2d8b" stroke-width="1.5" stroke-dasharray="5,2" d="M1940.32,-796C2094.45,-796 2379.33,-796 2379.33,-796 2379.33,-796 2379.33,-644.47 2379.33,-644.47"/>
<polygon fill="#7b2d8b" stroke="#7b2d8b" stroke-width="1.5" points="2382.83,-644.47 2379.33,-634.47 2375.83,-644.47 2382.83,-644.47"/>
<text xml:space="preserve" text-anchor="middle" x="2371.11" y="-713.35" font-family="Helvetica,sans-Serif" font-size="9.00">&amp;&#39;a []</text>
</g>
<!-- TickContext&#45;&gt;RuntimeConfig -->
<g id="edge31" class="edge">
<title>TickContext&#45;&gt;RuntimeConfig</title>
<path fill="none" stroke="#7b2d8b" stroke-width="1.5" stroke-dasharray="5,2" d="M1940.3,-804C2148.07,-804 2619.83,-804 2619.83,-804 2619.83,-804 2619.83,-677.5 2619.83,-677.5"/>
<polygon fill="#7b2d8b" stroke="#7b2d8b" stroke-width="1.5" points="2623.33,-677.5 2619.83,-667.5 2616.33,-677.5 2623.33,-677.5"/>
<text xml:space="preserve" text-anchor="middle" x="2569.36" y="-713.35" font-family="Helvetica,sans-Serif" font-size="9.00">&amp;&#39;a</text>
</g>
<!-- Worker&#45;&gt;WorkerId -->
<g id="edge6" class="edge">
<title>Worker&#45;&gt;WorkerId</title>
<path fill="none" stroke="#333333" d="M1979.1,-549C1811.6,-549 1533.6,-549 1533.6,-549 1533.6,-549 1533.6,-433.94 1533.6,-433.94"/>
<polygon fill="#333333" stroke="#333333" points="1537.1,-433.94 1533.6,-423.94 1530.1,-433.94 1537.1,-433.94"/>
<text xml:space="preserve" text-anchor="middle" x="1974.5" y="-492.75" font-family="Helvetica,sans-Serif" font-size="9.00">id</text>
</g>
<!-- ActorPool -->
<g id="node21" class="node">
<title>ActorPool</title>
<polygon fill="white" stroke="black" points="1991.29,-370.15 1991.29,-422.15 2210.71,-422.15 2210.71,-370.15 1991.29,-370.15"/>
<polygon fill="#c0392b" stroke="none" points="1999.29,-396.15 1999.29,-418.15 2202.71,-418.15 2202.71,-396.15 1999.29,-396.15"/>
<polygon fill="none" stroke="black" points="1999.29,-396.15 1999.29,-418.15 2202.71,-418.15 2202.71,-396.15 1999.29,-396.15"/>
<text xml:space="preserve" text-anchor="start" x="2077.11" y="-405.15" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">ActorPool</text>
<polygon fill="none" stroke="black" points="1999.29,-374.15 1999.29,-396.15 2202.71,-396.15 2202.71,-374.15 1999.29,-374.15"/>
<text xml:space="preserve" text-anchor="start" x="2004.29" y="-382.15" font-family="Helvetica,sans-Serif" font-size="10.00">actors: HashMap&lt;ActorAddress, ActorSlot&gt;</text>
</g>
<!-- Worker&#45;&gt;ActorPool -->
<g id="edge5" class="edge">
<title>Worker&#45;&gt;ActorPool</title>
<path fill="none" stroke="#333333" d="M2101,-535.37C2101,-535.37 2101,-434.08 2101,-434.08"/>
<polygon fill="#333333" stroke="#333333" points="2104.5,-434.08 2101,-424.08 2097.5,-434.08 2104.5,-434.08"/>
<text xml:space="preserve" text-anchor="middle" x="2112.51" y="-492.75" font-family="Helvetica,sans-Serif" font-size="9.00">owns</text>
</g>
<!-- Worker&#45;&gt;ReceiverCh -->
<g id="edge16" class="edge">
<title>Worker&#45;&gt;ReceiverCh</title>
<path fill="none" stroke="#333333" d="M2226.72,-539C2246.72,-539 2261.01,-539 2261.01,-539 2261.01,-539 2261.01,-433.86 2261.01,-433.86"/>
<polygon fill="#333333" stroke="#333333" points="2264.51,-433.86 2261.01,-423.86 2257.51,-433.86 2264.51,-433.86"/>
<text xml:space="preserve" text-anchor="middle" x="2433.39" y="-498.15" font-family="Helvetica,sans-Serif" font-size="9.00">transfer_rx +</text>
<text xml:space="preserve" text-anchor="middle" x="2433.39" y="-487.35" font-family="Helvetica,sans-Serif" font-size="9.00">spawn_rx</text>
</g>
<!-- Worker&#45;&gt;WorkerStats -->
<g id="edge22" class="edge">
<title>Worker&#45;&gt;WorkerStats</title>
<path fill="none" stroke="#e67300" stroke-width="2.5" d="M2227.04,-543C2402.39,-543 2701.65,-543 2701.65,-543 2701.65,-543 2701.65,-458.42 2701.65,-458.42"/>
<polygon fill="#e67300" stroke="#e67300" stroke-width="2.5" points="2705.15,-458.42 2701.65,-448.42 2698.15,-458.42 2705.15,-458.42"/>
<text xml:space="preserve" text-anchor="middle" x="2728.75" y="-492.75" font-family="Helvetica,sans-Serif" font-size="9.00">Arc</text>
</g>
<!-- WorkerContext -->
<g id="node20" class="node">
<title>WorkerContext</title>
<polygon fill="white" stroke="black" points="1979.05,-1054.15 1979.05,-1150.15 2170.95,-1150.15 2170.95,-1054.15 1979.05,-1054.15"/>
<polygon fill="#c0392b" stroke="none" points="1987.05,-1124.15 1987.05,-1146.15 2162.95,-1146.15 2162.95,-1124.15 1987.05,-1124.15"/>
<polygon fill="none" stroke="black" points="1987.05,-1124.15 1987.05,-1146.15 2162.95,-1146.15 2162.95,-1124.15 1987.05,-1124.15"/>
<text xml:space="preserve" text-anchor="start" x="2029.35" y="-1133.15" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">WorkerContext&lt;&#39;a&gt;</text>
<polygon fill="none" stroke="black" points="1987.05,-1102.15 1987.05,-1124.15 2162.95,-1124.15 2162.95,-1102.15 1987.05,-1102.15"/>
<text xml:space="preserve" text-anchor="start" x="1992.05" y="-1110.15" font-family="Helvetica,sans-Serif" font-size="10.00">worker_id: WorkerId</text>
<polygon fill="none" stroke="black" points="1987.05,-1080.15 1987.05,-1102.15 2162.95,-1102.15 2162.95,-1080.15 1987.05,-1080.15"/>
<text xml:space="preserve" text-anchor="start" x="1992.05" y="-1088.15" font-family="Helvetica,sans-Serif" font-size="10.00">tc: &amp;&#39;a TickContext&lt;&#39;a&gt;</text>
<polygon fill="none" stroke="black" points="1987.05,-1058.15 1987.05,-1080.15 2162.95,-1080.15 2162.95,-1058.15 1987.05,-1058.15"/>
<text xml:space="preserve" text-anchor="start" x="1992.05" y="-1066.15" font-family="Helvetica,sans-Serif" font-size="10.00">pending_local: &amp;&#39;a RefCell&lt;Vec&lt;...&gt;&gt;</text>
</g>
<!-- WorkerContext&#45;&gt;ContextInner -->
<g id="edge36" class="edge">
<title>WorkerContext&#45;&gt;ContextInner</title>
<path fill="none" stroke="#2e8b57" stroke-width="1.5" stroke-dasharray="1,5" d="M1978.68,-1062C1659.63,-1062 657.46,-1062 657.46,-1062 657.46,-1062 657.46,-658.51 657.46,-658.51"/>
<polygon fill="none" stroke="#2e8b57" stroke-width="1.5" points="660.96,-658.51 657.46,-648.51 653.96,-658.51 660.96,-658.51"/>
<text xml:space="preserve" text-anchor="middle" x="707.25" y="-867.65" font-family="Helvetica,sans-Serif" font-size="9.00">impl</text>
</g>
<!-- WorkerContext&#45;&gt;TickContext -->
<g id="edge32" class="edge">
<title>WorkerContext&#45;&gt;TickContext</title>
<path fill="none" stroke="#7b2d8b" stroke-width="1.5" stroke-dasharray="5,2" d="M1978.71,-1059C1915.32,-1059 1844,-1059 1844,-1059 1844,-1059 1844,-963.99 1844,-963.99"/>
<polygon fill="#7b2d8b" stroke="#7b2d8b" stroke-width="1.5" points="1847.5,-963.99 1844,-953.99 1840.5,-963.99 1847.5,-963.99"/>
<text xml:space="preserve" text-anchor="middle" x="1965.36" y="-1016.55" font-family="Helvetica,sans-Serif" font-size="9.00">&amp;&#39;a</text>
</g>
<!-- ActorSlot -->
<g id="node22" class="node">
<title>ActorSlot</title>
<polygon fill="white" stroke="black" points="1981.91,-193.35 1981.91,-267.35 2206.09,-267.35 2206.09,-193.35 1981.91,-193.35"/>
<polygon fill="#c0392b" stroke="none" points="1989.91,-241.35 1989.91,-263.35 2198.09,-263.35 2198.09,-241.35 1989.91,-241.35"/>
<polygon fill="none" stroke="black" points="1989.91,-241.35 1989.91,-263.35 2198.09,-263.35 2198.09,-241.35 1989.91,-241.35"/>
<text xml:space="preserve" text-anchor="start" x="2071.5" y="-250.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">ActorSlot</text>
<polygon fill="none" stroke="black" points="1989.91,-219.35 1989.91,-241.35 2198.09,-241.35 2198.09,-219.35 1989.91,-219.35"/>
<text xml:space="preserve" text-anchor="start" x="1994.91" y="-227.35" font-family="Helvetica,sans-Serif" font-size="10.00">mailbox: VecDeque&lt;Box&lt;dyn Any + Send&gt;&gt;</text>
<polygon fill="none" stroke="black" points="1989.91,-197.35 1989.91,-219.35 2198.09,-219.35 2198.09,-197.35 1989.91,-197.35"/>
<text xml:space="preserve" text-anchor="start" x="1994.91" y="-205.35" font-family="Helvetica,sans-Serif" font-size="10.00">actor: Box&lt;dyn AnyActor&gt;</text>
</g>
<!-- ActorPool&#45;&gt;ActorSlot -->
<g id="edge7" class="edge">
<title>ActorPool&#45;&gt;ActorSlot</title>
<path fill="none" stroke="#333333" d="M2098.69,-369.87C2098.69,-369.87 2098.69,-279.11 2098.69,-279.11"/>
<polygon fill="#333333" stroke="#333333" points="2102.19,-279.11 2098.69,-269.11 2095.19,-279.11 2102.19,-279.11"/>
<text xml:space="preserve" text-anchor="middle" x="2134.02" y="-299.55" font-family="Helvetica,sans-Serif" font-size="9.00">HashMap&lt;Addr, _&gt;</text>
</g>
<!-- ActorSlot&#45;&gt;AnyActor -->
<g id="edge8" class="edge">
<title>ActorSlot&#45;&gt;AnyActor</title>
<path fill="none" stroke="#333333" d="M1981.51,-198C1645.39,-198 671.76,-198 671.76,-198 671.76,-198 671.76,-106.52 671.76,-106.52"/>
<polygon fill="#333333" stroke="#333333" points="675.26,-106.52 671.76,-96.52 668.26,-106.52 675.26,-106.52"/>
<text xml:space="preserve" text-anchor="middle" x="675.27" y="-155.75" font-family="Helvetica,sans-Serif" font-size="9.00">Box&lt;dyn&gt;</text>
</g>
<!-- HybridChannel -->
<g id="node23" class="node">
<title>HybridChannel</title>
<polygon fill="white" stroke="black" points="2258.8,-193.35 2258.8,-267.35 2393.2,-267.35 2393.2,-193.35 2258.8,-193.35"/>
<polygon fill="#148f77" stroke="none" points="2266.8,-241.35 2266.8,-263.35 2385.2,-263.35 2385.2,-241.35 2266.8,-241.35"/>
<polygon fill="none" stroke="black" points="2266.8,-241.35 2266.8,-263.35 2385.2,-263.35 2385.2,-241.35 2266.8,-241.35"/>
<text xml:space="preserve" text-anchor="start" x="2281.55" y="-250.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">HybridChannel&lt;T&gt;</text>
<polygon fill="none" stroke="black" points="2266.8,-219.35 2266.8,-241.35 2385.2,-241.35 2385.2,-219.35 2266.8,-219.35"/>
<text xml:space="preserve" text-anchor="start" x="2271.8" y="-227.35" font-family="Helvetica,sans-Serif" font-size="10.00">ring: ArrayQueue&lt;T&gt;</text>
<polygon fill="none" stroke="black" points="2266.8,-197.35 2266.8,-219.35 2385.2,-219.35 2385.2,-197.35 2266.8,-197.35"/>
<text xml:space="preserve" text-anchor="start" x="2271.8" y="-205.35" font-family="Helvetica,sans-Serif" font-size="10.00">overflow: SegQueue&lt;T&gt;</text>
</g>
<!-- HybridChannel&#45;&gt;HybridChannel -->
<g id="edge17" class="edge">
<title>HybridChannel&#45;&gt;HybridChannel</title>
<path fill="none" stroke="#333333" d="M2303.6,-267.43C2303.6,-284.92 2303.6,-302 2303.6,-302 2303.6,-302 2254.91,-302 2254.91,-302 2254.91,-302 2254.91,-158 2254.91,-158 2254.91,-158 2326,-158 2326,-158 2326,-158 2326,-175.25 2326,-192.9"/>
</g>
<!-- SenderCh&#45;&gt;SenderT -->
<g id="edge37" class="edge">
<title>SenderCh&#45;&gt;SenderT</title>
<path fill="none" stroke="#2e8b57" stroke-width="1.5" stroke-dasharray="1,5" d="M2343.5,-579.5C2343.5,-546.75 2343.5,-495 2343.5,-495 2343.5,-495 1864.68,-495 1864.68,-495 1864.68,-495 1864.68,-437.27 1864.68,-437.27"/>
<polygon fill="none" stroke="#2e8b57" stroke-width="1.5" points="1868.18,-437.27 1864.68,-427.27 1861.18,-437.27 1868.18,-437.27"/>
<text xml:space="preserve" text-anchor="middle" x="2295.52" y="-498.15" font-family="Helvetica,sans-Serif" font-size="9.00">impl for</text>
<text xml:space="preserve" text-anchor="middle" x="2295.52" y="-487.35" font-family="Helvetica,sans-Serif" font-size="9.00">Sender&lt;M&gt;</text>
</g>
<!-- SenderCh&#45;&gt;HybridChannel -->
<g id="edge23" class="edge">
<title>SenderCh&#45;&gt;HybridChannel</title>
<path fill="none" stroke="#e67300" stroke-width="2.5" d="M2428.49,-579.32C2428.49,-493.8 2428.49,-230 2428.49,-230 2428.49,-230 2407.29,-230 2407.29,-230"/>
<polygon fill="#e67300" stroke="#e67300" stroke-width="2.5" points="2407.29,-226.5 2397.29,-230 2407.29,-233.5 2407.29,-226.5"/>
<text xml:space="preserve" text-anchor="middle" x="2485.75" y="-393.45" font-family="Helvetica,sans-Serif" font-size="9.00">Arc</text>
</g>
<!-- ReceiverCh&#45;&gt;HybridChannel -->
<g id="edge24" class="edge">
<title>ReceiverCh&#45;&gt;HybridChannel</title>
<path fill="none" stroke="#e67300" stroke-width="2.5" d="M2348.4,-369.87C2348.4,-369.87 2348.4,-281.38 2348.4,-281.38"/>
<polygon fill="#e67300" stroke="#e67300" stroke-width="2.5" points="2351.9,-281.38 2348.4,-271.38 2344.9,-281.38 2351.9,-281.38"/>
<text xml:space="preserve" text-anchor="middle" x="2337.75" y="-299.55" font-family="Helvetica,sans-Serif" font-size="9.00">Arc</text>
</g>
<!-- WorkerInfo -->
<g id="node27" class="node">
<title>WorkerInfo</title>
<polygon fill="white" stroke="black" points="2714.24,-811.35 2714.24,-929.35 2859.76,-929.35 2859.76,-811.35 2714.24,-811.35"/>
<polygon fill="#3949ab" stroke="none" points="2722.24,-903.35 2722.24,-925.35 2851.76,-925.35 2851.76,-903.35 2722.24,-903.35"/>
<polygon fill="none" stroke="black" points="2722.24,-903.35 2722.24,-925.35 2851.76,-925.35 2851.76,-903.35 2722.24,-903.35"/>
<text xml:space="preserve" text-anchor="start" x="2760.61" y="-912.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">WorkerInfo</text>
<polygon fill="none" stroke="black" points="2722.24,-881.35 2722.24,-903.35 2851.76,-903.35 2851.76,-881.35 2722.24,-881.35"/>
<text xml:space="preserve" text-anchor="start" x="2727.24" y="-889.35" font-family="Helvetica,sans-Serif" font-size="10.00">id: usize</text>
<polygon fill="none" stroke="black" points="2722.24,-859.35 2722.24,-881.35 2851.76,-881.35 2851.76,-859.35 2722.24,-859.35"/>
<text xml:space="preserve" text-anchor="start" x="2727.24" y="-867.35" font-family="Helvetica,sans-Serif" font-size="10.00">num_actors: usize</text>
<polygon fill="none" stroke="black" points="2722.24,-837.35 2722.24,-859.35 2851.76,-859.35 2851.76,-837.35 2722.24,-837.35"/>
<text xml:space="preserve" text-anchor="start" x="2727.24" y="-845.35" font-family="Helvetica,sans-Serif" font-size="10.00">mailbox_depth: usize</text>
<polygon fill="none" stroke="black" points="2722.24,-815.35 2722.24,-837.35 2851.76,-837.35 2851.76,-815.35 2722.24,-815.35"/>
<text xml:space="preserve" text-anchor="start" x="2727.24" y="-823.35" font-family="Helvetica,sans-Serif" font-size="10.00">messages_processed: u64</text>
</g>
<!-- RuntimeStats -->
<g id="node28" class="node">
<title>RuntimeStats</title>
<polygon fill="white" stroke="black" points="2695.36,-1054.15 2695.36,-1150.15 2878.64,-1150.15 2878.64,-1054.15 2695.36,-1054.15"/>
<polygon fill="#3949ab" stroke="none" points="2703.36,-1124.15 2703.36,-1146.15 2870.64,-1146.15 2870.64,-1124.15 2703.36,-1124.15"/>
<polygon fill="none" stroke="black" points="2703.36,-1124.15 2703.36,-1146.15 2870.64,-1146.15 2870.64,-1124.15 2703.36,-1124.15"/>
<text xml:space="preserve" text-anchor="start" x="2754.77" y="-1133.15" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">RuntimeStats</text>
<polygon fill="none" stroke="black" points="2703.36,-1102.15 2703.36,-1124.15 2870.64,-1124.15 2870.64,-1102.15 2703.36,-1102.15"/>
<text xml:space="preserve" text-anchor="start" x="2708.36" y="-1110.15" font-family="Helvetica,sans-Serif" font-size="10.00">num_workers: usize</text>
<polygon fill="none" stroke="black" points="2703.36,-1080.15 2703.36,-1102.15 2870.64,-1102.15 2870.64,-1080.15 2703.36,-1080.15"/>
<text xml:space="preserve" text-anchor="start" x="2708.36" y="-1088.15" font-family="Helvetica,sans-Serif" font-size="10.00">actors: Vec&lt;(ActorAddress, usize)&gt;</text>
<polygon fill="none" stroke="black" points="2703.36,-1058.15 2703.36,-1080.15 2870.64,-1080.15 2870.64,-1058.15 2703.36,-1058.15"/>
<text xml:space="preserve" text-anchor="start" x="2708.36" y="-1066.15" font-family="Helvetica,sans-Serif" font-size="10.00">workers: Vec&lt;WorkerInfo&gt;</text>
</g>
<!-- RuntimeStats&#45;&gt;WorkerInfo -->
<g id="edge4" class="edge">
<title>RuntimeStats&#45;&gt;WorkerInfo</title>
<path fill="none" stroke="#333333" d="M2787,-1053.84C2787,-1053.84 2787,-941.22 2787,-941.22"/>
<polygon fill="#333333" stroke="#333333" points="2790.5,-941.22 2787,-931.22 2783.5,-941.22 2790.5,-941.22"/>
<text xml:space="preserve" text-anchor="middle" x="2800.01" y="-1016.55" font-family="Helvetica,sans-Serif" font-size="9.00">Vec&lt;&gt;</text>
</g>
<!-- BackoffPolicy -->
<g id="node30" class="node">
<title>BackoffPolicy</title>
<polygon fill="white" stroke="black" points="2525.13,-337.15 2525.13,-455.15 2662.87,-455.15 2662.87,-337.15 2525.13,-337.15"/>
<polygon fill="#d4ac0d" stroke="none" points="2533.13,-429.15 2533.13,-451.15 2654.87,-451.15 2654.87,-429.15 2533.13,-429.15"/>
<polygon fill="none" stroke="black" points="2533.13,-429.15 2533.13,-451.15 2654.87,-451.15 2654.87,-429.15 2533.13,-429.15"/>
<text xml:space="preserve" text-anchor="start" x="2560.93" y="-438.15" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="10.00" fill="white">BackoffPolicy</text>
<polygon fill="none" stroke="black" points="2533.13,-407.15 2533.13,-429.15 2654.87,-429.15 2654.87,-407.15 2533.13,-407.15"/>
<text xml:space="preserve" text-anchor="start" x="2538.13" y="-415.15" font-family="Helvetica,sans-Serif" font-size="10.00">spin_threshold: u32</text>
<polygon fill="none" stroke="black" points="2533.13,-385.15 2533.13,-407.15 2654.87,-407.15 2654.87,-385.15 2533.13,-385.15"/>
<text xml:space="preserve" text-anchor="start" x="2538.13" y="-393.15" font-family="Helvetica,sans-Serif" font-size="10.00">yield_threshold: u32</text>
<polygon fill="none" stroke="black" points="2533.13,-363.15 2533.13,-385.15 2654.87,-385.15 2654.87,-363.15 2533.13,-363.15"/>
<text xml:space="preserve" text-anchor="start" x="2538.13" y="-371.15" font-family="Helvetica,sans-Serif" font-size="10.00">sleep_increment_us: u64</text>
<polygon fill="none" stroke="black" points="2533.13,-341.15 2533.13,-363.15 2654.87,-363.15 2654.87,-341.15 2533.13,-341.15"/>
<text xml:space="preserve" text-anchor="start" x="2538.13" y="-349.15" font-family="Helvetica,sans-Serif" font-size="10.00">sleep_max_us: u64</text>
</g>
<!-- RuntimeConfig&#45;&gt;BackoffPolicy -->
<g id="edge3" class="edge">
<title>RuntimeConfig&#45;&gt;BackoffPolicy</title>
<path fill="none" stroke="#333333" d="M2594,-546.52C2594,-546.52 2594,-467.1 2594,-467.1"/>
<polygon fill="#333333" stroke="#333333" points="2597.5,-467.1 2594,-457.1 2590.5,-467.1 2597.5,-467.1"/>
<text xml:space="preserve" text-anchor="middle" x="2604.51" y="-492.75" font-family="Helvetica,sans-Serif" font-size="9.00">owns</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 62 KiB

View file

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

1118
docs/architecture.svg Normal file

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 92 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

View file

@ -1,191 +1,53 @@
{
"graph": {
"n_nodes": 36,
"n_edges": 78,
"n_modules": 8,
"n_nodes": 39,
"n_edges": 97,
"n_modules": 9,
"connected_components": 2,
"modules": [
"actor",
"worker",
"channel",
"error",
"config",
"channel",
"actor",
"address_map",
"delivery",
"stats",
"runtime",
"worker",
"python"
"transport"
]
},
"spectral": {
"eigenvalues": [
0.0,
0.0,
0.18637427422819514,
0.4813940269111958,
0.6123548189907484,
0.7985629750697533,
0.8319091149970231,
1.004600219615323,
1.2394224070963267,
1.3689639255261323,
1.4526860286383532,
1.626080007307936,
2.321279039207482,
2.3935870074779477,
2.909249108581605,
3.1569529438124246,
3.219980753498557,
3.3901681819448264,
3.4799333923457128,
3.605153966968332,
3.8847634489335645,
4.186333826694949,
4.707024553452379,
5.173220891347629,
5.586454240023603,
5.795938099946378,
5.8549806331718415,
6.1828765255391644,
6.461944112192484,
6.898584006266002,
7.3807011063714905,
7.896480708195232,
9.160238969430825,
11.171010263156152,
14.04747425561517,
15.533322167445291
],
"fiedler_value": 0.0,
"fiedler_vector": [
0.0,
1.6667674979754847e-17,
-4.4166826078552935e-16,
-5.256955919501151e-16,
-1.6422080940489055e-18,
-7.037238109196825e-17,
8.390622125197347e-16,
2.3690827037115515e-17,
1.4176669953736474e-16,
1.4226827878099615e-16,
3.1675939003075104e-17,
2.7236604915425953e-18,
-1.744993274089968e-16,
2.918795638720409e-17,
-2.1047785816801073e-16,
1.6100142369066343e-16,
-1.1048855416219909e-16,
2.623380592723269e-16,
-6.257340472605819e-17,
-2.7901019807352287e-17,
7.954130131218555e-17,
-2.8145783605573126e-16,
5.097927800469914e-17,
1.0000000000000002,
-7.635525673846673e-17,
4.0203070989124624e-17,
5.607482503879278e-17,
1.4848475991077948e-17,
-8.451175680174382e-17,
-1.3333327282927672e-16,
2.6566833162138994e-16,
1.0987812721413363e-16,
4.959951093541129e-16,
-1.2067067461630528e-16,
-2.172546179303562e-16,
-2.3212297109883297e-16
],
"node_names": [
"Error",
"BackoffPolicy",
"RuntimeConfig",
"HybridChannel",
"Receiver",
"Sender",
"Actor",
"ActorAddress",
"ActorInterface",
"AnyActor",
"ContextInner",
"Ctx",
"Message",
"AddressMap",
"Placement",
"WorkerId",
"Envelope",
"Inbox",
"InboxRegistry",
"Runtime",
"RuntimeHandle",
"SenderT",
"ActorPool",
"Mailbox",
"TickContext",
"Worker",
"WorkerContext",
"Effect",
"PyActor",
"PyActorAddress",
"PyCtx",
"PyInbox",
"PyMsg",
"PyRuntime",
"PyRuntimeConfig",
"PyRuntimeHandle"
],
"node_modules": [
"error",
"config",
"config",
"channel",
"channel",
"channel",
"actor",
"actor",
"actor",
"actor",
"actor",
"actor",
"actor",
"address_map",
"address_map",
"address_map",
"runtime",
"runtime",
"runtime",
"runtime",
"runtime",
"runtime",
"worker",
"worker",
"worker",
"worker",
"worker",
"python",
"python",
"python",
"python",
"python",
"python",
"python",
"python",
"python"
]
"structural": {
"avg_degree": 2.4871794871794872,
"max_fan_in": {
"count": 16,
"node": "ActorAddress"
},
"max_fan_out": {
"count": 18,
"node": "Runtime"
},
"dag_depth": 7,
"clustering_coefficient": 0.26676384839650147,
"avg_module_size": 4.333333333333333
},
"module_coupling": {
"module_names": [
"actor",
"worker",
"channel",
"error",
"config",
"channel",
"actor",
"address_map",
"delivery",
"stats",
"runtime",
"worker",
"python"
"transport"
],
"coupling_matrix": [
[
9.0,
0.0,
0.0,
0.0,
2.0,
0.0,
0.0,
0.0,
@ -193,12 +55,13 @@
0.0
],
[
0.0,
9.0,
2.0,
1.0,
1.0,
0.0,
0.0,
0.0,
0.0,
5.0,
3.0,
0.0,
0.0
],
@ -210,71 +73,97 @@
0.0,
1.0,
0.0,
0.0,
0.0
],
[
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
],
[
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0
],
[
5.0,
0.0,
1.0,
2.0,
0.0,
0.0,
1.0,
7.0,
0.0,
0.0,
0.0,
0.0
],
[
0.0,
0.0,
0.0,
1.0,
2.0,
0.0,
0.0,
0.0
2.0
],
[
2.0,
1.0,
2.0,
6.0,
2.0,
6.0,
1.0,
0.0
],
[
1.0,
1.0,
2.0,
9.0,
4.0,
3.0,
3.0,
0.0
],
[
0.0,
0.0,
0.0,
0.0,
0.0,
5.0,
0.0,
0.0
],
[
4.0,
1.0,
2.0,
1.0,
1.0,
5.0,
2.0,
3.0,
2.0
],
[
3.0,
0.0,
10.0
2.0,
4.0,
0.0,
0.0,
0.0,
0.0,
5.0
]
],
"cross_module_edges": 46,
"total_edges": 78
"cross_module_edges": 62,
"total_edges": 97
},
"module_cohesion": {
"actor": 0.21428571428571427,
"worker": 0.16666666666666666,
"channel": 0.5,
"error": null,
"config": 0.5,
"delivery": 0.16666666666666666,
"stats": 0.25,
"runtime": 0.5,
"transport": 0.11904761904761904
},
"metrics": {
"algebraic_connectivity": 0.0,
"normalized_algebraic_connectivity": 0.0,
"spectral_entropy": 4.641128070102523,
"normalized_spectral_entropy": 0.9122677088609219,
"edge_density": 0.06190476190476191,
"cross_module_ratio": 0.5897435897435898,
"spectral_radius": 6.676215667817795,
"normalized_spectral_radius": 0.19074901908050843,
"cci": 0.383913712311739
"spectral_entropy": 4.728701071955628,
"edge_density": 0.06545209176788123,
"cross_module_ratio": 0.6391752577319587,
"spectral_radius": 7.96362061601628,
"avg_module_cohesion": 0.3020833333333333,
"cci": 0.39601706111504315
}
}

View file

@ -4,121 +4,62 @@
GRAPH SUMMARY
----------------------------------------
Nodes: 36
Directed edges: 78
Modules: 8
Nodes: 39
Directed edges: 97
Modules: 9
Connected components: 2
Modules: error, config, channel, actor, address_map, runtime, worker, python
Modules: actor, worker, channel, error, config, delivery, stats, runtime, transport
LAPLACIAN EIGENVALUE SPECTRUM
STRUCTURAL PROPERTIES
----------------------------------------
lambda_ 0 = 0.0000
lambda_ 1 = 0.0000 <-- Fiedler value (lambda_2)
lambda_ 2 = 0.1864
lambda_ 3 = 0.4814
lambda_ 4 = 0.6124
lambda_ 5 = 0.7986
lambda_ 6 = 0.8319
lambda_ 7 = 1.0046
lambda_ 8 = 1.2394
lambda_ 9 = 1.3690
lambda_10 = 1.4527
lambda_11 = 1.6261
lambda_12 = 2.3213
lambda_13 = 2.3936
lambda_14 = 2.9092
lambda_15 = 3.1570
lambda_16 = 3.2200
lambda_17 = 3.3902
lambda_18 = 3.4799
lambda_19 = 3.6052
lambda_20 = 3.8848
lambda_21 = 4.1863
lambda_22 = 4.7070
lambda_23 = 5.1732
lambda_24 = 5.5865
lambda_25 = 5.7959
lambda_26 = 5.8550
lambda_27 = 6.1829
lambda_28 = 6.4619
lambda_29 = 6.8986
lambda_30 = 7.3807
lambda_31 = 7.8965
lambda_32 = 9.1602
lambda_33 = 11.1710
lambda_34 = 14.0475
lambda_35 = 15.5333
Edges/node (avg degree): 2.49
Max fan-in: 16 (ActorAddress)
Max fan-out: 18 (Runtime)
DAG depth: 7
Clustering coefficient: 0.2668
Spectral gap (lambda_max - lambda_2): 15.5333
Fiedler value (algebraic connectivity): 0.0000
FIEDLER VECTOR — SPECTRAL BISECTION
MODULE COHESION
----------------------------------------
Partition A (Fiedler < 0):
HybridChannel [channel ] f = -0.0000
RuntimeConfig [config ] f = -0.0000
SenderT [runtime ] f = -0.0000
PyRuntimeHandle [python ] f = -0.0000
PyRuntimeConfig [python ] f = -0.0000
Placement [address_map ] f = -0.0000
Message [actor ] f = -0.0000
PyActorAddress [python ] f = -0.0000
PyRuntime [python ] f = -0.0000
Envelope [runtime ] f = -0.0000
PyActor [python ] f = -0.0000
TickContext [worker ] f = -0.0000
Sender [channel ] f = -0.0000
InboxRegistry [runtime ] f = -0.0000
Runtime [runtime ] f = -0.0000
Receiver [channel ] f = -0.0000
────────────────────────────────────
Partition B (Fiedler >= 0):
Error [error ] f = +0.0000
Ctx [actor ] f = +0.0000
Effect [python ] f = +0.0000
BackoffPolicy [config ] f = +0.0000
ActorAddress [actor ] f = +0.0000
AddressMap [address_map ] f = +0.0000
ContextInner [actor ] f = +0.0000
Worker [worker ] f = +0.0000
ActorPool [worker ] f = +0.0000
WorkerContext [worker ] f = +0.0000
RuntimeHandle [runtime ] f = +0.0000
PyInbox [python ] f = +0.0000
ActorInterface [actor ] f = +0.0000
AnyActor [actor ] f = +0.0000
WorkerId [address_map ] f = +0.0000
Inbox [runtime ] f = +0.0000
PyCtx [python ] f = +0.0000
PyMsg [python ] f = +0.0000
Actor [actor ] f = +0.0000
Mailbox [worker ] f = +1.0000
Module Size Cohesion
actor 7 0.214
worker 4 0.167
channel 3 0.500
error 1 —
config 2 0.500
delivery 7 0.167
stats 5 0.250
runtime 3 0.500
transport 7 0.119
────────────────────────────────
Average cohesion: 0.302
Avg module size: 4.3
MODULE COUPLING MATRIX (directed edge counts)
----------------------------------------
error config channel actoraddress_map runtime worker python
error 0 0 0 0 0 0 0 0
config 0 1 0 0 0 0 0 0
channel 0 0 3 0 0 1 0 0
actor 2 0 0 7 0 0 0 0
address_map 0 0 0 1 2 0 0 0
runtime 2 1 2 6 2 6 1 0
worker 1 1 2 9 4 3 3 0
python 0 0 0 5 0 3 0 10
actor worker channel error config delivery stats runtime transport
actor 9 0 0 2 0 0 0 0 0
worker 9 2 1 1 0 5 3 0 0
channel 0 0 3 0 0 1 0 0 0
error 0 0 0 0 0 0 0 0 0
config 0 0 0 0 1 0 0 0 0
delivery 5 0 1 2 1 7 0 0 2
stats 2 0 0 0 0 0 5 0 0
runtime 4 1 2 1 1 5 2 3 2
transport 3 0 2 4 0 0 0 0 5
Cross-module edges: 46 / 78 (59.0%)
Cross-module edges: 62 / 97 (63.9%)
CONNECTOME COMPLEXITY INDEX (CCI)
----------------------------------------
Sub-metric Raw Normalized Weight Contrib
──────────────────────────────────────── ────────── ────────── ──────── ────────
Algebraic connectivity (lambda_2/n) 0.0000 0.0000 0.25 0.0000
Spectral entropy (H/log2(k)) 4.6411 0.9123 0.25 0.2281
Edge density (|E|/n(n-1)) 0.0619 0.0619 0.15 0.0093
Cross-module coupling ratio 0.5897 0.5897 0.20 0.1179
Spectral radius (rho/(n-1)) 6.6762 0.1907 0.15 0.0286
Spectral entropy (H/log2(k)) 4.7287 0.9077 0.25 0.2269
Edge density (|E|/n(n-1)) 0.0655 0.0655 0.15 0.0098
Cross-module coupling ratio 0.6392 0.6392 0.20 0.1278
Spectral radius (rho/(n-1)) 7.9636 0.2096 0.15 0.0314
──────────────────────────────────────── ────────── ────────── ──────── ────────
CCI (weighted sum) 1.00 0.3839
CCI (weighted sum) 1.00 0.3960
Interpretation: MODERATE complexity — typical well-structured codebase

View file

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -2,26 +2,29 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
DOCS_DIR="$SCRIPT_DIR"
OUT_DIR="$DOCS_DIR"
TOOLS_DIR="$(cd "$SCRIPT_DIR/../../../tools" && pwd)"
TOOLS_DIR="$ROOT_DIR/tools"
# --- Phase 1: Generate architecture.dot from source AST ---
mkdir -p "$OUT_DIR"
echo "==> Generating architecture.dot from source..."
cargo run --manifest-path "$TOOLS_DIR/depgraph/Cargo.toml" \
-- --src-dir "$ROOT_DIR/src/" --output architecture --output-dir "$DOCS_DIR/"
# --- Phase 2: Render all .dot files to .svg ---
# Collect .dot sources
dots=("$DOCS_DIR"/*.dot)
if [ ${#dots[@]} -eq 0 ]; then
echo "No .dot files found in $DOCS_DIR"
exit 0
fi
# Pick a renderer: prefer graphviz `dot`, fall back to @viz-js/viz via Node
render_with_dot() {
for src in "${dots[@]}"; do
name="$(basename "$src" .dot)"
echo " dot: $name.dot -> generated/$name.svg"
dot -Tsvg "$src" -o "$OUT_DIR/$name.svg"
echo " dot: $name.dot -> $name.svg"
dot -Tsvg "$src" -o "$DOCS_DIR/$name.svg"
done
}
@ -39,7 +42,6 @@ const require = createRequire("$TOOLS_DIR/package.json");
const { instance } = require("@viz-js/viz");
const docsDir = "$DOCS_DIR";
const outDir = "$OUT_DIR";
const viz = await instance();
const dots = readdirSync(docsDir).filter(f => f.endsWith(".dot"));
@ -48,20 +50,19 @@ for (const file of dots) {
const src = readFileSync(join(docsDir, file), "utf-8");
const name = basename(file, ".dot");
const svg = viz.renderString(src, { format: "svg" });
writeFileSync(join(outDir, \`\${name}.svg\`), svg);
console.log(\` viz-js: \${file} -> generated/\${name}.svg\`);
writeFileSync(join(docsDir, \`\${name}.svg\`), svg);
console.log(\` viz-js: \${file} -> \${name}.svg\`);
}
NODEJS
node "$tmpfile"
}
echo "Rendering DOT diagrams..."
echo "==> Rendering DOT -> SVG..."
if command -v dot &>/dev/null; then
render_with_dot
elif command -v node &>/dev/null; then
# Ensure @viz-js/viz is available
if [ -f "$TOOLS_DIR/package.json" ]; then
if ! [ -d "$TOOLS_DIR/node_modules/@viz-js/viz" ]; then
echo "Installing @viz-js/viz..."
@ -78,4 +79,4 @@ else
exit 1
fi
echo "Done. Output in ${OUT_DIR}"
echo "==> Done. SVGs in $DOCS_DIR/"

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

118
docs/transport.md Normal file
View file

@ -0,0 +1,118 @@
# Transport-Agnostic Messaging
Enables actors on different runtimes to communicate transparently via
pluggable codecs (serialization) and transports (delivery protocol).
**Feature-gated:** `#[cfg(feature = "transport")]`. Without the flag, the
binary is identical to the baseline runtime.
```bash
cargo build --features transport
cargo test --features transport
```
## Two Layers of Pluggability
- **`Codec<M>`** — HOW bytes are encoded. gRPC/protobuf, bincode, custom, etc.
No serde bounds — the codec defines what it needs from `M`.
- **`Transport`** — WHERE bytes are sent. InMemory (testing), TCP, gRPC, etc.
See [transport_routing.svg](../crates/runtime-dashboard/docs/transport_routing.svg)
for the extended routing chain, and
[transport_encode_decode.svg](../crates/runtime-dashboard/docs/transport_encode_decode.svg)
for the encode/decode data flow.
## Core Types
| Type | Role |
|------|------|
| `Codec<M>` | User-implemented encode/decode for a message type |
| `NetworkMessage` | Marker trait: adds `fn type_tag() -> &'static str` for wire routing |
| `WireEnvelope` | `{ dest: ActorAddress, type_tag: String, payload: Vec<u8> }` |
| `Transport` | `fn send(WireEnvelope) -> Result<()>` — pluggable delivery |
| `CodecRegistry` | Maps `TypeId → encoder` (send side) and `type_tag → decoder` (receive side) |
| `TransportRouter` | Maps `ActorAddress → Arc<dyn Transport>` for remote addresses |
| `TransportBridge` | Deserializes incoming `WireEnvelope` → `(ActorAddress, Box<dyn Any + Send>)` |
| `InMemoryTransport` | mpsc-backed transport for testing |
## Routing Chain
Without transport, unresolved addresses fall through to `InboxRegistry`.
With transport enabled, a third step is inserted:
1. **AddressMap::lookup** → local worker delivery (zero-copy, no serialize)
2. **InboxRegistry::contains** → external inbox delivery
3. **TransportRouter::lookup** → codec.encode + transport.send (remote)
4. **Fallback** → `InboxRegistry::try_deliver` (Err if not found)
This chain runs in `Runtime::send_to`, `ContextInner for Runtime`, and
`WorkerContext::send_any` — all three follow the same logic.
## Type Erasure Bridge
Messages are `Box<dyn Any + Send>` before routing, but `dyn Any` can't be
serialized. The bridge:
**Send:** `Box<dyn Any>` → `(*msg).type_id()` → `encoders[TypeId]` →
downcast to `M` → `Codec<M>::encode` → `(type_tag, Vec<u8>)` → `WireEnvelope`
**Receive:** `WireEnvelope` → `decoders[type_tag]` → `Codec<M>::decode` →
`Box::new(msg) as Box<dyn Any + Send>` → `runtime.deliver_raw(addr, msg)`
`TypeId` (compiler-assigned, process-local) is used for encoding.
`type_tag` (user-defined, stable) is used on the wire for decoding.
## Setup
```rust
// 1. Register codecs
let mut codecs = CodecRegistry::new();
codecs.register::<Ping, _>(MyCodec);
codecs.register::<Pong, _>(MyCodec);
// 2. Create transport + router
let (transport, rx) = InMemoryTransport::pair();
let router = TransportRouter::new();
router.add_route(remote_addr, transport);
// 3. Attach to runtime
let mut rt = Runtime::new(RuntimeConfig::default());
rt.set_codec_registry(Arc::new(codecs));
rt.set_transport_router(Arc::new(router));
// 4. Send transparently
rt.send_to(remote_addr, Ping { value: 42, reply_to: inbox_addr }).unwrap();
// 5. Receive side: bridge deserializes, deliver_raw injects
let bridge = TransportBridge::new(codecs_arc);
let (addr, msg) = bridge.receive(wire_envelope).unwrap();
rt.deliver_raw(addr, msg).unwrap();
```
## Address Resolution
Addresses are **not automatically discovered**. Each runtime must be told
which remote addresses exist via `router.add_route()`. Since addresses are
32 random bytes, runtimes must exchange them out-of-band (e.g., over the TCP
connection itself — see `examples/tcp_ping_pong.rs`).
## Limitations
- **No automatic discovery** — manual address exchange required
- **Transport::send is synchronous** — blocking transports stall the worker
- **One route per address** — no wildcard/prefix routing
- **No ordering guarantees** across transports (depends on transport impl)
- **No back-pressure** from remote — fire-and-forget delivery
- **TypeId is not stable** across compilations (only used process-locally; wire uses type_tag)
## Where Things Live
| Concept | File |
|---------|------|
| All transport types | `src/transport.rs` |
| `InboxRegistry::contains()` | `src/delivery.rs` |
| Transport fields on `TickContext` | `src/delivery.rs` |
| `Runtime::deliver_raw`, setters | `src/runtime.rs` |
| Transport fallback in worker | `src/worker.rs` |
| Integration tests | `tests/transport_api.rs` |
| TCP example | `examples/tcp_ping_pong.rs` |

View file

@ -0,0 +1,183 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 920" font-family="system-ui, sans-serif">
<defs>
<filter id="shadow" x="-4%" y="-4%" width="108%" height="108%">
<feDropShadow dx="1" dy="2" stdDeviation="3" flood-color="#000" flood-opacity="0.10"/>
</filter>
<marker id="arrow-gray" viewBox="0 0 10 7" refX="10" refY="3.5"
markerWidth="10" markerHeight="7" orient="auto-start-reverse" fill="#5f6368">
<polygon points="0 0, 10 3.5, 0 7"/>
</marker>
<marker id="arrow-green" viewBox="0 0 10 7" refX="10" refY="3.5"
markerWidth="10" markerHeight="7" orient="auto-start-reverse" fill="#34a853">
<polygon points="0 0, 10 3.5, 0 7"/>
</marker>
<marker id="arrow-purple" viewBox="0 0 10 7" refX="10" refY="3.5"
markerWidth="10" markerHeight="7" orient="auto-start-reverse" fill="#9334e6">
<polygon points="0 0, 10 3.5, 0 7"/>
</marker>
<marker id="arrow-blue" viewBox="0 0 10 7" refX="10" refY="3.5"
markerWidth="10" markerHeight="7" orient="auto-start-reverse" fill="#4285f4">
<polygon points="0 0, 10 3.5, 0 7"/>
</marker>
</defs>
<!-- Background -->
<rect width="1100" height="920" rx="8" fill="#f8f9fb" stroke="#e0e3e8" stroke-width="1.5"/>
<!-- Title -->
<text x="550" y="40" text-anchor="middle" font-size="20" font-weight="bold" fill="#202124">Transport Encode / Decode Flow</text>
<text x="550" y="60" text-anchor="middle" font-size="12" fill="#5f6368">src/transport.rs · CodecRegistry · TransportBridge</text>
<!-- ============================================ -->
<!-- ENCODE SIDE (Runtime A) -->
<!-- ============================================ -->
<rect x="30" y="80" width="500" height="760" rx="6" fill="#e8f0fe" fill-opacity="0.15" stroke="#4285f4" stroke-width="1.5" stroke-opacity="0.5"/>
<text x="280" y="102" text-anchor="middle" font-size="15" font-weight="700" fill="#4285f4">SENDING RUNTIME</text>
<text x="280" y="118" text-anchor="middle" font-size="11" fill="#5f6368">Encode Path</text>
<!-- Step: ctx.send -->
<rect x="140" y="138" width="280" height="46" rx="12" fill="#e8f0fe" stroke="#4285f4" stroke-width="1.8" filter="url(#shadow)"/>
<text x="280" y="158" text-anchor="middle" font-size="13" font-weight="600" fill="#1a73e8">ctx.send(addr, msg)</text>
<text x="280" y="174" text-anchor="middle" font-size="10" fill="#5f6368">typed message enters send path</text>
<line x1="280" y1="184" x2="280" y2="218" stroke="#5f6368" stroke-width="1.8" marker-end="url(#arrow-gray)"/>
<!-- Step: type erasure -->
<rect x="130" y="222" width="300" height="44" rx="12" fill="#f1f3f4" stroke="#9aa0a6" stroke-width="1.5" filter="url(#shadow)"/>
<text x="280" y="241" text-anchor="middle" font-size="12" font-weight="600" fill="#5f6368">Box::new(msg) as Box&lt;dyn Any + Send&gt;</text>
<text x="280" y="257" text-anchor="middle" font-size="10" fill="#9aa0a6">type erased — concrete type lost</text>
<line x1="280" y1="266" x2="280" y2="300" stroke="#5f6368" stroke-width="1.8" marker-end="url(#arrow-gray)"/>
<!-- Step: routing reaches transport -->
<rect x="100" y="304" width="360" height="40" rx="10" fill="none" stroke="#5f6368" stroke-width="1" stroke-dasharray="4,3"/>
<text x="280" y="320" text-anchor="middle" font-size="11" fill="#5f6368">routing: AddressMap miss → Inbox miss → Transport</text>
<text x="280" y="336" text-anchor="middle" font-size="10" fill="#9aa0a6">see transport_routing.svg</text>
<line x1="280" y1="344" x2="280" y2="380" stroke="#9334e6" stroke-width="1.8" marker-end="url(#arrow-purple)"/>
<!-- Step: TypeId lookup -->
<rect x="115" y="384" width="330" height="50" rx="12" fill="#f3e8fd" stroke="#9334e6" stroke-width="1.8" filter="url(#shadow)"/>
<text x="280" y="405" text-anchor="middle" font-size="12" font-weight="600" fill="#7627bb">(*msg).type_id() → TypeId</text>
<text x="280" y="423" text-anchor="middle" font-size="10" fill="#5f6368">encoders.get(&amp;type_id) → Arc&lt;ErasedCodec&gt;</text>
<line x1="280" y1="434" x2="280" y2="468" stroke="#9334e6" stroke-width="1.8" marker-end="url(#arrow-purple)"/>
<!-- Step: downcast + encode -->
<rect x="95" y="472" width="370" height="68" rx="12" fill="#f3e8fd" stroke="#9334e6" stroke-width="1.8" filter="url(#shadow)"/>
<text x="280" y="492" text-anchor="middle" font-size="13" font-weight="700" fill="#7627bb">TypedCodec&lt;M, C&gt;</text>
<text x="280" y="510" text-anchor="middle" font-size="11" fill="#5f6368">msg.downcast::&lt;M&gt;() — recover concrete type</text>
<text x="280" y="526" text-anchor="middle" font-size="11" fill="#5f6368">codec.encode(&amp;typed_msg) → Vec&lt;u8&gt;</text>
<line x1="280" y1="540" x2="280" y2="578" stroke="#9334e6" stroke-width="1.8" marker-end="url(#arrow-purple)"/>
<!-- Step: WireEnvelope -->
<rect x="110" y="582" width="340" height="58" rx="12" fill="#f3e8fd" stroke="#9334e6" stroke-width="2" filter="url(#shadow)"/>
<text x="280" y="604" text-anchor="middle" font-size="14" font-weight="700" fill="#7627bb">WireEnvelope</text>
<text x="280" y="622" text-anchor="middle" font-size="10" fill="#5f6368">{ dest: addr, type_tag: "app::Ping", payload }</text>
<text x="280" y="636" text-anchor="middle" font-size="10" fill="#9aa0a6">type_tag from M::type_tag() — stable string</text>
<line x1="280" y1="640" x2="280" y2="680" stroke="#9334e6" stroke-width="1.8" marker-end="url(#arrow-purple)"/>
<!-- Step: transport.send -->
<rect x="140" y="684" width="280" height="44" rx="12" fill="#f3e8fd" stroke="#9334e6" stroke-width="1.8" filter="url(#shadow)"/>
<text x="280" y="703" text-anchor="middle" font-size="12" font-weight="600" fill="#7627bb">transport.send(wire_envelope)</text>
<text x="280" y="719" text-anchor="middle" font-size="10" fill="#5f6368">TCP, gRPC, InMemory, etc.</text>
<!-- Key annotations -->
<rect x="60" y="760" width="410" height="60" rx="8" fill="none" stroke="#9aa0a6" stroke-width="1" stroke-dasharray="4,3"/>
<text x="265" y="778" text-anchor="middle" font-size="11" font-weight="600" fill="#5f6368">Encode key: TypeId (Rust compiler-assigned)</text>
<text x="265" y="795" text-anchor="middle" font-size="10" fill="#9aa0a6">Fast lookup. NOT stable across compilations.</text>
<text x="265" y="810" text-anchor="middle" font-size="10" fill="#9aa0a6">Works because encoder runs in the same process as the sender.</text>
<!-- ============================================ -->
<!-- WIRE BOUNDARY -->
<!-- ============================================ -->
<line x1="540" y1="130" x2="540" y2="790" stroke="#9334e6" stroke-width="2.5" stroke-dasharray="8,6"/>
<rect x="519" y="420" width="42" height="80" rx="6" fill="#f3e8fd" stroke="#9334e6" stroke-width="1.5"/>
<text x="540" y="450" text-anchor="middle" font-size="11" font-weight="700" fill="#9334e6" transform="rotate(90, 540, 460)">WIRE</text>
<!-- Arrow across wire -->
<path d="M 450 706 Q 540 706 575 706" fill="none" stroke="#9334e6" stroke-width="2.5" marker-end="url(#arrow-purple)"/>
<!-- ============================================ -->
<!-- DECODE SIDE (Runtime B) -->
<!-- ============================================ -->
<rect x="570" y="80" width="500" height="760" rx="6" fill="#e6f4ea" fill-opacity="0.15" stroke="#34a853" stroke-width="1.5" stroke-opacity="0.5"/>
<text x="820" y="102" text-anchor="middle" font-size="15" font-weight="700" fill="#34a853">RECEIVING RUNTIME</text>
<text x="820" y="118" text-anchor="middle" font-size="11" fill="#5f6368">Decode Path</text>
<!-- Step: receive wire envelope -->
<rect x="680" y="138" width="280" height="46" rx="12" fill="#e6f4ea" stroke="#34a853" stroke-width="1.8" filter="url(#shadow)"/>
<text x="820" y="158" text-anchor="middle" font-size="13" font-weight="600" fill="#137333">receive WireEnvelope</text>
<text x="820" y="174" text-anchor="middle" font-size="10" fill="#5f6368">from TCP, gRPC, InMemory, etc.</text>
<line x1="820" y1="184" x2="820" y2="218" stroke="#5f6368" stroke-width="1.8" marker-end="url(#arrow-gray)"/>
<!-- Step: TransportBridge -->
<rect x="660" y="222" width="320" height="48" rx="12" fill="#e6f4ea" stroke="#34a853" stroke-width="1.8" filter="url(#shadow)"/>
<text x="820" y="243" text-anchor="middle" font-size="13" font-weight="600" fill="#137333">TransportBridge::receive(envelope)</text>
<text x="820" y="259" text-anchor="middle" font-size="10" fill="#5f6368">deserializes wire bytes back to Box&lt;dyn Any&gt;</text>
<line x1="820" y1="270" x2="820" y2="308" stroke="#34a853" stroke-width="1.8" marker-end="url(#arrow-green)"/>
<!-- Step: type_tag lookup -->
<rect x="655" y="312" width="330" height="50" rx="12" fill="#e6f4ea" stroke="#34a853" stroke-width="1.8" filter="url(#shadow)"/>
<text x="820" y="333" text-anchor="middle" font-size="12" font-weight="600" fill="#137333">decoders.get(type_tag) → DecodeFn</text>
<text x="820" y="350" text-anchor="middle" font-size="10" fill="#5f6368">lookup by "app::Ping" → closure</text>
<line x1="820" y1="362" x2="820" y2="400" stroke="#34a853" stroke-width="1.8" marker-end="url(#arrow-green)"/>
<!-- Step: codec.decode -->
<rect x="635" y="404" width="370" height="68" rx="12" fill="#e6f4ea" stroke="#34a853" stroke-width="1.8" filter="url(#shadow)"/>
<text x="820" y="424" text-anchor="middle" font-size="13" font-weight="700" fill="#137333">DecodeFn (closure)</text>
<text x="820" y="442" text-anchor="middle" font-size="11" fill="#5f6368">codec.decode(bytes) → Ping { value: 42, ... }</text>
<text x="820" y="458" text-anchor="middle" font-size="11" fill="#5f6368">Box::new(msg) as Box&lt;dyn Any + Send&gt;</text>
<line x1="820" y1="472" x2="820" y2="510" stroke="#34a853" stroke-width="1.8" marker-end="url(#arrow-green)"/>
<!-- Step: deliver_raw -->
<rect x="680" y="514" width="280" height="46" rx="12" fill="#e6f4ea" stroke="#34a853" stroke-width="1.8" filter="url(#shadow)"/>
<text x="820" y="534" text-anchor="middle" font-size="12" font-weight="600" fill="#137333">runtime.deliver_raw(addr, msg)</text>
<text x="820" y="550" text-anchor="middle" font-size="10" fill="#5f6368">→ transfer queue → worker</text>
<line x1="820" y1="560" x2="820" y2="598" stroke="#4285f4" stroke-width="1.8" marker-end="url(#arrow-blue)"/>
<!-- Step: normal tick processing -->
<rect x="660" y="602" width="320" height="48" rx="12" fill="#e8f0fe" stroke="#4285f4" stroke-width="1.8" filter="url(#shadow)"/>
<text x="820" y="622" text-anchor="middle" font-size="12" font-weight="600" fill="#1a73e8">tick → mailbox → handle_any()</text>
<text x="820" y="638" text-anchor="middle" font-size="10" fill="#5f6368">downcast Box&lt;dyn Any&gt; → Ping</text>
<line x1="820" y1="650" x2="820" y2="688" stroke="#4285f4" stroke-width="1.8" marker-end="url(#arrow-blue)"/>
<!-- Step: user handler -->
<rect x="700" y="692" width="240" height="44" rx="12" fill="#e8f0fe" stroke="#4285f4" stroke-width="1.8" filter="url(#shadow)"/>
<text x="820" y="711" text-anchor="middle" font-size="13" font-weight="600" fill="#1a73e8">PongActor::handle(Ping)</text>
<text x="820" y="727" text-anchor="middle" font-size="10" fill="#5f6368">user code runs</text>
<!-- Key annotations -->
<rect x="610" y="760" width="430" height="60" rx="8" fill="none" stroke="#9aa0a6" stroke-width="1" stroke-dasharray="4,3"/>
<text x="825" y="778" text-anchor="middle" font-size="11" font-weight="600" fill="#5f6368">Decode key: type_tag string (user-defined)</text>
<text x="825" y="795" text-anchor="middle" font-size="10" fill="#9aa0a6">Stable across compilations, processes, and machines.</text>
<text x="825" y="810" text-anchor="middle" font-size="10" fill="#9aa0a6">Carried on the wire inside WireEnvelope.</text>
<!-- ============================================ -->
<!-- Legend -->
<!-- ============================================ -->
<rect x="50" y="858" width="1000" height="48" rx="8" fill="none" stroke="#e0e3e8" stroke-width="1"/>
<text x="70" y="882" font-size="12" font-weight="600" fill="#202124">Legend</text>
<line x1="140" y1="880" x2="175" y2="880" stroke="#9334e6" stroke-width="1.8" marker-end="url(#arrow-purple)"/>
<text x="185" y="884" font-size="11" fill="#9334e6">Encode path</text>
<line x1="300" y1="880" x2="335" y2="880" stroke="#34a853" stroke-width="1.8" marker-end="url(#arrow-green)"/>
<text x="345" y="884" font-size="11" fill="#34a853">Decode path</text>
<line x1="460" y1="880" x2="495" y2="880" stroke="#4285f4" stroke-width="1.8" marker-end="url(#arrow-blue)"/>
<text x="505" y="884" font-size="11" fill="#4285f4">Local delivery</text>
<line x1="630" y1="880" x2="665" y2="880" stroke="#9334e6" stroke-width="2.5" stroke-dasharray="8,6"/>
<text x="675" y="884" font-size="11" fill="#9334e6">Wire boundary</text>
<text x="850" y="884" font-size="10" fill="#9aa0a6">transport.rs · runtime.rs · worker.rs</text>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

173
docs/transport_routing.svg Normal file
View file

@ -0,0 +1,173 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 880" font-family="system-ui, sans-serif">
<defs>
<filter id="shadow" x="-4%" y="-4%" width="108%" height="108%">
<feDropShadow dx="1" dy="2" stdDeviation="3" flood-color="#000" flood-opacity="0.10"/>
</filter>
<marker id="arrow-gray" viewBox="0 0 10 7" refX="10" refY="3.5"
markerWidth="10" markerHeight="7" orient="auto-start-reverse" fill="#5f6368">
<polygon points="0 0, 10 3.5, 0 7"/>
</marker>
<marker id="arrow-green" viewBox="0 0 10 7" refX="10" refY="3.5"
markerWidth="10" markerHeight="7" orient="auto-start-reverse" fill="#34a853">
<polygon points="0 0, 10 3.5, 0 7"/>
</marker>
<marker id="arrow-blue" viewBox="0 0 10 7" refX="10" refY="3.5"
markerWidth="10" markerHeight="7" orient="auto-start-reverse" fill="#4285f4">
<polygon points="0 0, 10 3.5, 0 7"/>
</marker>
<marker id="arrow-purple" viewBox="0 0 10 7" refX="10" refY="3.5"
markerWidth="10" markerHeight="7" orient="auto-start-reverse" fill="#9334e6">
<polygon points="0 0, 10 3.5, 0 7"/>
</marker>
<marker id="arrow-red" viewBox="0 0 10 7" refX="10" refY="3.5"
markerWidth="10" markerHeight="7" orient="auto-start-reverse" fill="#ea4335">
<polygon points="0 0, 10 3.5, 0 7"/>
</marker>
</defs>
<!-- Background -->
<rect width="1100" height="880" rx="8" fill="#f8f9fb" stroke="#e0e3e8" stroke-width="1.5"/>
<!-- Title -->
<text x="550" y="40" text-anchor="middle" font-size="20" font-weight="bold" fill="#202124">Transport Routing Chain</text>
<text x="550" y="60" text-anchor="middle" font-size="12" fill="#5f6368">src/runtime.rs · src/worker.rs · src/transport.rs</text>
<!-- ============================================ -->
<!-- ORIGIN -->
<!-- ============================================ -->
<rect x="30" y="80" width="1040" height="100" rx="6" fill="#e8f0fe" fill-opacity="0.25" stroke="#4285f4" stroke-width="1" stroke-opacity="0.4"/>
<text x="50" y="100" font-size="13" font-weight="700" fill="#4285f4" letter-spacing="1">ORIGIN</text>
<rect x="200" y="110" width="220" height="48" rx="12" fill="#e8f0fe" stroke="#4285f4" stroke-width="1.8" filter="url(#shadow)"/>
<text x="310" y="130" text-anchor="middle" font-size="13" font-weight="600" fill="#1a73e8">runtime.send_to(addr, msg)</text>
<text x="310" y="147" text-anchor="middle" font-size="10" fill="#5f6368">or ctx.send(addr, msg)</text>
<!-- Box<dyn Any + Send> annotation -->
<rect x="640" y="110" width="230" height="48" rx="12" fill="#f1f3f4" stroke="#9aa0a6" stroke-width="1.2" filter="url(#shadow)"/>
<text x="755" y="132" text-anchor="middle" font-size="11" font-weight="600" fill="#5f6368">msg type-erased to</text>
<text x="755" y="148" text-anchor="middle" font-size="12" font-weight="600" fill="#5f6368" font-style="italic">Box&lt;dyn Any + Send&gt;</text>
<line x1="420" y1="134" x2="630" y2="134" stroke="#9aa0a6" stroke-width="1.2" stroke-dasharray="4,3"/>
<!-- Arrow down from origin -->
<line x1="310" y1="158" x2="310" y2="206" stroke="#5f6368" stroke-width="1.8" marker-end="url(#arrow-gray)"/>
<!-- ============================================ -->
<!-- STEP 1: AddressMap -->
<!-- ============================================ -->
<rect x="30" y="195" width="1040" height="130" rx="6" fill="#e6f4ea" fill-opacity="0.2" stroke="#34a853" stroke-width="1" stroke-opacity="0.4"/>
<text x="50" y="215" font-size="13" font-weight="700" fill="#34a853" letter-spacing="1">STEP 1</text>
<!-- Decision diamond -->
<polygon points="310,218 415,260 310,302 205,260" fill="#e6f4ea" stroke="#34a853" stroke-width="1.8" filter="url(#shadow)"/>
<text x="310" y="256" text-anchor="middle" font-size="11" font-weight="600" fill="#137333">AddressMap</text>
<text x="310" y="270" text-anchor="middle" font-size="11" font-weight="600" fill="#137333">::lookup(addr)</text>
<!-- Found → local delivery -->
<line x1="415" y1="260" x2="590" y2="260" stroke="#34a853" stroke-width="1.8" marker-end="url(#arrow-green)"/>
<text x="500" y="252" text-anchor="middle" font-size="11" fill="#34a853" font-weight="600">found</text>
<rect x="600" y="237" width="250" height="46" rx="12" fill="#e6f4ea" stroke="#34a853" stroke-width="1.8" filter="url(#shadow)"/>
<text x="725" y="257" text-anchor="middle" font-size="12" font-weight="600" fill="#137333">Local Worker Delivery</text>
<text x="725" y="273" text-anchor="middle" font-size="10" fill="#5f6368">transfer_txs[wid] — zero-copy, no serialize</text>
<!-- Not found → continue -->
<line x1="310" y1="302" x2="310" y2="348" stroke="#5f6368" stroke-width="1.8" marker-end="url(#arrow-gray)"/>
<text x="322" y="328" font-size="11" fill="#5f6368" font-weight="600">not found</text>
<!-- ============================================ -->
<!-- STEP 2: InboxRegistry -->
<!-- ============================================ -->
<rect x="30" y="340" width="1040" height="130" rx="6" fill="#fef7e0" fill-opacity="0.2" stroke="#f9ab00" stroke-width="1" stroke-opacity="0.4"/>
<text x="50" y="360" font-size="13" font-weight="700" fill="#e37400" letter-spacing="1">STEP 2</text>
<!-- Decision diamond -->
<polygon points="310,363 415,405 310,447 205,405" fill="#fef7e0" stroke="#f9ab00" stroke-width="1.8" filter="url(#shadow)"/>
<text x="310" y="401" text-anchor="middle" font-size="11" font-weight="600" fill="#e37400">InboxRegistry</text>
<text x="310" y="415" text-anchor="middle" font-size="11" font-weight="600" fill="#e37400">::contains(addr)</text>
<!-- Found → inbox delivery -->
<line x1="415" y1="405" x2="590" y2="405" stroke="#34a853" stroke-width="1.8" marker-end="url(#arrow-green)"/>
<text x="500" y="397" text-anchor="middle" font-size="11" fill="#34a853" font-weight="600">found</text>
<rect x="600" y="382" width="250" height="46" rx="12" fill="#fef7e0" stroke="#f9ab00" stroke-width="1.8" filter="url(#shadow)"/>
<text x="725" y="402" text-anchor="middle" font-size="12" font-weight="600" fill="#e37400">External Inbox Delivery</text>
<text x="725" y="418" text-anchor="middle" font-size="10" fill="#5f6368">inbox_registry.try_deliver(addr, msg)</text>
<!-- Not found → continue -->
<line x1="310" y1="447" x2="310" y2="493" stroke="#5f6368" stroke-width="1.8" marker-end="url(#arrow-gray)"/>
<text x="322" y="473" font-size="11" fill="#5f6368" font-weight="600">not found</text>
<!-- ============================================ -->
<!-- STEP 3: Transport (NEW) -->
<!-- ============================================ -->
<rect x="30" y="485" width="1040" height="190" rx="6" fill="#f3e8fd" fill-opacity="0.25" stroke="#9334e6" stroke-width="1" stroke-opacity="0.4"/>
<text x="50" y="505" font-size="13" font-weight="700" fill="#9334e6" letter-spacing="1">STEP 3 — TRANSPORT (feature-gated)</text>
<!-- Decision diamond -->
<polygon points="310,518 415,560 310,602 205,560" fill="#f3e8fd" stroke="#9334e6" stroke-width="1.8" filter="url(#shadow)"/>
<text x="310" y="553" text-anchor="middle" font-size="11" font-weight="600" fill="#7627bb">TransportRouter</text>
<text x="310" y="567" text-anchor="middle" font-size="11" font-weight="600" fill="#7627bb">::lookup(addr)</text>
<!-- Found → encode + send -->
<line x1="415" y1="560" x2="500" y2="560" stroke="#9334e6" stroke-width="1.8" marker-end="url(#arrow-purple)"/>
<text x="455" y="552" text-anchor="middle" font-size="11" fill="#9334e6" font-weight="600">found</text>
<!-- Encode box -->
<rect x="510" y="525" width="210" height="44" rx="12" fill="#f3e8fd" stroke="#9334e6" stroke-width="1.8" filter="url(#shadow)"/>
<text x="615" y="544" text-anchor="middle" font-size="12" font-weight="600" fill="#7627bb">CodecRegistry::encode()</text>
<text x="615" y="560" text-anchor="middle" font-size="10" fill="#5f6368">TypeId → type_tag + Vec&lt;u8&gt;</text>
<!-- Arrow to transport.send -->
<line x1="720" y1="547" x2="780" y2="547" stroke="#9334e6" stroke-width="1.8" marker-end="url(#arrow-purple)"/>
<!-- Transport send box -->
<rect x="790" y="520" width="215" height="55" rx="12" fill="#f3e8fd" stroke="#9334e6" stroke-width="1.8" filter="url(#shadow)"/>
<text x="897" y="540" text-anchor="middle" font-size="12" font-weight="600" fill="#7627bb">transport.send(</text>
<text x="897" y="556" text-anchor="middle" font-size="12" font-weight="600" fill="#7627bb">WireEnvelope)</text>
<text x="897" y="570" text-anchor="middle" font-size="10" fill="#5f6368">→ remote runtime</text>
<!-- WireEnvelope annotation -->
<rect x="570" y="590" width="300" height="42" rx="8" fill="none" stroke="#9aa0a6" stroke-width="1" stroke-dasharray="4,3"/>
<text x="720" y="607" text-anchor="middle" font-size="10" fill="#9aa0a6">WireEnvelope { dest, type_tag, payload: Vec&lt;u8&gt; }</text>
<text x="720" y="623" text-anchor="middle" font-size="10" fill="#9aa0a6">Transport impl decides the wire format</text>
<!-- Not found → fallback -->
<line x1="310" y1="602" x2="310" y2="698" stroke="#5f6368" stroke-width="1.8" marker-end="url(#arrow-gray)"/>
<text x="322" y="650" font-size="11" fill="#5f6368" font-weight="600">not found</text>
<!-- ============================================ -->
<!-- STEP 4: Fallback -->
<!-- ============================================ -->
<rect x="30" y="690" width="1040" height="110" rx="6" fill="#fce8e6" fill-opacity="0.2" stroke="#ea4335" stroke-width="1" stroke-opacity="0.4"/>
<text x="50" y="710" font-size="13" font-weight="700" fill="#ea4335" letter-spacing="1">FALLBACK</text>
<rect x="210" y="715" width="200" height="50" rx="12" fill="#fce8e6" stroke="#ea4335" stroke-width="1.8" filter="url(#shadow)"/>
<text x="310" y="736" text-anchor="middle" font-size="12" font-weight="600" fill="#c5221f">InboxRegistry</text>
<text x="310" y="752" text-anchor="middle" font-size="10" fill="#5f6368">::try_deliver(addr, msg)</text>
<line x1="410" y1="740" x2="590" y2="740" stroke="#ea4335" stroke-width="1.8" marker-end="url(#arrow-red)"/>
<rect x="600" y="720" width="220" height="40" rx="10" fill="#fce8e6" stroke="#ea4335" stroke-width="1.5" filter="url(#shadow)"/>
<text x="710" y="744" text-anchor="middle" font-size="12" font-weight="600" fill="#c5221f">Err("Address not found")</text>
<!-- ============================================ -->
<!-- Legend -->
<!-- ============================================ -->
<rect x="50" y="820" width="1000" height="45" rx="8" fill="none" stroke="#e0e3e8" stroke-width="1"/>
<text x="70" y="842" font-size="12" font-weight="600" fill="#202124">Legend</text>
<line x1="140" y1="840" x2="175" y2="840" stroke="#34a853" stroke-width="1.8" marker-end="url(#arrow-green)"/>
<text x="185" y="844" font-size="11" fill="#34a853">Local delivery</text>
<line x1="310" y1="840" x2="345" y2="840" stroke="#9334e6" stroke-width="1.8" marker-end="url(#arrow-purple)"/>
<text x="355" y="844" font-size="11" fill="#9334e6">Transport path (new)</text>
<line x1="510" y1="840" x2="545" y2="840" stroke="#ea4335" stroke-width="1.8" marker-end="url(#arrow-red)"/>
<text x="555" y="844" font-size="11" fill="#ea4335">Error</text>
<line x1="630" y1="840" x2="665" y2="840" stroke="#5f6368" stroke-width="1.8" marker-end="url(#arrow-gray)"/>
<text x="675" y="844" font-size="11" fill="#5f6368">Fallthrough</text>
<text x="850" y="844" font-size="10" fill="#9aa0a6">runtime.rs · worker.rs · transport.rs</text>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

336
examples/tcp_ping_pong.rs Normal file
View file

@ -0,0 +1,336 @@
//! Two-process transport demo.
//!
//! Run in two terminals:
//!
//! ```bash
//! # Terminal 1 — starts the receiver (has the actor)
//! cargo run --example tcp_ping_pong --features transport -- receiver
//!
//! # Terminal 2 — sends pings across TCP
//! cargo run --example tcp_ping_pong --features transport -- sender
//! ```
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::{Arc, Mutex};
use swactor::{
actor::{ActorAddress, ActorInterface},
runtime::{Ctx, Runtime, RuntimeConfig},
transport::{
Codec, CodecRegistry, NetworkMessage, Transport, TransportRouter,
WireEnvelope,
},
Error,
};
// ─── Messages ───────────────────────────────────────────────────────────────
#[derive(Clone, Debug)]
struct Ping {
value: u32,
reply_to: ActorAddress,
}
impl NetworkMessage for Ping {
fn type_tag() -> &'static str {
"example::Ping"
}
}
#[derive(Clone, Debug, PartialEq)]
struct Pong {
value: u32,
}
impl NetworkMessage for Pong {
fn type_tag() -> &'static str {
"example::Pong"
}
}
// ─── Codec (hand-rolled, no serde needed) ───────────────────────────────────
struct ExampleCodec;
impl Codec<Ping> for ExampleCodec {
fn encode(&self, msg: &Ping) -> Result<Vec<u8>, Error> {
let mut buf = Vec::with_capacity(36);
buf.extend_from_slice(&msg.value.to_be_bytes());
buf.extend_from_slice(&msg.reply_to.0);
Ok(buf)
}
fn decode(&self, bytes: &[u8]) -> Result<Ping, Error> {
if bytes.len() < 36 {
return Err(Error::from("Ping: short read"));
}
let value = u32::from_be_bytes(bytes[0..4].try_into().unwrap());
let mut addr = [0u8; 32];
addr.copy_from_slice(&bytes[4..36]);
Ok(Ping {
value,
reply_to: ActorAddress(addr),
})
}
}
impl Codec<Pong> for ExampleCodec {
fn encode(&self, msg: &Pong) -> Result<Vec<u8>, Error> {
Ok(msg.value.to_be_bytes().to_vec())
}
fn decode(&self, bytes: &[u8]) -> Result<Pong, Error> {
if bytes.len() < 4 {
return Err(Error::from("Pong: short read"));
}
Ok(Pong {
value: u32::from_be_bytes(bytes[0..4].try_into().unwrap()),
})
}
}
// ─── TCP Transport ──────────────────────────────────────────────────────────
/// Simple length-prefixed TCP transport.
///
/// Wire format per envelope:
/// [4 bytes: total frame len (BE u32)]
/// [32 bytes: dest address]
/// [4 bytes: type_tag len (BE u32)]
/// [N bytes: type_tag UTF-8]
/// [remaining: payload bytes]
struct TcpTransport {
stream: Mutex<TcpStream>,
}
impl Transport for TcpTransport {
fn send(&self, envelope: WireEnvelope) -> Result<(), Error> {
let tag_bytes = envelope.type_tag.as_bytes();
let frame_len: u32 = (32 + 4 + tag_bytes.len() + envelope.payload.len()) as u32;
let mut buf = Vec::with_capacity(4 + frame_len as usize);
buf.extend_from_slice(&frame_len.to_be_bytes());
buf.extend_from_slice(&envelope.dest.0);
buf.extend_from_slice(&(tag_bytes.len() as u32).to_be_bytes());
buf.extend_from_slice(tag_bytes);
buf.extend_from_slice(&envelope.payload);
self.stream
.lock()
.unwrap()
.write_all(&buf)
.map_err(|e| Error::from(format!("TCP send: {e}")))
}
}
/// Read one WireEnvelope from a TCP stream.
fn read_envelope(stream: &mut TcpStream) -> std::io::Result<WireEnvelope> {
// Frame length
let mut len_buf = [0u8; 4];
stream.read_exact(&mut len_buf)?;
let frame_len = u32::from_be_bytes(len_buf) as usize;
// Read entire frame
let mut frame = vec![0u8; frame_len];
stream.read_exact(&mut frame)?;
// Parse
let mut dest = [0u8; 32];
dest.copy_from_slice(&frame[0..32]);
let tag_len = u32::from_be_bytes(frame[32..36].try_into().unwrap()) as usize;
let type_tag = String::from_utf8_lossy(&frame[36..36 + tag_len]).to_string();
let payload = frame[36 + tag_len..].to_vec();
Ok(WireEnvelope {
dest: ActorAddress(dest),
type_tag,
payload,
})
}
// ─── Actor ──────────────────────────────────────────────────────────────────
struct PongActor;
impl ActorInterface for PongActor {
type Incoming = Ping;
type Response = Pong;
fn handle(&mut self, ctx: &Ctx, msg: Ping) {
println!(
" PongActor received Ping({}), replying with Pong({})",
msg.value,
msg.value + 1
);
let _ = ctx.send(msg.reply_to, Pong { value: msg.value + 1 });
}
}
// ─── Codec registry (shared) ───────────────────────────────────────────────
fn build_codecs() -> CodecRegistry {
let mut cr = CodecRegistry::new();
cr.register::<Ping, _>(ExampleCodec);
cr.register::<Pong, _>(ExampleCodec);
cr
}
// ─── Main ───────────────────────────────────────────────────────────────────
const ADDR: &str = "127.0.0.1:9100";
fn main() {
let args: Vec<String> = std::env::args().collect();
let role = args.get(1).map(|s| s.as_str()).unwrap_or("help");
match role {
"receiver" => run_receiver(),
"sender" => run_sender(),
_ => {
eprintln!("Usage: two_process <receiver|sender>");
eprintln!();
eprintln!(" Terminal 1: cargo run --example two_process --features transport -- receiver");
eprintln!(" Terminal 2: cargo run --example two_process --features transport -- sender");
std::process::exit(1);
}
}
}
/// Receiver process: hosts the PongActor, listens for incoming envelopes on TCP.
fn run_receiver() {
println!("[receiver] Starting on {ADDR}...");
let codecs = Arc::new(build_codecs());
let codecs_recv = codecs.clone();
// Build runtime with PongActor at the well-known address
let mut rt = Runtime::new(RuntimeConfig::default());
// We need the actor at the agreed address. Since spawn() generates a random
// address, we'll use a workaround: spawn normally, then register a route
// for replies going back to the sender (those will be Pong messages).
// But actually, the sender's inbox address is dynamic, so the receiver
// needs a transport to send Pong back.
// For this demo: the receiver accepts a TCP connection, and uses that same
// connection (reversed) to send replies.
let pong_addr = rt.spawn(PongActor).unwrap();
rt.tick(); // drain spawn queue
println!("[receiver] PongActor spawned at {pong_addr}");
println!("[receiver] Listening for connections...");
let listener =
TcpListener::bind(ADDR).expect("failed to bind");
// Accept one connection
let (mut stream, peer) = listener.accept().expect("accept failed");
println!("[receiver] Connection from {peer}");
// Read the sender's inbox address (first 32 bytes)
let mut inbox_bytes = [0u8; 32];
stream.read_exact(&mut inbox_bytes).unwrap();
let sender_inbox_addr = ActorAddress(inbox_bytes);
println!("[receiver] Sender inbox: {sender_inbox_addr}");
// Send back the actual PongActor address (so sender can address messages)
stream.write_all(&pong_addr.0).unwrap();
// Set up transport for replies back to sender
let reply_transport = Arc::new(TcpTransport {
stream: Mutex::new(stream.try_clone().unwrap()),
});
let router = TransportRouter::new();
router.add_route(sender_inbox_addr, reply_transport);
rt.set_codec_registry(codecs.clone());
rt.set_transport_router(Arc::new(router));
// Event loop: read envelopes from TCP, deliver, tick
println!("[receiver] Ready — waiting for pings...\n");
loop {
match read_envelope(&mut stream) {
Ok(envelope) => {
let (addr, msg) = codecs_recv.receive(envelope).unwrap();
rt.deliver_raw(addr, msg).unwrap();
rt.tick();
}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
println!("\n[receiver] Sender disconnected.");
break;
}
Err(e) => {
eprintln!("[receiver] Read error: {e}");
break;
}
}
}
}
/// Sender process: connects to receiver, sends Pings, reads Pong replies.
fn run_sender() {
println!("[sender] Connecting to {ADDR}...");
let codecs = Arc::new(build_codecs());
let codecs_recv = codecs.clone();
let mut rt = Runtime::new(RuntimeConfig::default());
let inbox = rt.new_inbox::<Pong>().unwrap();
let inbox_addr = *inbox.addr();
// Connect and exchange addresses
let mut stream =
TcpStream::connect(ADDR).expect("failed to connect — is the receiver running?");
// Send our inbox address
stream.write_all(&inbox_addr.0).unwrap();
// Read the PongActor's address
let mut pong_bytes = [0u8; 32];
stream.read_exact(&mut pong_bytes).unwrap();
let pong_addr = ActorAddress(pong_bytes);
println!("[sender] Connected. PongActor is at {pong_addr}\n");
// Set up transport to send Pings to receiver
let send_transport = Arc::new(TcpTransport {
stream: Mutex::new(stream.try_clone().unwrap()),
});
let router = TransportRouter::new();
router.add_route(pong_addr, send_transport);
rt.set_codec_registry(codecs.clone());
rt.set_transport_router(Arc::new(router));
// Send 5 pings
for i in 1..=5 {
println!("[sender] Sending Ping({i})...");
rt.send_to(
pong_addr,
Ping {
value: i,
reply_to: inbox_addr,
},
)
.unwrap();
// Read the reply from TCP
match read_envelope(&mut stream) {
Ok(envelope) => {
let (addr, msg) = codecs_recv.receive(envelope).unwrap();
rt.deliver_raw(addr, msg).unwrap();
}
Err(e) => {
eprintln!("[sender] Read error: {e}");
break;
}
}
// Check inbox
if let Some(pong) = inbox.try_recv() {
println!("[sender] Got Pong({})!", pong.value);
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
println!("\n[sender] Done.");
}

View file

@ -31,18 +31,8 @@ impl<T> HybridChannel<T> {
}
pub fn pop(&self) -> Option<T> {
if let Some(value) = self.ring.pop() {
return Some(value);
}
match self.overflow.pop() {
Some(value) => {
Some(value)
}
None => None,
}
self.ring.pop().or_else(|| self.overflow.pop())
}
}
pub(crate) struct Receiver<T> {
@ -56,7 +46,7 @@ impl<T> Receiver<T> {
Self { queue }
}
pub fn try_recv(&self) -> Option<T> {
return self.queue.pop();
self.queue.pop()
}
pub fn new_sender(&self) -> Sender<T> {
@ -72,7 +62,7 @@ pub(crate) struct Sender<T> {
impl<T> Sender<T> {
pub fn try_send(&self, value: T) -> Result<(), T> {
return self.queue.push(value);
self.queue.push(value)
}
}

View file

@ -1,410 +0,0 @@
//! Tests meant to be run against the crate-level API
use std::any::Any;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
use crate::actor::{ActorAddress, AnyActor, Ctx};
use crate::channel::Receiver;
use crate::config::RuntimeConfig;
use crate::delivery::{AddressMap, Envelope, InboxRegistry, Placement, TickContext, WorkerId};
use crate::stats::{MailboxSnapshot, WorkerStats};
use crate::worker::Worker;
// ── Actors ─────────────────────────────────────────────────────────
/// Counts how many u64 messages it successfully handled.
struct CounterActor(Arc<AtomicUsize>);
impl AnyActor for CounterActor {
fn handle_any(&mut self, _ctx: &Ctx, msg: Box<dyn Any + Send>) {
if msg.downcast::<u64>().is_ok() {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
}
// ── Harness ────────────────────────────────────────────────────────
fn addr(id: u8) -> ActorAddress {
let mut bytes = [0u8; 32];
bytes[0] = id;
ActorAddress(bytes)
}
/// Self-contained single-worker test environment.
///
/// Holds the worker, its channels, shared state for TickContext, and
/// per-actor handle counters — everything needed to drive scenarios.
struct Env {
worker: Worker,
stats: Arc<WorkerStats>,
feed_transfer: crate::channel::Sender<Envelope>,
feed_spawn: crate::channel::Sender<(ActorAddress, Box<dyn AnyActor>)>,
address_map: AddressMap,
placement: Placement,
inbox_registry: InboxRegistry,
config: RuntimeConfig,
tc_transfer_txs: Vec<crate::channel::Sender<Envelope>>,
tc_spawn_txs: Vec<crate::channel::Sender<(ActorAddress, Box<dyn AnyActor>)>>,
counters: HashMap<u8, Arc<AtomicUsize>>,
}
impl Env {
fn new() -> Self {
Self::with_config(RuntimeConfig::default())
}
fn with_config(config: RuntimeConfig) -> Self {
let transfer_rx = Receiver::<Envelope>::new(256);
let spawn_rx = Receiver::<(ActorAddress, Box<dyn AnyActor>)>::new(256);
let feed_transfer = transfer_rx.new_sender();
let feed_spawn = spawn_rx.new_sender();
let tc_transfer = transfer_rx.new_sender();
let tc_spawn = spawn_rx.new_sender();
let stats = Arc::new(WorkerStats::new());
let mbox_snap = Arc::new(std::sync::Mutex::new(MailboxSnapshot::new()));
let worker = Worker::new(WorkerId(0), transfer_rx, spawn_rx, stats.clone(), mbox_snap);
Self {
worker,
stats,
feed_transfer,
feed_spawn,
address_map: AddressMap::new(),
placement: Placement::new(1),
inbox_registry: InboxRegistry::new(),
config,
tc_transfer_txs: vec![tc_transfer],
tc_spawn_txs: vec![tc_spawn],
counters: HashMap::new(),
}
}
/// Enqueue an actor spawn (drained on next tick, phase 1).
fn spawn(&mut self, id: u8) {
let counter = Arc::new(AtomicUsize::new(0));
let actor: Box<dyn AnyActor> = Box::new(CounterActor(counter.clone()));
self.feed_spawn.try_send((addr(id), actor)).ok().unwrap();
self.counters.insert(id, counter);
}
/// Enqueue a u64 message (drained on next tick, phase 2).
fn send(&self, id: u8, val: u64) {
self.feed_transfer
.try_send(Envelope::new(addr(id), Box::new(val)))
.ok()
.unwrap();
}
/// Enqueue a wrong-typed message (String instead of u64).
fn send_bad(&self, id: u8) {
self.feed_transfer
.try_send(Envelope::new(addr(id), Box::new("bad".to_string())))
.ok()
.unwrap();
}
/// Remove an actor from the pool (immediate, no tick needed).
fn remove(&mut self, id: u8) {
self.worker.pool.remove(&addr(id));
}
/// Run one tick of the worker loop.
fn tick(&mut self) {
let tc = TickContext {
address_map: &self.address_map,
transfer_txs: &self.tc_transfer_txs,
spawn_txs: &self.tc_spawn_txs,
placement: &self.placement,
inbox_registry: &self.inbox_registry,
config: &self.config,
};
self.worker.tick_once(&tc);
}
// ── Readouts ───────────────────────────────────────────────────
fn handled(&self, id: u8) -> usize {
self.counters[&id].load(Ordering::Relaxed)
}
fn pool_len(&self) -> usize {
self.worker.pool.len()
}
fn depth(&self) -> usize {
self.stats.total_mailbox_depth.load(Ordering::Relaxed)
}
fn processed(&self) -> u64 {
self.stats.messages_processed.load(Ordering::Relaxed)
}
fn num_actors_stat(&self) -> usize {
self.stats.num_actors.load(Ordering::Relaxed)
}
}
// ── Step-driven runner ─────────────────────────────────────────────
enum Step {
Spawn(u8),
Send(u8, u64),
SendBad(u8),
Remove(u8),
Tick,
Expect { pool_len: usize, depth: usize, processed: u64 },
ExpectHandled(u8, usize),
}
fn run(steps: &[Step]) {
run_with(RuntimeConfig::default(), steps);
}
fn run_with(config: RuntimeConfig, steps: &[Step]) {
let mut env = Env::with_config(config);
for (i, step) in steps.iter().enumerate() {
match step {
Step::Spawn(id) => env.spawn(*id),
Step::Send(id, val) => env.send(*id, *val),
Step::SendBad(id) => env.send_bad(*id),
Step::Remove(id) => env.remove(*id),
Step::Tick => env.tick(),
Step::Expect { pool_len, depth, processed } => {
assert_eq!(env.pool_len(), *pool_len, "step {i}: pool_len");
assert_eq!(env.depth(), *depth, "step {i}: depth");
assert_eq!(env.processed(), *processed, "step {i}: processed");
}
Step::ExpectHandled(id, n) => {
assert_eq!(env.handled(*id), *n, "step {i}: handled({})", id);
}
}
}
}
// ── Tests ──────────────────────────────────────────────────────────
#[test]
fn spawn_send_process() {
run(&[
Step::Spawn(1),
Step::Spawn(2),
Step::Tick,
Step::Expect { pool_len: 2, depth: 0, processed: 0 },
Step::Send(1, 10),
Step::Send(1, 20),
Step::Send(2, 30),
Step::Tick,
Step::Expect { pool_len: 2, depth: 0, processed: 3 },
Step::ExpectHandled(1, 2),
Step::ExpectHandled(2, 1),
]);
}
#[test]
fn remove_drops_future_messages() {
run(&[
Step::Spawn(1),
Step::Spawn(2),
Step::Tick,
// Remove actor 1 directly from pool
Step::Remove(1),
Step::Expect { pool_len: 1, depth: 0, processed: 0 },
// Messages to actor 1 are drained from the transfer queue
// but pool.deliver finds no slot — silently dropped
Step::Send(1, 42),
Step::Send(2, 99),
Step::Tick,
Step::Expect { pool_len: 1, depth: 0, processed: 1 },
Step::ExpectHandled(1, 0),
Step::ExpectHandled(2, 1),
]);
}
#[test]
fn wrong_type_silently_dropped() {
run(&[
Step::Spawn(1),
Step::Tick,
// Mix correct (u64) and incorrect (String) types
Step::Send(1, 1),
Step::SendBad(1),
Step::Send(1, 2),
Step::SendBad(1),
Step::SendBad(1),
Step::Send(1, 3),
Step::Tick,
// All 6 popped from mailbox ("processed" by the pool),
// but only the 3 u64 messages were handled by the actor
Step::Expect { pool_len: 1, depth: 0, processed: 6 },
Step::ExpectHandled(1, 3),
]);
}
#[test]
fn all_messages_drain_in_one_tick() {
run(&[
Step::Spawn(1),
Step::Tick,
// Send 10 messages
Step::Send(1, 0), Step::Send(1, 1), Step::Send(1, 2), Step::Send(1, 3),
Step::Send(1, 4), Step::Send(1, 5), Step::Send(1, 6), Step::Send(1, 7),
Step::Send(1, 8), Step::Send(1, 9),
// All 10 processed in a single tick
Step::Tick,
Step::Expect { pool_len: 1, depth: 0, processed: 10 },
Step::ExpectHandled(1, 10),
]);
}
#[test]
fn spawn_and_send_same_tick() {
// Spawn is phase 1, transfer is phase 2, processing is phase 3.
// All three happen within a single tick_once call.
run(&[
Step::Spawn(1),
Step::Send(1, 42),
Step::Tick,
Step::Expect { pool_len: 1, depth: 0, processed: 1 },
Step::ExpectHandled(1, 1),
]);
}
#[test]
fn stats_track_pool_mutations() {
let mut env = Env::new();
// Before any tick, stats are zeroed
assert_eq!(env.num_actors_stat(), 0);
assert_eq!(env.depth(), 0);
assert_eq!(env.processed(), 0);
// Spawn 3 + tick → stats reflect 3 actors
env.spawn(1);
env.spawn(2);
env.spawn(3);
env.tick();
assert_eq!(env.num_actors_stat(), 3);
// Send 5 to actor 1 + tick → processed increases
for i in 0..5 {
env.send(1, i);
}
env.tick();
assert_eq!(env.processed(), 5);
assert_eq!(env.depth(), 0);
assert_eq!(env.num_actors_stat(), 3);
// Remove actor 2 + tick → stats update
env.remove(2);
env.tick();
assert_eq!(env.num_actors_stat(), 2);
assert_eq!(env.pool_len(), 2);
}
/// A long mixed-action sequence: spawns, sends, removes, wrong types,
/// and backpressure — all in one run.
#[test]
fn interleaved_lifecycle() {
run(&[
// ── Phase 1: build the pool ────────────────────────────────
Step::Spawn(1),
Step::Spawn(2),
Step::Spawn(3),
Step::Tick,
Step::Expect { pool_len: 3, depth: 0, processed: 0 },
// ── Phase 2: normal message flow ───────────────────────────
Step::Send(1, 100),
Step::Send(2, 200),
Step::Send(3, 300),
Step::Tick,
Step::ExpectHandled(1, 1),
Step::ExpectHandled(2, 1),
Step::ExpectHandled(3, 1),
Step::Expect { pool_len: 3, depth: 0, processed: 3 },
// ── Phase 3: remove actor 2, send to all 3 ────────────────
Step::Remove(2),
Step::Send(1, 101),
Step::Send(2, 201), // actor 2 gone — dropped at deliver
Step::Send(3, 301),
Step::Tick,
Step::Expect { pool_len: 2, depth: 0, processed: 5 },
Step::ExpectHandled(1, 2),
Step::ExpectHandled(2, 1), // unchanged since removal
Step::ExpectHandled(3, 2),
// ── Phase 4: late spawn + immediate send ───────────────────
Step::Spawn(4),
Step::Send(4, 400),
Step::Tick,
Step::Expect { pool_len: 3, depth: 0, processed: 6 },
Step::ExpectHandled(4, 1),
// ── Phase 5: bad types mixed with good ─────────────────────
Step::SendBad(1),
Step::SendBad(1),
Step::SendBad(1),
Step::Send(1, 999),
Step::Tick,
// 4 popped (3 bad + 1 good), only 1 handled by actor
Step::Expect { pool_len: 3, depth: 0, processed: 10 },
Step::ExpectHandled(1, 3), // 2 from prior phases + 1 good
// ── Phase 6: remove all, send to ghosts ────────────────────
Step::Remove(1),
Step::Remove(3),
Step::Remove(4),
Step::Expect { pool_len: 0, depth: 0, processed: 10 },
Step::Send(1, 0),
Step::Send(3, 0),
Step::Tick,
Step::Expect { pool_len: 0, depth: 0, processed: 10 },
]);
}
#[test]
fn run_loop_stops_on_shutdown() {
let transfer_rx = Receiver::<Envelope>::new(64);
let spawn_rx = Receiver::<(ActorAddress, Box<dyn AnyActor>)>::new(64);
let transfer_tx = transfer_rx.new_sender();
let spawn_tx = spawn_rx.new_sender();
let stats = Arc::new(WorkerStats::new());
let mbox_snap = Arc::new(std::sync::Mutex::new(MailboxSnapshot::new()));
let mut worker = Worker::new(WorkerId(0), transfer_rx, spawn_rx, stats, mbox_snap);
let is_running = AtomicBool::new(false);
let address_map = AddressMap::new();
let placement = Placement::new(1);
let inbox_registry = InboxRegistry::new();
let config = RuntimeConfig::default();
let tc = TickContext {
address_map: &address_map,
transfer_txs: &[transfer_tx],
spawn_txs: &[spawn_tx],
placement: &placement,
inbox_registry: &inbox_registry,
config: &config,
};
thread::scope(|s| {
s.spawn(|| worker.run(&tc, &is_running));
});
}

View file

@ -28,12 +28,6 @@ pub(crate) struct AddressMap {
}
impl AddressMap {
pub fn new() -> Self {
Self {
inner: RwLock::new(HashMap::new()),
}
}
pub fn with_capacity(cap: usize) -> Self {
Self {
inner: RwLock::new(HashMap::with_capacity(cap)),
@ -44,18 +38,10 @@ impl AddressMap {
self.inner.write().unwrap().insert(addr, worker);
}
pub fn remove(&self, addr: &ActorAddress) {
self.inner.write().unwrap().remove(addr);
}
pub fn lookup(&self, addr: &ActorAddress) -> Option<WorkerId> {
self.inner.read().unwrap().get(addr).copied()
}
pub fn len(&self) -> usize {
self.inner.read().unwrap().len()
}
/// Returns a snapshot of all (address, worker) pairs.
pub fn snapshot(&self) -> Vec<(ActorAddress, WorkerId)> {
self.inner
@ -106,10 +92,6 @@ impl Envelope {
self.dest
}
pub fn downcast<M: 'static>(self) -> Option<M> {
self.payload.downcast::<M>().ok().map(|b| *b)
}
pub fn into_payload(self) -> Box<dyn Any + Send> {
self.payload
}
@ -144,6 +126,11 @@ impl InboxRegistry {
self.senders.write().unwrap().insert(addr, sender);
}
/// Check if an address is registered without consuming a message.
pub fn contains(&self, addr: &ActorAddress) -> bool {
self.senders.read().unwrap().contains_key(addr)
}
pub fn try_deliver(
&self,
addr: ActorAddress,
@ -167,4 +154,29 @@ pub(crate) struct TickContext<'a> {
pub(crate) placement: &'a Placement,
pub(crate) inbox_registry: &'a InboxRegistry,
pub(crate) config: &'a RuntimeConfig,
#[cfg(feature = "transport")]
pub(crate) codec_registry: Option<&'a crate::transport::CodecRegistry>,
#[cfg(feature = "transport")]
pub(crate) transport_router: Option<&'a crate::transport::TransportRouter>,
}
impl<'a> TickContext<'a> {
/// Route a message whose destination is not in the local address map.
/// Tries inbox registry, then remote transport, then falls back to inbox error.
pub(crate) fn route_nonlocal(
&self,
addr: ActorAddress,
msg: Box<dyn Any + Send>,
) -> Result<(), Error> {
#[cfg(feature = "transport")]
{
if self.inbox_registry.contains(&addr) {
return self.inbox_registry.try_deliver(addr, msg);
}
if let (Some(cr), Some(tr)) = (self.codec_registry, self.transport_router) {
return crate::transport::send_via_transport(addr, msg, cr, tr);
}
}
self.inbox_registry.try_deliver(addr, msg)
}
}

View file

@ -14,18 +14,15 @@
/// ```
#[derive(Debug)]
pub struct Error(Box<dyn std::error::Error + Send + Sync + 'static>);
pub(crate) fn convert_err<E: std::fmt::Debug>(e: E) -> Error {
Error(format!("{e:?}").into())
}
impl<T: AsRef<str>> From<T> for Error {
fn from(value: T) -> Self {
convert_err(value.as_ref())
Error(format!("{:?}", value.as_ref()).into())
}
}
impl ToString for Error {
fn to_string(&self) -> String {
format!("{:?}", self.0)
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.0)
}
}

View file

@ -12,6 +12,9 @@ pub mod stats;
pub mod runtime;
#[cfg(feature = "transport")]
pub mod transport;
#[cfg(feature = "getrandom")]
pub(crate) fn get_random(buf: &mut [u8]) {
getrandom::getrandom(buf).unwrap()
@ -30,7 +33,3 @@ pub(crate) fn get_random(buf: &mut [u8]) {
*byte = bytes[i % core::mem::size_of::<usize>()];
}
}
#[cfg(test)]
mod crate_test;

View file

@ -9,7 +9,7 @@ use crate::channel::{Receiver, Sender};
// Re-export config types so existing code using `runtime::RuntimeConfig` still works
pub use crate::config::{BackoffPolicy, RuntimeConfig};
use crate::delivery::{AddressMap, Envelope, InboxRegistry, Placement, TickContext, WorkerId};
use crate::stats::{ActorInfo, MailboxSnapshot, WorkerStats};
use crate::stats::{ActorInfo, WorkerStats};
// Re-export stats types so existing code using `runtime::*` still works
pub use crate::stats::{RuntimeStats, WorkerInfo};
use crate::worker::Worker;
@ -67,9 +67,13 @@ pub struct Runtime {
is_running: AtomicBool,
worker_stats: Vec<Arc<WorkerStats>>,
/// Per-worker mailbox snapshots, updated each tick by workers.
mailbox_snapshots: Vec<Arc<std::sync::Mutex<MailboxSnapshot>>>,
mailbox_snapshots: Vec<Arc<std::sync::Mutex<Vec<(ActorAddress, usize)>>>>,
/// Workers available for tick(). run() drains this and moves workers to threads.
tick_workers: RefCell<Vec<Worker>>,
#[cfg(feature = "transport")]
codec_registry: Option<Arc<crate::transport::CodecRegistry>>,
#[cfg(feature = "transport")]
transport_router: Option<Arc<crate::transport::TransportRouter>>,
}
// Safety: RefCell<Vec<Worker>> is only accessed from the owning thread via tick().
@ -107,7 +111,7 @@ impl Runtime {
spawn_txs.push(spawn_tx);
let stats = Arc::new(WorkerStats::new());
let mbox_snap = Arc::new(std::sync::Mutex::new(MailboxSnapshot::new()));
let mbox_snap = Arc::new(std::sync::Mutex::new(Vec::new()));
worker_stats.push(stats.clone());
mailbox_snapshots.push(mbox_snap.clone());
workers.push(Worker::new(WorkerId(i), transfer_rx, spawn_rx, stats, mbox_snap));
@ -124,6 +128,10 @@ impl Runtime {
worker_stats,
mailbox_snapshots,
tick_workers: RefCell::new(workers),
#[cfg(feature = "transport")]
codec_registry: None,
#[cfg(feature = "transport")]
transport_router: None,
};
#[cfg(feature = "tracing")]
@ -158,17 +166,12 @@ impl Runtime {
/// Send a message to an actor address
pub fn send_to<M: Message>(&self, addr: ActorAddress, msg: M) -> Result<(), Error> {
let msg_box: Box<dyn Any + Send> = Box::new(msg);
let result = self.send_any(addr, Box::new(msg));
#[cfg(feature = "tracing")]
tracing::trace!(dest = %addr, "message.sent");
match self.address_map.lookup(&addr) {
Some(wid) => self.transfer_txs[wid.as_usize()]
.try_send(Envelope::new(addr, msg_box))
.map_err(|_| Error::from("Transfer queue full")),
None => self.inbox_registry.try_deliver(addr, msg_box),
}
result
}
/// Create an external inbox for receiving messages in the outer process containing the runtime
@ -183,6 +186,21 @@ impl Runtime {
})
}
fn make_tick_context(&self) -> TickContext<'_> {
TickContext {
address_map: &self.address_map,
transfer_txs: &self.transfer_txs,
spawn_txs: &self.spawn_txs,
placement: &self.placement,
inbox_registry: &self.inbox_registry,
config: &self.config,
#[cfg(feature = "transport")]
codec_registry: self.codec_registry.as_deref(),
#[cfg(feature = "transport")]
transport_router: self.transport_router.as_deref(),
}
}
/// Drive one tick of the single-threaded worker.
///
/// Panics if called on a multi-threaded runtime — use `run()` instead.
@ -191,14 +209,7 @@ impl Runtime {
self.config.num_threads < 2,
"tick() is only valid for single-threaded runtimes; use run() for multi-threaded"
);
let tc = TickContext {
address_map: &self.address_map,
transfer_txs: &self.transfer_txs,
spawn_txs: &self.spawn_txs,
placement: &self.placement,
inbox_registry: &self.inbox_registry,
config: &self.config,
};
let tc = self.make_tick_context();
for worker in self.tick_workers.borrow_mut().iter_mut() {
worker.tick_once(&tc);
}
@ -226,14 +237,7 @@ impl Runtime {
let handle = thread::Builder::new()
.name(name)
.spawn(move || {
let tc = TickContext {
address_map: &rt_clone.address_map,
transfer_txs: &rt_clone.transfer_txs,
spawn_txs: &rt_clone.spawn_txs,
placement: &rt_clone.placement,
inbox_registry: &rt_clone.inbox_registry,
config: &rt_clone.config,
};
let tc = rt_clone.make_tick_context();
worker.run(&tc, &rt_clone.is_running);
})
.expect("failed to spawn worker thread");
@ -248,61 +252,28 @@ impl Runtime {
/// Returns a snapshot of runtime stats: actor placements and per-worker info.
pub fn stats(&self) -> RuntimeStats {
let num_workers = if self.config.num_threads < 2 {
1
} else {
self.config.num_threads
};
let workers = self
.worker_stats
.iter()
.enumerate()
.map(|(i, ws)| WorkerInfo {
id: i,
num_actors: ws.num_actors.load(Ordering::Relaxed),
mailbox_depth: ws.total_mailbox_depth.load(Ordering::Relaxed),
messages_processed: ws.messages_processed.load(Ordering::Relaxed),
local_sends: ws.local_sends.load(Ordering::Relaxed),
cross_sends: ws.cross_sends.load(Ordering::Relaxed),
inbox_sends: ws.inbox_sends.load(Ordering::Relaxed),
type_mismatches: ws.type_mismatches.load(Ordering::Relaxed),
panics: ws.panics.load(Ordering::Relaxed),
})
let num_workers = if self.config.num_threads < 2 { 1 } else { self.config.num_threads };
let workers = self.worker_stats.iter().enumerate()
.map(|(i, ws)| ws.snapshot(i))
.collect();
let actors = self
.address_map
.snapshot()
.into_iter()
let actors = self.address_map.snapshot().into_iter()
.map(|(addr, wid)| (addr, wid.as_usize()))
.collect();
// Collect per-actor mailbox depth details
let mut actor_details = Vec::new();
for (wid, snap_lock) in self.mailbox_snapshots.iter().enumerate() {
let snap = snap_lock.lock().unwrap();
for &(addr, depth) in &snap.depths {
actor_details.push(ActorInfo {
address: addr,
worker_id: wid,
mailbox_depth: depth,
});
for &(addr, depth) in snap_lock.lock().unwrap().iter() {
actor_details.push(ActorInfo { address: addr, worker_id: wid, mailbox_depth: depth });
}
}
// Drain tick timings from each worker
let tick_timings = self
.worker_stats
.iter()
let tick_timings = self.worker_stats.iter()
.map(|ws| ws.drain_tick_timings())
.collect();
RuntimeStats {
num_workers,
actors,
workers,
actor_details,
tick_timings,
}
RuntimeStats { num_workers, actors, workers, actor_details, tick_timings }
}
/// Signal all workers to stop
@ -312,16 +283,63 @@ impl Runtime {
self.is_running.store(false, Ordering::Release);
}
/// Set the codec registry for remote transport.
#[cfg(feature = "transport")]
pub fn set_codec_registry(&mut self, registry: Arc<crate::transport::CodecRegistry>) {
self.codec_registry = Some(registry);
}
/// Set the transport router for remote message delivery.
#[cfg(feature = "transport")]
pub fn set_transport_router(&mut self, router: Arc<crate::transport::TransportRouter>) {
self.transport_router = Some(router);
}
/// Route a message whose destination is not in the local address map.
fn route_nonlocal(
&self,
addr: ActorAddress,
msg: Box<dyn Any + Send>,
) -> Result<(), Error> {
#[cfg(feature = "transport")]
{
if self.inbox_registry.contains(&addr) {
return self.inbox_registry.try_deliver(addr, msg);
}
if let (Some(cr), Some(tr)) = (&self.codec_registry, &self.transport_router) {
return crate::transport::send_via_transport(addr, msg, cr, tr);
}
}
self.inbox_registry.try_deliver(addr, msg)
}
/// Deliver a raw deserialized message into the runtime.
///
/// Used by [`CodecRegistry::receive`](crate::transport::CodecRegistry::receive)
/// to inject incoming messages from remote runtimes.
#[cfg(feature = "transport")]
pub fn deliver_raw(
&self,
addr: ActorAddress,
msg: Box<dyn Any + Send>,
) -> Result<(), Error> {
match self.address_map.lookup(&addr) {
Some(wid) => self.transfer_txs[wid.as_usize()]
.try_send(Envelope::new(addr, msg))
.map_err(|_| Error::from("Transfer queue full")),
None => self.inbox_registry.try_deliver(addr, msg),
}
}
}
impl ContextInner for Runtime {
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error> {
match self.address_map.lookup(&addr) {
Some(wid) => {
let _ = self.transfer_txs[wid.as_usize()].try_send(Envelope::new(addr, msg));
Ok(())
}
None => self.inbox_registry.try_deliver(addr, msg),
Some(wid) => self.transfer_txs[wid.as_usize()]
.try_send(Envelope::new(addr, msg))
.map_err(|_| Error::from("Transfer queue full")),
None => self.route_nonlocal(addr, msg),
}
}

View file

@ -1,7 +1,10 @@
use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, AtomicUsize};
use crate::actor::ActorAddress;
const TICK_BUFFER_CAP: usize = 1024;
/// Timing data for one tick_once invocation.
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -26,8 +29,8 @@ pub struct WorkerStats {
// Error counters
pub type_mismatches: AtomicU64,
pub panics: AtomicU64,
// Tick timing ring buffer (last N ticks)
tick_timings: std::sync::Mutex<RingBuffer<TickTiming>>,
// Tick timing buffer (last N ticks)
tick_timings: std::sync::Mutex<VecDeque<TickTiming>>,
}
impl WorkerStats {
@ -41,17 +44,37 @@ impl WorkerStats {
inbox_sends: AtomicU64::new(0),
type_mismatches: AtomicU64::new(0),
panics: AtomicU64::new(0),
tick_timings: std::sync::Mutex::new(RingBuffer::new(1024)),
tick_timings: std::sync::Mutex::new(VecDeque::with_capacity(TICK_BUFFER_CAP)),
}
}
pub fn push_tick_timing(&self, timing: TickTiming) {
self.tick_timings.lock().unwrap().push(timing);
let mut buf = self.tick_timings.lock().unwrap();
if buf.len() >= TICK_BUFFER_CAP {
buf.pop_front();
}
buf.push_back(timing);
}
/// Returns a snapshot of recent tick timings (drains the buffer).
pub fn drain_tick_timings(&self) -> Vec<TickTiming> {
self.tick_timings.lock().unwrap().drain()
self.tick_timings.lock().unwrap().drain(..).collect()
}
/// Create a point-in-time snapshot as a [`WorkerInfo`].
pub fn snapshot(&self, id: usize) -> WorkerInfo {
use std::sync::atomic::Ordering::Relaxed;
WorkerInfo {
id,
num_actors: self.num_actors.load(Relaxed),
mailbox_depth: self.total_mailbox_depth.load(Relaxed),
messages_processed: self.messages_processed.load(Relaxed),
local_sends: self.local_sends.load(Relaxed),
cross_sends: self.cross_sends.load(Relaxed),
inbox_sends: self.inbox_sends.load(Relaxed),
type_mismatches: self.type_mismatches.load(Relaxed),
panics: self.panics.load(Relaxed),
}
}
}
@ -92,41 +115,3 @@ pub struct RuntimeStats {
/// Recent tick timings per worker (index = worker id).
pub tick_timings: Vec<Vec<TickTiming>>,
}
/// Simple ring buffer for storing recent values.
pub(crate) struct RingBuffer<T> {
buf: Vec<T>,
capacity: usize,
}
impl<T> RingBuffer<T> {
pub fn new(capacity: usize) -> Self {
Self {
buf: Vec::with_capacity(capacity),
capacity,
}
}
pub fn push(&mut self, value: T) {
if self.buf.len() >= self.capacity {
self.buf.remove(0);
}
self.buf.push(value);
}
/// Drain all items, returning them and leaving the buffer empty.
pub fn drain(&mut self) -> Vec<T> {
std::mem::take(&mut self.buf)
}
}
/// Per-actor mailbox depth snapshot, collected by workers.
pub(crate) struct MailboxSnapshot {
pub depths: Vec<(ActorAddress, usize)>,
}
impl MailboxSnapshot {
pub fn new() -> Self {
Self { depths: Vec::new() }
}
}

229
src/transport.rs Normal file
View file

@ -0,0 +1,229 @@
//! Transport-agnostic messaging framework.
//!
//! Enables actors on different runtimes to communicate transparently via
//! pluggable codecs (serialization) and transports (delivery protocol).
//!
//! # Two layers of pluggability
//!
//! - **[`Codec<M>`]**: HOW bytes are encoded — gRPC/protobuf, bincode, custom, etc.
//! - **[`Transport`]**: WHERE bytes are sent — in-memory, gRPC channel, TCP, etc.
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use crate::actor::{ActorAddress, Message};
use crate::Error;
// ─── Codec ──────────────────────────────────────────────────────────────────
/// User-implemented codec for a specific message type.
///
/// This is where serialization logic lives — gRPC/protobuf, bincode,
/// msgpack, or any custom format. The framework imposes no serialization
/// constraints on message types; the codec defines what it needs from `M`.
pub trait Codec<M>: Send + Sync + 'static {
fn encode(&self, msg: &M) -> Result<Vec<u8>, Error>;
fn decode(&self, bytes: &[u8]) -> Result<M, Error>;
}
// ─── NetworkMessage ─────────────────────────────────────────────────────────
/// Marker for messages that can cross runtime boundaries.
///
/// The only requirement is a stable `type_tag` string used for deserialization
/// routing on the receiving side. No serialization bounds — the [`Codec`]
/// handles that separately.
pub trait NetworkMessage: Message {
/// Stable identifier for this message type, used for deserialization routing.
/// Must be unique per type and stable across compilations.
/// Convention: `"crate_name::TypeName"`.
fn type_tag() -> &'static str;
}
// ─── WireEnvelope ───────────────────────────────────────────────────────────
/// Serialized message ready for transport across runtime boundaries.
///
/// A plain struct — the [`Transport`] implementation decides how to put it
/// on the wire (protobuf, raw bytes, etc.).
#[derive(Debug, Clone)]
pub struct WireEnvelope {
pub dest: ActorAddress,
pub type_tag: String,
pub payload: Vec<u8>,
}
// ─── Transport ──────────────────────────────────────────────────────────────
/// Pluggable transport protocol.
///
/// Implementations queue or send the envelope to a remote runtime.
/// `send` should not block the calling thread.
pub trait Transport: Send + Sync {
fn send(&self, envelope: WireEnvelope) -> Result<(), Error>;
}
// ─── CodecRegistry ──────────────────────────────────────────────────────────
type EncodeFn = Box<dyn Fn(Box<dyn Any + Send>) -> Result<(String, Vec<u8>), Error> + Send + Sync>;
type DecodeFn = Box<dyn Fn(&[u8]) -> Result<Box<dyn Any + Send>, Error> + Send + Sync>;
/// Unified registry for encoding (`TypeId` → encoder) and decoding
/// (`type_tag` → decoder).
///
/// Built at setup time via [`register`](Self::register), then shared
/// read-only via `Arc`.
pub struct CodecRegistry {
encoders: HashMap<TypeId, EncodeFn>,
decoders: HashMap<String, DecodeFn>,
}
impl CodecRegistry {
pub fn new() -> Self {
Self {
encoders: HashMap::new(),
decoders: HashMap::new(),
}
}
/// Register a message type with its codec.
///
/// Both encoding and decoding are handled by the same codec instance.
pub fn register<M: NetworkMessage, C: Codec<M>>(&mut self, codec: C) {
let codec = Arc::new(codec);
// Encoder side — closure downcasts Any → M, encodes, returns (tag, bytes)
let encode_codec = codec.clone();
let encode_fn: EncodeFn = Box::new(move |msg: Box<dyn Any + Send>| {
let typed = msg
.downcast::<M>()
.map_err(|_| Error::from("Transport: type downcast failed during encode"))?;
let bytes = encode_codec.encode(&*typed)?;
Ok((M::type_tag().to_string(), bytes))
});
self.encoders.insert(TypeId::of::<M>(), encode_fn);
// Decoder side — closure captures Arc<C>
let decode_fn: DecodeFn = Box::new(move |bytes: &[u8]| {
let msg: M = codec.decode(bytes)?;
Ok(Box::new(msg) as Box<dyn Any + Send>)
});
self.decoders.insert(M::type_tag().to_string(), decode_fn);
}
/// Encode a type-erased message. Returns `(type_tag, payload_bytes)`.
pub(crate) fn encode(
&self,
type_id: TypeId,
msg: Box<dyn Any + Send>,
) -> Result<(String, Vec<u8>), Error> {
let encoder = self.encoders.get(&type_id).ok_or_else(|| {
Error::from("Transport: message type not registered for remote transport")
})?;
encoder(msg)
}
/// Decode bytes back to a type-erased message using the `type_tag` key.
pub fn decode(
&self,
type_tag: &str,
bytes: &[u8],
) -> Result<Box<dyn Any + Send>, Error> {
let decoder = self.decoders.get(type_tag).ok_or_else(|| {
Error::from(format!("Transport: unknown type_tag '{type_tag}'"))
})?;
decoder(bytes)
}
/// Deserialize a [`WireEnvelope`] into an address and type-erased message.
pub fn receive(
&self,
envelope: WireEnvelope,
) -> Result<(ActorAddress, Box<dyn Any + Send>), Error> {
let payload = self.decode(&envelope.type_tag, &envelope.payload)?;
Ok((envelope.dest, payload))
}
}
// ─── TransportRouter ────────────────────────────────────────────────────────
/// Maps remote actor addresses to their [`Transport`].
pub struct TransportRouter {
routes: RwLock<HashMap<ActorAddress, Arc<dyn Transport>>>,
}
impl TransportRouter {
pub fn new() -> Self {
Self {
routes: RwLock::new(HashMap::new()),
}
}
/// Register a remote address as reachable via the given transport.
pub fn add_route(&self, addr: ActorAddress, transport: Arc<dyn Transport>) {
self.routes.write().unwrap().insert(addr, transport);
}
/// Look up which transport handles a given address.
pub(crate) fn lookup(&self, addr: &ActorAddress) -> Option<Arc<dyn Transport>> {
self.routes.read().unwrap().get(addr).cloned()
}
}
// ─── InMemoryTransport ──────────────────────────────────────────────────────
/// In-process transport connecting two runtimes via an `mpsc` channel.
///
/// Use [`pair`](Self::pair) to create a linked transport + receiver.
pub struct InMemoryTransport {
tx: std::sync::Mutex<std::sync::mpsc::Sender<WireEnvelope>>,
}
impl InMemoryTransport {
/// Create a linked pair: the transport sends to the returned receiver.
pub fn pair() -> (Arc<InMemoryTransport>, std::sync::mpsc::Receiver<WireEnvelope>) {
let (tx, rx) = std::sync::mpsc::channel();
let transport = Arc::new(InMemoryTransport {
tx: std::sync::Mutex::new(tx),
});
(transport, rx)
}
}
impl Transport for InMemoryTransport {
fn send(&self, envelope: WireEnvelope) -> Result<(), Error> {
self.tx
.lock()
.unwrap()
.send(envelope)
.map_err(|_| Error::from("InMemoryTransport: receiver dropped"))
}
}
// ─── send_via_transport (crate-internal helper) ─────────────────────────────
/// Attempt to serialize and send a message via the transport router.
///
/// Called by the send paths in `Runtime` and `WorkerContext` when an address
/// is not found locally or in the inbox registry.
pub(crate) fn send_via_transport(
addr: ActorAddress,
msg: Box<dyn Any + Send>,
codec_registry: &CodecRegistry,
transport_router: &TransportRouter,
) -> Result<(), Error> {
let transport = transport_router
.lookup(&addr)
.ok_or_else(|| Error::from("Address not found"))?;
let type_id = (*msg).type_id();
let (type_tag, payload) = codec_registry.encode(type_id, msg)?;
let wire = WireEnvelope {
dest: addr,
type_tag,
payload,
};
transport.send(wire)
}

View file

@ -9,7 +9,7 @@ use std::time::Instant;
use crate::actor::{ActorAddress, AnyActor, ContextInner, Ctx};
use crate::channel::Receiver;
use crate::delivery::{Envelope, TickContext, WorkerId};
use crate::stats::{MailboxSnapshot, TickTiming, WorkerStats};
use crate::stats::{TickTiming, WorkerStats};
use crate::Error;
/// A worker owns a set of actors and runs them in a loop.
@ -20,7 +20,7 @@ pub(crate) struct Worker {
spawn_rx: Receiver<(ActorAddress, Box<dyn AnyActor>)>,
stats: Arc<WorkerStats>,
/// Shared snapshot of per-actor mailbox depths, readable by Runtime::stats().
mailbox_snapshot: Arc<std::sync::Mutex<MailboxSnapshot>>,
mailbox_snapshot: Arc<std::sync::Mutex<Vec<(ActorAddress, usize)>>>,
}
impl Worker {
@ -29,7 +29,7 @@ impl Worker {
transfer_rx: Receiver<Envelope>,
spawn_rx: Receiver<(ActorAddress, Box<dyn AnyActor>)>,
stats: Arc<WorkerStats>,
mailbox_snapshot: Arc<std::sync::Mutex<MailboxSnapshot>>,
mailbox_snapshot: Arc<std::sync::Mutex<Vec<(ActorAddress, usize)>>>,
) -> Self {
Self {
id,
@ -127,8 +127,7 @@ impl Worker {
// Publish per-actor mailbox depths
{
let depths: Vec<(ActorAddress, usize)> = self.pool.mailbox_depths();
let mut snap = self.mailbox_snapshot.lock().unwrap();
snap.depths = depths;
*self.mailbox_snapshot.lock().unwrap() = depths;
}
let t6 = Instant::now();
@ -205,22 +204,18 @@ impl ContextInner for WorkerContext<'_> {
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error> {
match self.tc.address_map.lookup(&addr) {
Some(wid) if wid == self.worker_id => {
// Same worker: buffer for local delivery (after current tick round)
self.stats.local_sends.fetch_add(1, Ordering::Relaxed);
self.pending_local.borrow_mut().push((addr, msg));
Ok(())
}
Some(wid) => {
// Cross worker: envelope through transfer queue
self.stats.cross_sends.fetch_add(1, Ordering::Relaxed);
let envelope = Envelope::new(addr, msg);
let _ = self.tc.transfer_txs[wid.as_usize()].try_send(envelope);
let _ = self.tc.transfer_txs[wid.as_usize()].try_send(Envelope::new(addr, msg));
Ok(())
}
None => {
// Try inbox registry (external inboxes)
self.stats.inbox_sends.fetch_add(1, Ordering::Relaxed);
self.tc.inbox_registry.try_deliver(addr, msg)
self.tc.route_nonlocal(addr, msg)
}
}
}
@ -232,7 +227,6 @@ impl ContextInner for WorkerContext<'_> {
.try_send((addr, actor))
.map_err(|_| Error::from("Spawn queue full"))
}
}
struct ActorSlot {
@ -259,10 +253,6 @@ impl ActorPool {
});
}
pub fn remove(&mut self, addr: &ActorAddress) -> Option<Box<dyn AnyActor>> {
self.actors.remove(addr).map(|slot| slot.actor)
}
/// Deliver a type-erased message to the actor at `addr`.
/// Returns `true` if the actor exists (message is queued; type check deferred to tick).
pub fn deliver(&mut self, addr: &ActorAddress, msg: Box<dyn Any + Send>) -> bool {

File diff suppressed because it is too large Load diff

View file

@ -1,91 +0,0 @@
use swactor::{
actor::{ActorAddress, ActorInterface},
runtime::{Ctx, Runtime, RuntimeConfig},
};
#[derive(Clone)]
struct Ping {
reply_to: ActorAddress,
}
#[derive(Clone)]
struct Pong;
struct PingActor;
impl ActorInterface for PingActor {
type Incoming = Ping;
type Response = Pong;
fn handle(&mut self, ctx: &Ctx, msg: Ping) {
let _ = ctx.send(msg.reply_to, Pong);
}
}
struct Counter(u64);
impl ActorInterface for Counter {
type Incoming = u64;
type Response = ();
fn handle(&mut self, _ctx: &Ctx, _msg: u64) {
self.0 += 1;
}
}
#[test]
fn stats_reflect_actor_lifecycle() {
let rt = Runtime::new(RuntimeConfig::default());
let _ping1 = rt.spawn(PingActor).unwrap();
let _ping2 = rt.spawn(PingActor).unwrap();
let counter = rt.spawn(Counter(0)).unwrap();
for i in 0..20u64 {
rt.send_to(counter, i).unwrap();
}
// Tick enough to fully drain all messages
for _ in 0..6 {
rt.tick();
}
let s = rt.stats();
assert_eq!(s.num_workers, 1);
assert_eq!(s.actors.len(), 3);
assert_eq!(s.workers[0].num_actors, 3);
assert_eq!(s.workers[0].mailbox_depth, 0);
assert_eq!(s.workers[0].messages_processed, 20);
}
#[test]
fn stats_reflect_multi_worker_distribution() {
let config = RuntimeConfig {
num_threads: 3,
..Default::default()
};
let rt = Runtime::new(config);
let mut addrs = Vec::new();
for _ in 0..6 {
addrs.push(rt.spawn(Counter(0)).unwrap());
}
for &addr in &addrs {
for i in 0..10u64 {
rt.send_to(addr, i).unwrap();
}
}
let handle = rt.run().unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
let s = handle.runtime.stats();
handle.shutdown();
handle.join();
assert_eq!(s.num_workers, 3);
assert_eq!(s.actors.len(), 6);
let total_processed: u64 = s.workers.iter().map(|w| w.messages_processed).sum();
assert_eq!(total_processed, 60);
}

325
tests/transport_api.rs Normal file
View file

@ -0,0 +1,325 @@
#![cfg(feature = "transport")]
use std::sync::Arc;
use swactor::{
actor::{ActorAddress, ActorInterface},
runtime::{Ctx, Runtime, RuntimeConfig},
transport::{
Codec, CodecRegistry, InMemoryTransport, NetworkMessage,
TransportRouter, WireEnvelope,
},
Error,
};
// ---------------------------------------------------------------------------
// Test codec — simple big-endian u32 encoding (no serde dependency)
// ---------------------------------------------------------------------------
/// Minimal hand-rolled codec to prove the framework is serde-agnostic.
struct TestCodec;
impl Codec<Ping> for TestCodec {
fn encode(&self, msg: &Ping) -> Result<Vec<u8>, Error> {
let mut buf = Vec::with_capacity(36);
buf.extend_from_slice(&msg.value.to_be_bytes());
buf.extend_from_slice(&msg.reply_to.0);
Ok(buf)
}
fn decode(&self, bytes: &[u8]) -> Result<Ping, Error> {
if bytes.len() < 36 {
return Err(Error::from("Ping decode: not enough bytes"));
}
let value = u32::from_be_bytes(bytes[0..4].try_into().unwrap());
let mut addr_bytes = [0u8; 32];
addr_bytes.copy_from_slice(&bytes[4..36]);
Ok(Ping {
value,
reply_to: ActorAddress(addr_bytes),
})
}
}
impl Codec<Pong> for TestCodec {
fn encode(&self, msg: &Pong) -> Result<Vec<u8>, Error> {
Ok(msg.value.to_be_bytes().to_vec())
}
fn decode(&self, bytes: &[u8]) -> Result<Pong, Error> {
if bytes.len() < 4 {
return Err(Error::from("Pong decode: not enough bytes"));
}
Ok(Pong {
value: u32::from_be_bytes(bytes[0..4].try_into().unwrap()),
})
}
}
// ---------------------------------------------------------------------------
// Message types
// ---------------------------------------------------------------------------
#[derive(Clone, Debug)]
struct Ping {
value: u32,
reply_to: ActorAddress,
}
impl NetworkMessage for Ping {
fn type_tag() -> &'static str {
"test::Ping"
}
}
#[derive(Clone, Debug, PartialEq)]
struct Pong {
value: u32,
}
impl NetworkMessage for Pong {
fn type_tag() -> &'static str {
"test::Pong"
}
}
// ---------------------------------------------------------------------------
// Actor fixtures
// ---------------------------------------------------------------------------
/// Replies with Pong { value: ping.value + 1 }
struct PongActor;
impl ActorInterface for PongActor {
type Incoming = Ping;
type Response = Pong;
fn handle(&mut self, ctx: &Ctx, msg: Ping) {
let _ = ctx.send(msg.reply_to, Pong { value: msg.value + 1 });
}
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
fn build_codec_registry() -> CodecRegistry {
let mut cr = CodecRegistry::new();
cr.register::<Ping, _>(TestCodec);
cr.register::<Pong, _>(TestCodec);
cr
}
fn tick_n(rt: &Runtime, n: usize) {
for _ in 0..n {
rt.tick();
}
}
/// Drain a transport receiver and deliver all envelopes into a runtime.
fn drain_transport(
rx: &std::sync::mpsc::Receiver<WireEnvelope>,
codecs: &CodecRegistry,
rt: &Runtime,
) {
for envelope in rx.try_iter() {
let (addr, msg) = codecs.receive(envelope).unwrap();
rt.deliver_raw(addr, msg).unwrap();
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
/// Given two runtimes connected via InMemoryTransport,
/// when runtime A sends a Ping to an actor on runtime B,
/// then the actor on B receives the deserialized message and processes it.
#[test]
fn two_runtimes_communicate_via_in_memory_transport() {
let codecs = Arc::new(build_codec_registry());
// Runtime A — the sender
let mut rt_a = Runtime::new(RuntimeConfig::default());
let (transport_a_to_b, rx_b) = InMemoryTransport::pair();
let router_a = TransportRouter::new();
// Runtime B — has the PongActor
let mut rt_b = Runtime::new(RuntimeConfig::default());
let (transport_b_to_a, rx_a) = InMemoryTransport::pair();
let router_b = TransportRouter::new();
// Spawn PongActor on B, drain spawn queue
let pong_addr = rt_b.spawn(PongActor).unwrap();
tick_n(&rt_b, 1);
// Create inbox on A to receive the reply
let inbox_a = rt_a.new_inbox::<Pong>().unwrap();
let inbox_addr = *inbox_a.addr();
// Register routes: A knows pong_addr is remote (via transport to B)
router_a.add_route(pong_addr, transport_a_to_b);
// B knows inbox_addr is remote (via transport to A)
router_b.add_route(inbox_addr, transport_b_to_a);
rt_a.set_codec_registry(codecs.clone());
rt_a.set_transport_router(Arc::new(router_a));
rt_b.set_codec_registry(codecs.clone());
rt_b.set_transport_router(Arc::new(router_b));
// A sends Ping to pong_addr — this goes via transport
rt_a.send_to(pong_addr, Ping { value: 42, reply_to: inbox_addr })
.unwrap();
// Deliver from A→B transport, tick B to process
drain_transport(&rx_b, &codecs, &rt_b);
tick_n(&rt_b, 1);
// Deliver reply from B→A transport
drain_transport(&rx_a, &codecs, &rt_a);
// A's inbox should have the Pong reply
let pong = inbox_a.try_recv().expect("should have received Pong");
assert_eq!(pong, Pong { value: 43 });
}
/// Given a transport route exists but the message type is not registered,
/// when sending to that address,
/// then the error mentions "not registered".
#[test]
fn unregistered_type_produces_clear_error() {
// Registry with NO types registered
let codecs = Arc::new(CodecRegistry::new());
let (transport, _rx) = InMemoryTransport::pair();
let router = TransportRouter::new();
let fake_addr = ActorAddress::new_random();
router.add_route(fake_addr, transport);
let mut rt = Runtime::new(RuntimeConfig::default());
rt.set_codec_registry(codecs);
rt.set_transport_router(Arc::new(router));
let result = rt.send_to(fake_addr, Ping {
value: 1,
reply_to: ActorAddress::default(),
});
let err_msg = format!("{:?}", result.unwrap_err());
assert!(
err_msg.contains("not registered"),
"Expected 'not registered' in error, got: {err_msg}"
);
}
/// Given a WireEnvelope arrives with a type_tag not in the codec registry,
/// when CodecRegistry receives it,
/// then the error mentions "unknown type_tag".
#[test]
fn unknown_type_tag_on_receive_produces_clear_error() {
let codecs = CodecRegistry::new(); // empty registry
let envelope = WireEnvelope {
dest: ActorAddress::default(),
type_tag: "nonexistent::Type".to_string(),
payload: vec![1, 2, 3],
};
let result = codecs.receive(envelope);
let err_msg = format!("{:?}", result.unwrap_err());
assert!(
err_msg.contains("unknown type_tag"),
"Expected 'unknown type_tag' in error, got: {err_msg}"
);
}
/// Given both local actors and transport routes exist,
/// when an actor sends to another local actor,
/// then the message is delivered locally (no serialization, transport never called).
#[test]
fn local_send_still_bypasses_transport() {
let codecs = Arc::new(build_codec_registry());
let (transport, rx) = InMemoryTransport::pair();
let router = TransportRouter::new();
// Register a bogus remote route for a random address
let remote_addr = ActorAddress::new_random();
router.add_route(remote_addr, transport);
let mut rt = Runtime::new(RuntimeConfig::default());
rt.set_codec_registry(codecs);
rt.set_transport_router(Arc::new(router));
// Spawn a local PongActor + inbox
let pong_addr = rt.spawn(PongActor).unwrap();
let inbox = rt.new_inbox::<Pong>().unwrap();
// Send locally — should NOT go through transport
rt.send_to(pong_addr, Ping {
value: 10,
reply_to: *inbox.addr(),
})
.unwrap();
tick_n(&rt, 2);
// Verify local delivery worked
let pong = inbox.try_recv().expect("should receive Pong locally");
assert_eq!(pong, Pong { value: 11 });
// Verify transport was never used
assert!(
rx.try_recv().is_err(),
"Transport should not have received any envelope"
);
}
/// Full round-trip: actor on A sends to B, actor on B replies back to A.
/// Both directions go through transports.
#[test]
fn round_trip_across_two_runtimes() {
let codecs = Arc::new(build_codec_registry());
// Set up two runtimes with bidirectional transports
let mut rt_a = Runtime::new(RuntimeConfig::default());
let mut rt_b = Runtime::new(RuntimeConfig::default());
let (transport_a2b, rx_b) = InMemoryTransport::pair();
let (transport_b2a, rx_a) = InMemoryTransport::pair();
let router_a = TransportRouter::new();
let router_b = TransportRouter::new();
// Spawn actors
let pong_addr = rt_b.spawn(PongActor).unwrap();
tick_n(&rt_b, 1);
let inbox_a = rt_a.new_inbox::<Pong>().unwrap();
let inbox_addr = *inbox_a.addr();
// Wire routes
router_a.add_route(pong_addr, transport_a2b);
router_b.add_route(inbox_addr, transport_b2a);
rt_a.set_codec_registry(codecs.clone());
rt_a.set_transport_router(Arc::new(router_a));
rt_b.set_codec_registry(codecs.clone());
rt_b.set_transport_router(Arc::new(router_b));
// Send 3 pings and verify 3 pongs come back
for i in 0..3u32 {
rt_a.send_to(pong_addr, Ping { value: i * 10, reply_to: inbox_addr })
.unwrap();
}
// Flush A→B
drain_transport(&rx_b, &codecs, &rt_b);
tick_n(&rt_b, 1);
// Flush B→A
drain_transport(&rx_a, &codecs, &rt_a);
// Verify all 3 replies
for i in 0..3u32 {
let pong = inbox_a.try_recv().expect(&format!("missing pong #{i}"));
assert_eq!(pong, Pong { value: i * 10 + 1 });
}
assert!(inbox_a.try_recv().is_none(), "no extra messages");
}