- No sibling (alkstore) dependency in-core; reactivity is deployment-layer composition above the facade — cadence stays embedder-owned (ADR-005), with alkstore the named substrate for it; in-crate notify/scheduler/queues/invalidation hooks are non-goals. - Fleet sweeper lock pinned as 'named exclusive sweep lease' semantics; shipped provider unchanged (pg advisory clause); provider swaps are ADR-007-shaped substitution doors. - Composition contract for downstream ADRs: co-tenancy of CAS/GC state + reactive machinery + consumer/liveness tables on one fleet host; no-cross-store-atomicity ruled; compensation rests on idempotent re-puts, windowed deletes, sweeps-as-report-data. - OQ-11 resolved (the register's one live circular hedge): engine features pull their own driver clusters — sqlite becomes feature 'sqlite' (default-on, carries rusqlite); 'kv'/'large' tier features retired; 'local' ungated; pg-pure builds are supported, non-default. Deciding fact already in-tree (REQ-2/ADR-009). - requirements.md: UC-4/REQ-5 records the composition facts (operator record 2026-10-11, corroborated by alkstore's consumer-inventory). - Publish guard stated pre-code: facade ships before publish; semver-is-contract from first publish (alkstore ADR-017 discipline). - Amended accepted ADRs' feature wording (ADR-010 §1, ADR-011 §3/§4, ADR-003) with supersession notes; README/overview/open-questions/ backends/store-api/gc aligned; parked index now empty. Verification: docs-only round (no Rust surface touched); full-tree single-value check on the feature graph and sweep-lease wording.
242 lines
12 KiB
Markdown
242 lines
12 KiB
Markdown
# ADR-013: Family composition — the reactive boundary, the sweep lease, and the feature-graph resolution
|
|
|
|
## Status
|
|
|
|
Accepted (2026-10-11)
|
|
|
|
## Context
|
|
|
|
While this crate paused mid-pre-decomposition (ADR-012 was its last
|
|
round), the sibling **alkstore** shipped v0.1.0: one reactive
|
|
coordination contract (`Store` — notify/listen, named TTL locks,
|
|
durable queues, streams with per-consumer offsets, outbox,
|
|
leader-elected scheduler) behind engine crates (`-mem`, `-sqlite`,
|
|
`-postgres`), contract-pinned by a factory-driven suite. Its
|
|
consumer-inventory — built from the paused consumers' written
|
|
documents under this crate's own deferral discipline — grades three
|
|
needs of *this crate's specced machinery*:
|
|
|
|
- the fleet sweeper lock → named locks, **pinned**;
|
|
- maintenance cadence (sweep scheduling, pin-renewal workers) →
|
|
scheduler/queues, **documented** — against ADR-005's "embedder owns
|
|
cadence";
|
|
- fleet invalidation ("sweep/manifest changed" among fleet instances)
|
|
→ notify, **documented**.
|
|
|
|
The first consumers (alkgit, alkfs) will deploy both crates together;
|
|
composition facts are therefore inter-crate decisions that must be
|
|
decided on evidence in hand, not parked on deployments that cannot
|
|
pre-exist this crate (the Schrödinger's-code rule; the OQ register's
|
|
OQ-09/OQ-10 precedents). The audit round that produced this ADR also
|
|
found one live circular hedge in the register: OQ-11 parked the
|
|
pg-only kv feature graph on "a named deployment requirement arriving
|
|
from outside this crate" — a deciding fact *this tree already
|
|
contains* (REQ-2's consolidation posture, which ADR-009 shipped as
|
|
"a fleet node can consolidate on postgres-only"), the same
|
|
shape-shifter OQ-09/OQ-10 were rejected for, and the last one in the
|
|
register.
|
|
|
|
Additionally, the alkcall failure mode ("published doc surface
|
|
promising unimplemented behavior, surviving versions") motivates
|
|
stating this crate's publish discipline before anything ships.
|
|
|
|
## Decision
|
|
|
|
### 1. The reactive boundary: no sibling dependency in-core; reactivity is deployment-layer composition
|
|
|
|
**The core crate gains no alkstore dependency and no reactive
|
|
machinery.** No in-crate notify, no in-crate scheduler, no queues,
|
|
no invalidation hooks on any facade method. ADR-001's substrate
|
|
posture extends by the same reasoning to the reactive substrate:
|
|
the store layer ships bytes-and-addresses, and everything that makes
|
|
bytes *reactive* is composition above the facade.
|
|
|
|
- **ADR-005's "embedder owns cadence" stands, with the substrate
|
|
named.** An embedder wanting interval sweeps, pin-renewal workers
|
|
(the TTL/3 cadence, ADR-008), or maintenance dispatch composes them
|
|
above the facade — typically with the sibling substrate's
|
|
scheduler/queues. The punt was always a placement decision, not a
|
|
missing mechanism; the sibling's existence changes nothing here.
|
|
- **Sweep outcomes are data (ADR-012 §1) — with a pinned consumer.**
|
|
The report is what the embedder's reactive wiring consumes:
|
|
`SweepReport` → sibling-notify/stream events (downstream cache
|
|
invalidation), report cadence → the embedder's scheduler. No
|
|
in-crate hook exists for any of it; this crate never references
|
|
the sibling's types, channels, or names.
|
|
- The engine-driver dependency posture is likewise untouched: no
|
|
alkstore engine crate enters this crate's feature graph (see §4's
|
|
graph, which names only storage engines).
|
|
|
|
### 2. The sweep lease: semantics pinned, provider shipped
|
|
|
|
The fleet sweep single-flighter (ADR-008 §2's advisory lock, whose
|
|
state home is the engine-state seam's sweep-lock clause — ADR-010 §2,
|
|
ADR-012 §5) is hereby pinned as a **named exclusive sweep lease**:
|
|
|
|
- at most one sweep in flight per pool;
|
|
- acquisition is an atomic test-and-set at the fleet host;
|
|
- holdership ends at explicit release, owner-process death, or the
|
|
underlying engine session's loss;
|
|
- contention is the `GcAbortCause::SweeperLock` report abort with
|
|
nothing deleted (ADR-012 §1 — report data, never an error).
|
|
|
|
These are exactly pg advisory-lock semantics — the shipped provider
|
|
(ADR-008's mechanism, unchanged; a lease whose life is the sweep's
|
|
own) — and exactly what a named TTL lock provides when its lease
|
|
spans the sweep run. Pinning the *semantics* is what makes the
|
|
provider substitutable without a contract change: if the family ever
|
|
wants the in-crate provider swapped (e.g. onto the sibling's lock),
|
|
that is the ADR-007-shaped substitution door — a new ADR with its own
|
|
evidence, not an open question.
|
|
|
|
A deployment may *additionally* serialize its maintenance above this
|
|
crate: an embedder holding a named lease on the sibling substrate
|
|
before invoking `sweep()` composes one discipline over its own
|
|
scheduled tasks (sweeps, expiry reaping, renewal workers). This is
|
|
composition above the facade — it changes no contract and is not
|
|
required for fleet correctness, because the in-crate advisory clause
|
|
is this crate's own guarantee regardless.
|
|
|
|
### 3. Co-tenancy and the no-cross-store-atomicity ruling (the downstream-facing composition contract)
|
|
|
|
A fleet pg instance hosts, as co-tenants: this crate's CAS rows and
|
|
GC state (its pool; `gc_pins` / `gc_window` — ADR-012 §4); the
|
|
sibling substrate's reactive machinery; consumer business tables
|
|
(refs, manifests, path trees — outside this crate per ADR-004); and
|
|
the consumer liveness tables this crate's fleet arbitration
|
|
consults as named table references (ADR-012 §6.6). **No crate owns
|
|
another's tables; co-tenancy is a named deployment statement** — the
|
|
posture decisions/007 and ADR-008 §2 already assumed, now citable so
|
|
downstream ADRs (alkgit's refs, alkfs's tree) reference it rather
|
|
than re-derive it.
|
|
|
|
- **Cross-store atomicity is unachievable by construction** — each
|
|
crate owns its own transaction scope, and no seam bridges them.
|
|
The family posture downstream is the outbox/compensation pattern
|
|
(alkstore's `outbox`/`notify_tx` are its substrate), and this
|
|
crate's load-bearing properties for that pattern are already
|
|
guaranteed and stay so: **puts are idempotent under CAS**
|
|
(delete-then-recover is a byte-identical re-put, ADR-005),
|
|
**deletes are windowed and re-arbitrated before execution**
|
|
(ADR-005; ADR-012 §6.7), and **sweep outcomes are data** (ADR-012
|
|
§1). Nothing downstream may rely on cross-store-atomic anything;
|
|
nothing here relies on it either.
|
|
- **Fleet liveness registration's two forms are the
|
|
downstream-facing seam:** the fleet form — a named liveness table
|
|
co-tenant on the fleet host (ADR-012 §6.6) — and the single-node
|
|
form — the in-process callback (ADR-005). Every downstream crate
|
|
plans against both. The requirement to keep a consumer's liveness
|
|
protection expressible on the fleet host is a *composition
|
|
constraint* on downstream schema placement: a consumer whose
|
|
liveness lives on a non-co-tenant engine participates in fleet GC
|
|
via pins (put-path) only, and its sweep-time liveness is expressed
|
|
by materializing rows per ADR-008 §2's materialization rule.
|
|
|
|
### 4. OQ-11 resolved: engine features pull driver clusters independently — including sqlite
|
|
|
|
**Features gate engines; tiers are constructor composition, not
|
|
features.** The ADR-010 §1 discipline ("engine features pull the
|
|
driver cluster independently" — pg-lo does not imply postgres)
|
|
extends to every engine:
|
|
|
|
| Engine | Feature | Default | Pulls |
|
|
|---|---|---|---|
|
|
| kv `sqlite` | `sqlite` | **on** | `rusqlite` |
|
|
| kv `postgres` | `postgres` | off | `tokio-postgres` + `deadpool-postgres` |
|
|
| kv `mem` | `mem` | off | nothing |
|
|
| large `local` | *(ungated)* | always present | nothing |
|
|
| large `pg-lo` | `pg-lo` | off | the shared pg driver cluster |
|
|
|
|
- The base `kv` feature — which existed because sqlite was the kv
|
|
tier's only shippable driver carrier and so pulled `rusqlite` under
|
|
the tier's name — is retired, as is the `large` tier feature
|
|
(nothing was ever behind it; `local` is ungated because it pulls
|
|
nothing). ADR-011 §3's `feature fs` → `feature large` rename line
|
|
is superseded by this table; ADR-003's "tiers default-on" wording
|
|
now reads as constructor-presence, not feature claims.
|
|
- **A pg-pure build (`sqlite` off — kv=postgres, and `large`=pg-lo if
|
|
wanted) is a supported configuration this crate does not default
|
|
to.** This resolves OQ-11 on evidence in hand: REQ-2 (operator
|
|
authority, 2026-10-03) is the deployment requirement the parked row
|
|
awaited — its consolidation posture (ADR-009's postgres-only fleet
|
|
node) is the pg-pure node, and parking the feature graph on it was
|
|
circular (a deployment requirement whose name the tree already
|
|
carries is not an absent fact). Mechanism unchanged from the
|
|
precedent: engine features pull their own drivers, so sqlite's
|
|
absence is one feature toggle, never a graph surgery.
|
|
- **Default builds are unchanged**: `sqlite` default-on keeps the
|
|
batteries-included default (the lean-base principle constrains
|
|
default builds, which still pull only tokio/thiserror/parking_lot
|
|
plus the default engine's driver).
|
|
|
|
### 5. Publish guard (stated pre-code, on the alkcall lesson)
|
|
|
|
The crate publishes only after its facade exists and passes the
|
|
invariants' test gate. From first publish, its semver is the contract
|
|
version — every published surface is implemented, every contract
|
|
change cites its ADR — the discipline the sibling substrate pinned in
|
|
its contract-versioning ADR. No published doc surface ever carries a
|
|
"planned/not-implemented" promise.
|
|
|
|
## Consequences
|
|
|
|
**Positive**
|
|
|
|
- The composition contract exists as a named ADR: alkgit/alkfs cite
|
|
§3 for schema co-tenancy, the two-form liveness seam, and the
|
|
no-cross-store-atomicity posture, instead of re-deriving or
|
|
guessing.
|
|
- The register's one live circular hedge (OQ-11) is resolved on
|
|
in-tree evidence, pre-consumer, where the fix is cheapest — the
|
|
alkcall-shaped outcome this round exists to avoid.
|
|
- A new fleet node's dependency tree is honest: pg-pure nodes do not
|
|
compile rusqlite; nothing reactive is dead code in the core.
|
|
- The sweep lease's semantics and provider are single-valued; a
|
|
provider swap later is a mechanism ADR, not a contract break.
|
|
|
|
**Negative**
|
|
|
|
- One more ADR's worth of surface for every reader of the fleet GC
|
|
story (§2 changes no signatures; it pins names for semantics the
|
|
mechanisms already had).
|
|
- The feature graph must be implemented as specced in §4's table — an
|
|
implementation-facing edit arriving with the engine tasks rather
|
|
than inside one of them.
|
|
|
|
**Neutral**
|
|
|
|
- No tier, engine, constructor mode, facade signature, error variant,
|
|
or ops payload changes. `fleet: true` validation (ADR-012 §3) and
|
|
the composite fleet-validity predicate are untouched.
|
|
- The sibling substrate relationship is documentary here —
|
|
requirements-level facts and boundary rules — with the eventual
|
|
dependency edge (if any, e.g. an ops-layer convenience) requiring
|
|
its own ADR through the substitution door.
|
|
|
|
## References
|
|
|
|
- ADR-001 (the substrate posture §1 extends to the reactive
|
|
substrate), ADR-003/007 (the substitution door §2 applies), ADR-005
|
|
(cadence ownership; delete-then-recover as the idempotency
|
|
backstop), ADR-006 (the deferred-cost pattern this ADR's OQ-11
|
|
resolution follows), ADR-008 (the fleet sweep lock §2 pins and the
|
|
liveness materialization rule §3 reuses), ADR-010/012 (the
|
|
engine-state seam, GC-state host, outcomes-as-data, the composite
|
|
predicate), ADR-011 (vocabulary; the constructor tables §4
|
|
supersedes in feature shape)
|
|
- [requirements.md](../requirements.md) — REQ-2 (the fact OQ-11 parked
|
|
on), REQ-5 (the composition requirement §3 serves)
|
|
- [overview.md](../overview.md) (dependency posture, amended §4;
|
|
non-goals, extended §1); [backends-and-dispatch.md](../backends-and-dispatch.md)
|
|
(engine/features table, amended §4); [store-api.md](../store-api.md)
|
|
(sweep semantics, §2's lease wording); [gc-and-namespaces.md](../gc-and-namespaces.md)
|
|
(sweeper locking); [open-questions.md](../open-questions.md) (OQ-11
|
|
resolved)
|
|
- alkstore — `/workspace/@alkdev/alkstore`: its consumer-inventory
|
|
(`docs/research/consumer-inventory.md`, the graded fleet rows),
|
|
ADR-016 (deployment honesty), ADR-017 (contract versioning — §5's
|
|
cited discipline); the sibling's payload model (JSON-only) is its
|
|
own non-goal boundary, matching this crate's object-storage
|
|
boundary here
|
|
- The alkcall hedge precedent (published-doc-surface drift) — the
|
|
failure mode §5 guards, on the operator's record |