docs(architecture): ADR-013 — family composition, reactive boundary, sweep lease; OQ-11 resolved
- 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.
This commit is contained in:
1 parent
7b9d904b8a
commit
bee86b0dbd
11 files changed
+433
-64
No files matched your search
+242
@@ -0,0 +1,242 @@
|
||||
# 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
|
||||
Reference in new issue
Block a user