Repository navigation
Architecture Resilient KV Core
Status: draft overview · Spec artifact for the resilient-core programme · Coordination issue noetl/ai-meta#339
Grounded in noetl/ehdb@24e63b8 (tag v0.2.0, the tag noetl-server pins)
and prod readings taken 2026-09-11.
CockroachDB stacks five layers: storage (MVCC KV) → replication (Raft) → distribution (ranges) → transactions → SQL. The resilient KV core is layers 5→2; SQL is Postgres-compatibility cost. noetl does not need it: its access patterns are append-event, fold-projection, get/scan-by-key, range-by-sequence. None require a parser, planner or optimiser.
So EHDB's target is the resilient KV core, with event sourcing, projections and context handling directly on top, and no SQL engine — permanently.
event log (D1) projections (D3) context (D4 KV)
────────────────────────────────────────────────────
append fold get/put
┌──────────────────────────────────────────────────┐
│ resilient KV core │
│ • compare-and-append at expected sequence │ L2, narrowed
│ • quorum-acked unsealed tail ← THE GAP │ L4
│ • static hash partitioning + gossip membership │ L3, adequate
│ • immutable parts, N-way copy, cold-load │ L4, sealed: done
│ • manifest + sparse index + bloom + merge + GC │ L5, done
└──────────────────────────────────────────────────┘
NO SQL LAYER
⚠ Scope note that changes how this reads: noetl-server depends on
ehdb-l0 + ehdb-feed only. ehdb-reference (23,949 LOC), ehdb-service
and ehdb-transaction are not in the production dependency path. Several
capabilities below exist in the tree and are not linked into the running binary.
Immutable parts + ClickHouse-style meta-catalog (Manifest / PartMeta),
per-part SparseIndex, min/max pruning, per-granule blooms over execution_id,
small→big merge with atomic manifest swap, retention as drop-partition + orphan
GC, a columnar per-field codec, and fsync-per-append for D1.
Not an MVCC KV, and should not become one. The version axis is
global_sequence. Mutable state is modeled as a fold over an append-only op log
(D2 command_queue already does this). For event sourcing that is a better fit
than timestamps.
| Piece | State |
|---|---|
| N-way copy of sealed parts |
Implemented (L0.6): ReplicaTarget, PartMeta::replicas, read fallback |
| Quorum on the write path |
Missing — an append acks after fsync to one local disk |
| The unsealed tail | Unreplicated by construction — a part replicates only once sealed |
| Bounding the window | ✅ Live: NOETL_EHDB_SEAL_MAX_AGE_MS=5000 on the writer |
| Measuring the window | ✅ Live on both engines |
| Independent failure domain | ❌ Absent in prod — tier dir nested in the writer dir, one PVC |
open_replicated in prod |
❌ Never called outside tests |
validate_replica_domains |
❌ Never called outside tests — the guard cannot fire |
| Single-writer enforcement | ❌ Rests on StatefulSet replicas: 1
|
| Election / fencing | Implemented in ehdb-reference; not linked into the server, fencing shadow-mode |
| Leaseholder reads | Absent — no concept |
Prod readings, 2026-09-11:
writer :9106 (event bus, the D1 tier that is primary)
ehdb_l0_unreplicated_age_seconds{shard="0"} 0.000
ehdb_l0_unreplicated_records{shard="0"} 0
ehdb_l0_replicated_lag_seconds_count 383
writer :9102 (command bus)
ehdb_l0_unreplicated_age_seconds{shard="0"} 3.667
ehdb_l0_unreplicated_records{shard="0"} 1
NOETL_EHDB_SEAL_MAX_AGE_MS = 5000
NOETL_EVENT_BUS_WRITER_DIR = /data/eventbus
NOETL_EHDB_TIER_SERVICE_DIR = /data/eventbus/ehdb-tier ← same PVC
So the window is measured and bounded (~5s + one sweep interval), and 383
parts have genuinely replicated. What it replicates to is a subdirectory of
the volume holding the original. failure_domain.rs states the consequence
plainly:
An RF of N over one domain is an RF of 1 wearing a larger number.
Static hash partitioning (shard_for(execution_id)), an affinity layer, foca
gossip membership (Scuttlebutt + phi-accrual), D8 RuntimeDataset as the
topology projection. Missing: key ranges, auto-split/merge, rebalance.
Ranges are largely SQL-layer tax. Cockroach needs them because a SQL keyspace is unbounded and arbitrarily skewed. noetl's key is a snowflake — uniformly distributed by construction, so it does not develop hot ranges. What EHDB actually lacks is online reshard, not splitting.
Event sourcing wants exactly one primitive: append these N events at this expected sequence, or fail — a compare-and-append, not a transaction manager. Everything downstream is a deterministic fold.
ehdb-transaction is a control-plane metadata mutation log
(catalog/stream/retrieval/system/storage), used by ehdb-reference and
ehdb-service — not a data-plane engine, and not linked into the server.
The cross-dataset case (append a D1 event and update the D3 projection atomically) is best solved by not needing it: the projection is derived, so it may lag. That is the serve-on-behind design.
Removing it also removes the reason for most of L2 and much of L3.
ehdb-l0/src/lib.rs §2.7 already decided: N-way copy of immutable parts, HDFS
block-replication model, no consensus / no Raft, justified by immutability —
byte-identical copies cannot conflict. That argument is correct and this
programme keeps it. Raft over sealed parts would add cost and no guarantee.
The argument covers sealed parts. Between ack and seal there is no immutable object yet — only one node's opinion. So the question is not "Raft or not" but what is the smallest thing that needs agreement: the tail, and who owns it.
A small per-shard Raft group over the unsealed tail only.
- Makes the ack survivable — the one missing property.
- The log stays tiny: seal truncates it, so snapshot transfer and compaction mostly vanish. The sealed part is the snapshot; cold-load is the restore.
- Solves single-writer properly. The leader is the writer and its term is a
consensus-backed epoch — exactly what Invariant F's fencing needs.
⚠
election.rsalready warns: "a Lease elects; it does not fence", and a paused holder can believe it still holds an expired lease. - Self-sufficient: a crate, not a service to operate.
Rejected — full Raft-replicated ranges (Cockroach-style). Re-solves sealed-part replication that immutability already solves more cheaply, and drags in range/split/rebalance machinery whose purpose is a SQL keyspace.
Library: open question. openraft (tokio-native, storage behind a trait —
matches EHDB's shape) vs tikv/raft-rs (consensus module only, battle-tested in
TiKV). ⚠ Decide against a partition + clock-skew harness, not documentation:
consensus bugs fail silently, which is this codebase's most expensive failure
mode.
⚠ Corrected against prod rather than assumed. Two phases the original plan listed as work are already shipped — the recurring "built ahead of its consumers" pattern.
| Phase | What | State |
|---|---|---|
| 0 | recovery→tier (#307); D1 mirror at the server chokepoint; embedded engine in shadow; D3 serve-on-behind; AC14 verification independence live as v3.108.1 | ✅ shipped |
| 1 | Measure the durability window | ✅ already live — both engines export it |
| 2 | Bound the window | ✅ already live — SEAL_MAX_AGE_MS=5000
|
| 3a | Make the failure-domain guard able to fire (validate_replica_domains wired into open_replicated, shadow-first) |
✅ noetl/ehdb#359 — also found the Arc forwarding impl dropped failure_domain
|
| 3b | Give the tier its own failure domain | ⏸ deferred by decision — refill after the mirror is clean, not migrate (Plan) |
| 3c | Stop the mirror losing events | ⚠ partly — durable repair live+armed (v3.108.3), but delivery still fails (ai-meta#343) |
| 4 | Enforce single-writer: link election + fencing into the server's dependency path, promote fencing shadow → enforce | ⏳ |
| 5 | Decide the Raft library, then quorum-ack the tail | ⏳ |
| 6 | Online reshard | ⏳ |
Phase 3 is the first with real risk reduction: the replication API existed and nothing in production called it.
The tier was diverging from Postgres on 78% of recent executions. Two causes, and one was the measurement itself:
| cause | share | status |
|---|---|---|
(b) reference vs inlined context — the verification folded the raw Postgres row under PERMANENT_LOG_LEAN, digesting a pointer against content |
30% | ✅ fixed (v3.108.2 serve path, v3.108.3 diagnostic) |
| (a) real missing events — the mirror dropped batches terminally after ~63 s against a single-endpoint relay, and the repair endpoint had no caller | 48% | ⚠ mechanism fixed + armed, delivery still failing |
Parity proof for (b), same fixed 40 executions across the v3.108.3 deploy:
before: agree=10 divergent=30 (15 equal-count / 15 missing-events)
after : agree=26 divergent=14 newly agreeing 16, regressed 0
⚠⚠ The first attempt at this proof failed informatively. v3.108.2 fixed the
serve path and the divergence count moved by 0 of 40, because the endpoint
being measured (compare_sources) was never on the path that changed.
An instrument that cannot see a fix cannot validate one — it is now held to
the same rule, with a guard.
⚠ The remaining 14 are real loss and do not close yet: re-mirrored rows fail
at the transport (attempt{unavailable} ~47/min while the relay pod is up),
and an execution whose tier payload exceeds 1 MiB cannot be read back at all,
so it leaves the denominator rather than reporting as divergent
(ai-meta#343).
-
unreplicated_age_seconds = 0.000is a real reading, not an absence — the shard has nothing pending. Treat absent series and zero differently. - ⚠
ehdb_l0_ingest_append_failedis absent on :9106, not zero. A build predating that counter serves no such series, and that reads exactly like a healthy one. - ⚠ The writer hosts two L0 engines (:9102 cmdbus, :9106 event bus). Instrumenting or reading one is not the other — a grep finds one wiring and stops.
- Spec (cross-repo context):
specs/active/2026-09-11-ehdb-resilient-core - Architecture · Architecture: Four Engines · Consistency Invariants
- Embedded State Foundations
- 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)