Skip to content

Commit afd56cd

Browse files
committed
docs: add pages for telemetry and the runtime modules
Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
1 parent 58a44e9 commit afd56cd

9 files changed

Lines changed: 790 additions & 2 deletions

File tree

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ stability label.
6565
| `source/cloudevents` | CloudEvents codec with structured and binary content modes. | experimental |
6666
| `source/cdc` | Change-data-capture codec: decode Debezium/OpenCDC change events, drive by key. | experimental |
6767
| `source/statemachine` | Bridge: an inbound message drives a transition, ack tied to the durable commit. | experimental |
68+
| `durable` | Durable-execution runtime: record and replay nondeterminism to survive a crash. | experimental |
69+
| `cluster` | Distribution runtime: remote actors, supervision, and live instance migration. | experimental |
70+
| `transport` | gRPC network transport for cluster: remote deliver/spawn and time-travel. | experimental |
71+
| `wasm` | Run state behaviors as WebAssembly: polyglot guards over a JSON ABI via wazero. | experimental |
6872

6973
source also ships composable reliability middleware as its own opt-in modules
7074
(`source/retry`, `source/dlq`, `source/idempotency`, `source/schema`) and an
@@ -77,8 +81,11 @@ hierarchical, parallel, and final states, history, guard combinators, delayed
7781
transitions, invoked services, an actor model, snapshots, and JSON
7882
(de)serialization, backed by its `analysis`, `evolution`, and `conformance`
7983
packages. `telemetry`, `sink`, and `source` (with all their adapters, codecs, and
80-
middleware) are released and documented; `broker` is planned. Treat every API as
81-
experimental until it reaches v1.
84+
middleware) are released and documented, as are the host-side runtimes over the
85+
kernel: `durable` (durable execution), `cluster` (distribution and live
86+
migration), `transport` (the gRPC network transport for cluster), and `wasm`
87+
(polyglot behaviors). `broker` is planned. Treat every API as experimental until
88+
it reaches v1.
8289

8390
## Roadmap
8491

docs/astro.config.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,24 @@ export default defineConfig({
7070
// external streams and drive statecharts.
7171
items: [{ autogenerate: { directory: 'source' } }],
7272
},
73+
{
74+
label: 'Telemetry',
75+
// The vendor-neutral observability seam the IO modules depend on,
76+
// plus its optional backend adapters.
77+
items: [{ autogenerate: { directory: 'telemetry' } }],
78+
},
79+
{
80+
label: 'Runtimes',
81+
// Host-side runtimes layered additively over the state kernel:
82+
// durable execution, distribution, the gRPC transport, and polyglot
83+
// WASM behaviors. Each leaves the kernel pure and stdlib-only.
84+
items: [
85+
{ label: 'Durable execution', items: [{ autogenerate: { directory: 'durable' } }] },
86+
{ label: 'Cluster', items: [{ autogenerate: { directory: 'cluster' } }] },
87+
{ label: 'Transport', items: [{ autogenerate: { directory: 'transport' } }] },
88+
{ label: 'WASM behaviors', items: [{ autogenerate: { directory: 'wasm' } }] },
89+
],
90+
},
7391
{
7492
label: 'State machine',
7593
items: [
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
---
2+
title: What is crucible/cluster
3+
description: A host-side distribution runtime that spreads a state machine and its actors across nodes with remote delivery, supervision, and live instance migration over a pluggable transport.
4+
sidebar:
5+
order: 1
6+
---
7+
8+
<!-- IMAGE-SLOT: cluster-overview-nodes (a sky-squid smith routing molten streams between several connected crucibles on different anvils, one casting being carried to another anvil mid-pour; ember/copper on steel) 16:9 -->
9+
10+
`crucible/cluster` is the host-side **distribution runtime** for the
11+
[`state`](/crucible/start/introduction/) kernel: remote actors, supervision, and
12+
live instance migration. `state` runs a machine and its child-machine actors in
13+
one process; cluster spreads that across nodes. A parent on one node addresses
14+
and drives an actor running on another, failures are supervised with
15+
restart/backoff strategies, and a running instance can be migrated to a different
16+
node, all over a pluggable `Transport`, with the kernel left pure and stdlib-only.
17+
18+
The runtime is **additive** over the kernel. It consumes seams the kernel already
19+
reserves (the opaque `ActorRef` whose `Node` locator names the owning host, the
20+
injectable `ActorSystem`, the `Snapshot`/`Restore` pair, and the typed
21+
`ActorEscalation`/`EscalationHandler`) and needs no kernel change beyond the
22+
additive `ActorRef.Node` locator. The core is itself stdlib-only; transport
23+
dependencies live behind the `Transport` interface, out of the core.
24+
25+
## Remote actors
26+
27+
A `System` wraps a node's local `state.ActorSystem` with a node identity and an
28+
optional `Transport`. Operations on a ref this node owns are delegated locally;
29+
operations on a ref another node owns are routed over the transport.
30+
31+
```go
32+
tr := cluster.NewInMemoryTransport()
33+
34+
nodeA := cluster.NewSystem("node-a", actorSysA, cluster.WithTransport(tr))
35+
nodeB := cluster.NewSystem("node-b", actorSysB, cluster.WithTransport(tr))
36+
tr.Register("node-a", nodeA)
37+
tr.Register("node-b", nodeB)
38+
39+
// Spawn a worker on node-b from node-a, then drive it through the returned ref.
40+
ref, err := nodeA.Spawn(ctx, "node-b", "worker", "w-1", nil)
41+
_, err = nodeA.Deliver(ctx, ref, "start") // routed to node-b
42+
```
43+
44+
The ref is opaque: the holder never learns where the actor runs. A ref this node
45+
owns has an empty `Node`; a remote ref carries the owning node. A `System` with no
46+
`Transport` serves its local actors transparently and reports `ErrNoTransport` for
47+
a remote ref. The in-tree `InMemoryTransport` connects node-scoped systems in one
48+
process; a real network transport (see [`transport`](/crucible/transport/overview/))
49+
implements the same `Transport` interface.
50+
51+
## Supervision
52+
53+
A `Supervisor` turns the kernel's typed `ActorEscalation` into a per-source policy.
54+
Wire it with `ActorSystem.WithEscalationHandler(sup.Handle)`; each failed actor is
55+
routed to a `Decision` by the src it was spawned from:
56+
57+
| Decision | Behavior |
58+
| --- | --- |
59+
| `Escalate` | forward the failure to a sink up the hierarchy (the default) |
60+
| `Stop` | contain the failure at this level |
61+
| `Restart` | re-spawn through a `Respawner` (the `System`), bounded by a per-src budget; on exhaustion, escalate |
62+
| `Backoff` | defer the re-spawn behind an exponentially growing delay; the host applies due restarts via `Tick` |
63+
64+
```go
65+
sup := cluster.NewSupervisor(
66+
cluster.WithRestart("worker", 3), // up to 3 immediate restarts
67+
cluster.WithBackoff("flaky", 5, 100*time.Millisecond, time.Minute, 2.0),
68+
cluster.WithEscalationSink(parentHandler),
69+
)
70+
sup.SetRespawner(node)
71+
actorSys.WithEscalationHandler(sup.Handle)
72+
// ... drive backoff restarts from a timer loop:
73+
for range ticker.C { sup.Tick(ctx) }
74+
```
75+
76+
Backoff reads time through an injected `state.Clock` (`WithClock`, default the
77+
system clock), so it is deterministic under a `state.FakeClock` in tests.
78+
79+
## Live migration
80+
81+
`Capture` snapshots a running instance, its actor tree, and its machine definition
82+
into a wire-shippable `Checkpoint`; `Restore` rebuilds it on another node, resuming
83+
in place. The move is **gated on schema compatibility**. `Restore` diffs the source
84+
and target machine definitions with
85+
[`state/evolution`](/crucible/analysis/evolution/) and refuses a breaking target
86+
with `ErrIncompatibleMigration`, so an instance never resumes against a definition
87+
that would misread its state.
88+
89+
```go
90+
cp, err := cluster.Capture(inst, actorSys, machine) // on the source node
91+
// ... ship cp (it is all JSON) to the target node ...
92+
inst, sys, err := cluster.Restore(ctx, cp, targetMachine, // on the target node
93+
cluster.WithActorBehaviors(palette))
94+
// err is ErrIncompatibleMigration if targetMachine is a breaking change.
95+
```
96+
97+
## Where it fits
98+
99+
cluster is feature-complete on the in-memory transport, which is enough to build
100+
and test a distributed topology in one process. To carry deliveries, spawns, and
101+
time-travel queries between real nodes over the network, pair it with
102+
[`transport`](/crucible/transport/overview/), the gRPC implementation of the same
103+
`Transport` seam.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
---
2+
title: What is crucible/durable
3+
description: A host-side durable-execution runtime that records every nondeterministic value a state instance consumes and replays them to rebuild it byte-identically after a crash.
4+
sidebar:
5+
order: 1
6+
---
7+
8+
<!-- IMAGE-SLOT: durable-overview-replay (a sky-squid smith re-pouring a recorded molten stream back into the same mold so the casting comes out identical after the forge was relit; ember/copper on steel) 16:9 -->
9+
10+
`crucible/durable` is the host-side **durable-execution runtime** for the
11+
[`state`](/crucible/start/introduction/) kernel. `state` is a pure statechart
12+
engine: firing an event is a deterministic function of the instance's recorded
13+
inputs. durable makes a running instance survive a process crash by recording
14+
every nondeterministic value the instance consumes (clock readings,
15+
invoked-service results, actor outcomes) and persisting each step before it is
16+
acknowledged. Recovery **replays** those recorded values back through the same
17+
pure transition function, so a recovered instance reaches exactly the
18+
configuration, context, and history of a run that never crashed, without
19+
re-invoking any external source.
20+
21+
The runtime is **additive** over the kernel. It consumes seams the kernel already
22+
reserves (`Snapshot.Journal`, the `EffectEnvelope.EffectID` correlation slot, and
23+
the injectable `Clock` / `ServiceRunner` / `ActorSystem` drivers) and requires no
24+
change to the kernel, which stays pure and stdlib-only. Heavy dependencies
25+
(database drivers, cloud SDKs) never enter this module: persistent backends
26+
implement the `Store` interface out of tree.
27+
28+
## Guarantees
29+
30+
- **Deterministic replay.** A recovered instance is byte-identical to one that
31+
never crashed, because recovery replays the recorded driving events and
32+
nondeterministic results rather than re-executing their sources.
33+
- **Exactly-once effects.** A domain effect is applied exactly once over the
34+
instance's lifetime (the live run plus any number of recoveries), even though
35+
the replay loop is at-least-once. Each effect carries a deterministic
36+
`EffectID` and is deduplicated through the `Store`'s dispatch set.
37+
- **Durability across restart.** Every `Fire` step is write-ahead appended to the
38+
`Store` before it is acknowledged, so a crash after a successful `Fire` never
39+
loses the step. Periodic checkpoints bound the tail that recovery replays.
40+
41+
## The shape of it
42+
43+
Wire a `Runner` around a machine and a `Store`. `Start` creates a fresh durable
44+
instance; `Fire` drives it; `Recover` rebuilds it from the `Store` after a crash.
45+
46+
```go
47+
runner := durable.NewRunner(machine, durable.NewMemStore())
48+
49+
// Start a fresh instance: persists a baseline checkpoint.
50+
h, err := runner.Start(ctx, "order-42", OrderInput{ /* ... */ })
51+
52+
// Drive it. Each Fire write-ahead appends a Record before acknowledging.
53+
_, err = h.Fire(ctx, "submit")
54+
55+
// ...process crashes, comes back up...
56+
57+
// Recover purely from the Store: load the latest checkpoint, replay the tail.
58+
h, err = durable.Recover(ctx, machine, store, "order-42")
59+
_, err = h.Fire(ctx, "confirm") // continues recording from the live tip
60+
```
61+
62+
For a hot path firing many events in sequence, keep the `Handle` from `Start` or
63+
`Recover` and call `Handle.Fire` directly to avoid a `Store` round-trip per step.
64+
For a stateless handler that fires a single event per request, `Runner.Fire`
65+
loads, replays, fires, and re-records in one call.
66+
67+
## The seams
68+
69+
Each source of nondeterminism is isolated behind an injectable driver, recorded
70+
the first time, and replayed verbatim on recovery:
71+
72+
| Seam | Wire with | Recorded as |
73+
| --- | --- | --- |
74+
| **Clock** (timers) | `WithRunnerClock` | `JournalClockRead`, replayed so timers fire at the same recorded instants, wall-clock-independent; armed deadlines survive checkpoint compaction |
75+
| **Invoked services** | `WithServiceRegistry` + `Handle.RunService` | `JournalServiceResult`; the service runs once, then recovery replays its result through the kernel's settle seam |
76+
| **Child-machine actors** | `WithActorPalette` + `Handle.DeliverToActor` | `JournalActorMessage`; the behavior runs once, then recovery re-fires the recorded parent transition |
77+
| **Domain effects** | `WithEffectHandler` | dispatch set, applied exactly once via deterministic `EffectID` dedup |
78+
79+
Use `WithCheckpointEvery(n)` to tune how often a full snapshot is written: a
80+
shorter interval bounds recovery replay, a longer one cuts checkpoint cost.
81+
82+
## Stores
83+
84+
`Store` is the persistence seam. A durable instance is an ordered log of
85+
`Record`s (one per `Fire` step) layered over periodic full-snapshot checkpoints.
86+
Two stdlib-only reference implementations ship in-tree:
87+
88+
- **`MemStore`** (`NewMemStore`): in-memory, thread-safe, not durable across
89+
restarts. For tests, examples, and single-process development. `WithHistory`
90+
retains the full record history, enabling time-travel below.
91+
- **`FileStore`** (`NewFileStore`): on-disk, a directory of per-instance
92+
subdirectories, each an append-only journal, an atomic checkpoint, an
93+
idempotency ledger, and a dispatched-effect log. Each append flushes to stable
94+
storage; each checkpoint uses write-temp-plus-rename for crash-safe atomicity.
95+
Use it for durability across restarts without a database.
96+
97+
Persistent database backends (PostgreSQL, DynamoDB, and the like) implement
98+
`Store` out of tree, so their drivers never burden this module's dependency or
99+
vulnerability surface.
100+
101+
## Time-travel reader
102+
103+
`StateAt` reconstructs an instance's state as of any recorded step, read-only:
104+
restoring the start baseline and replaying recorded events forward to the target
105+
step, running no service, re-instantiating no actor, reading no wall clock, and
106+
dispatching no effect:
107+
108+
```go
109+
view, err := durable.StateAt(ctx, machine, store, "order-42", 3)
110+
// view.Snapshot(), view.Instance(), view.Step(): detached and safe to read
111+
```
112+
113+
Time-travel needs the full record history through the target step. A `Store` opts
114+
in by implementing `HistoryStore` (the in-tree `MemStore` does so under
115+
`WithHistory`); `StateAt` otherwise falls back to the latest checkpoint plus tail.
116+
117+
## A note on serialized payloads
118+
119+
Events, service done-data, actor done-data, and actor messages are recorded as
120+
their JSON form. A parent reducer that type-asserts a non-JSON Go type from
121+
`AssignCtx.Event` observes the JSON-decoded shape on a replayed `onDone`. A
122+
typed-codec option to carry the concrete Go value across the journal boundary is
123+
reserved for a later, additive change.

0 commit comments

Comments
 (0)