Repository navigation
Embedded State Foundations
NoETL is moving EHDB from a networked writer service to an embedded library called in-process per shard (noetl/ai-meta#332). This page records what that added to this repo and, for each guard, what it exists to prevent — so it is not later deleted as noise.
Nothing here is deployed. All of it is inert until the server opens a local engine, which is a separate gated step.
FORMAT_VERSION (currently 1) is written to the substrate as a marker at
FORMAT_VERSION_KEY. L0Engine::open* refuses to open a substrate whose marker
disagrees.
What it prevents. Embedding means two processes — server and worker — each
open() an engine directly, and they pin this repo by git rev: at the time
this landed they were 70 commits apart. Two builds writing one layout at that
divergence is silent corruption, and a git-rev pin has no mechanism that would
notice. Refusing is recoverable; misreading is not.
Where it runs. open_replicated_with_metrics — the chokepoint every
open* funnels through — before the manifest is read, and on every
replica (each is an independent substrate; a single-replica check waves through a
replica written by a different build).
⚠ Not manifest_version. That already existed and is a monotonic sequence
number for manifest snapshots — "which snapshot", not "which layout".
⚠ Bump FORMAT_VERSION in the same change as any incompatible layout change.
The first read_marker used get_range(key, 0, 32) with Err(_) => Ok(None).
get_range treats a range past the end as an error by design, and the marker
is 1–3 bytes — so the read failed every time, and the blanket arm turned that, a
corrupt marker, and an I/O failure into "absent" → re-initialise, writing
our marker over another build's layout. Now: exists() then get_all(), and
every other error propagates. UnreadableSubstrate in the tests is what pins it.
cursor::load(substrate, shard) / cursor::advance(substrate, shard, seq), a
durable u64 per shard at CURSOR/shard-<n>.
What it replaces. ehdb-reference's ProjectionCheckpoint derives the
cursor by replaying the whole log and taking the max. Correct and
exactly-once-safe, but recovery pays an O(log length) scan before it can start
replaying the tail.
Recovery is now cursor::load + L0Engine::read_partition_after(shard, seq).
Measured, not asserted: 20 records with the cursor at 15 replays 5.
Invariants:
- ⚠ Monotonic. A lower
advanceis ignored, not written. A backwards cursor re-applies an applied window; a forwards one past what was applied skips events, which is worse. -
An absent cursor reads as
0and replays everything — losing the pointer costs time, never correctness. That asymmetry is what makes storing it safe. - ⚠ An unreadable cursor refuses, rather than reading as
0. Reading as 0 is safe here, but it would hide a substrate failure.
Both ProjectionStore (D3) and RuntimeStore (D8) had zero consumers and zero
tests. They were built ahead of their consumers, so "it exists" and "it works"
were independent questions. Both now have the second answered:
D3 projection — append→read-latest; list_executions returns distinct ids;
state survives a reopen; cold_load recovers another instance's durable
state; the manifest snapshot is storage layout, not the read model.
D8 runtime — register/heartbeat/deregister fold latest-op-wins; a heartbeat
for a never-registered or deregistered node returns None and does not
resurrect it; list_live_since evicts by watermark with no wall clock, and a
quiet node stays registered rather than being confused with a departed one;
membership survives a reopen; cold_load reads another instance's membership;
and the op log retains all three events after the current state has forgotten the
node — the audit property a heartbeat column cannot give.
⚠ proj_seq is engine-assigned from a global sequence — not caller-supplied
and not per-execution.
What it prevents. D8 previously appended whatever it was handed, for any id. Membership is persisted, so a poisoned entry does not vanish when whoever wrote it leaves — it is in the log and every fold replays it. Validating at append is the only point at which refusing keeps it out.
Structural half (validate_op, no network trust required): non-empty and
length-bounded worker_id; a restricted charset [A-Za-z0-9-_.:]; a dot-run
rejection; a bounded contract; and field/event agreement — a Register or
Heartbeat carrying heartbeat: 0 is refused, because list_live_since(1)
would then silently drop a node that is beating.
⚠ The dot-run check looks redundant and is not: dots are legal in an id
(shard.3), so ".." is the one traversal shape the charset rule does not
catch. A whitespace check that was redundant was removed — an ungated branch is
an untested branch.
Network-trust half (OpOrigin): consulted on the append path, defaulting to
TrustedLocalOrigin — named for what it assumes, not described as safe.
RuntimeStore::with_origin(..) installs a real one.
⚠ TrustedLocalOrigin accepts every structurally valid op. Correct while
every caller is in-process; it must be replaced before gossip feeds this store
from the network.
A new crate, so ehdb-l0 keeps its two dependencies and consumers opt in by
not depending on this one.
The split: foca observes, EHDB records. foca 2.0.0 (SWIM + suspicion +
indirect probing) carries transport and failure detection only; every transition
it reports is appended to D8, and all state, history, recovery and query stay in
EHDB. Chosen over chitchat because chitchat is Scuttlebutt + phi-accrual and
does not do indirect probing — the property that distinguishes "I cannot
reach X" from "X is down".
-
ShardIdentityputs the shard in the gossip identity, so D8'scontractcomes from gossip rather than being inferred.renew()bumps an incarnation so a wrongly-evicted node can out-rank the corpse its peers hold; ⚠ higher incarnation wins — inverting it means a rejoining node is permanently rejected. -
MembershipSinkmaps all 7 foca notifications. ⚠ foca is edge-triggered — no per-peer liveness tick — so D8's heartbeat watermark is advanced by a local tick over the live set, which meanslist_live_sinceguards against this adapter stalling. A different property from what gossip guards. ⚠renameregisters the new identity before dropping the old, so the node is never absent from routing mid-rename. -
GossipOrigincannot be constructed without a verifier — noDefault, no permissive constructor, no relaxing feature. Default posture isRejectUnsigned: an unconfigured cluster trusts no remote membership.
Status: inert. No socket, no runtime, no bring-up.
ehdb-l0 is now consumed in-process by the NoETL server, via
tag = "v0.2.0" — the first git-tag-semver dependency on this repo, replacing
the rev pins that let two consumers drift 70 commits apart.
The server opens an L0Engine<D1EventLog> in shadow: it appends what
Postgres accepted and compares counts. It serves nothing from the engine.
What this exercises in this repo, on a real workload:
-
L0Engine::openand, with it, theFORMAT_VERSIONgate — this is the open the gate was written for; -
append_recordonD1EventLogfrom a second, non-writer process; -
LocalFsSubstrateunder a server's write rate.
Measured in kind: engine opened once, 24 batches compared, 0 divergences, 0 append failures, and the server's own dispatch unaffected.
On prod the flag is off, so the engine is not opened there yet. Arming it is a separate checkpoint.
⚠ Consequence for this repo: a breaking change to the on-disk layout now has
a real consumer. Bump FORMAT_VERSION in the same change, and cut a new tag —
the server pins tags, not revs, so it will not pick the change up silently.
- Central record: noetl/ai-meta#332
and
docs/rfc/ehdb-embedded-state.md,ehdb-packaging.md,ehdb-topology-membership.mdin ai-meta. -
Design-Projection-Read-Model-Engine— the D3 engine these tests exercise. -
RFC-Server-EHDB-Coupling-and-Storage-Substrate— the coupling question this program answers.
- Home
- Architecture
- Architecture — the four engines
- Architecture — resilient KV core
- Consistency Invariants (per tier)
- Roadmap
- Sessions Log
- Claude Handoff
- RFC: Completion Program
- RFC: External EHDB Driver
- L1 Command-Bus Cutover (T4/T5 — prepared, human-gated)
- Prod Cutover — Event-Log Tier (Phase 9, Tier 1)
- Runbook: Async Event-Log Mirror
- Durable Event-Log — Prod Durability Sign-off (§C, slice 6)