Files
alkstore/docs/architecture/deployment.md
T
glm-5.3-flash 060f262e62 ADR-024 — the mem engine adopted as the family's third engine (wave 6; release readiness renumbered to wave 7)
Row-first gate: consumer-inventory.md gains an engine-tier section — the mem-engine need recorded operator-authority, dated 2026-10-10 (wasm32 sandboxing across the alk protocol family + downstream ffi/napi/python-adapter economics; rusqlite/tokio-postgres structurally out on wasm, so mem is the only triad member the sandbox can carry).

ADR-024 (decisions/024-mem-engine.md) pins the decision set: (1) separate `alkstore-mem` crate — discharges and amends ADR-001 §4 (superseded in part: full contract-v1 engine, not an "implementation convenience"; annotation on ADR-001 §4 + Status, class-1 window still open); (2) honest-ephemeral posture — no durability, single-process, ephemeral by design, never fleet-valid (ADR-010 §3's shared-pool predicate fails structurally), riding ADR-016's carriers unchanged (identity + docs + matrix); (3) full contract v1 tier with the asymmetry classification — `PayloadTooLarge` never produced (SQLite arm), no reconnection concept (reconnect-wake/watcher rows absent from mem's per-engine column), receiver close at engine drop, wake coalescing N/A; (4) `MemStore::new()` engine-native constructor — the one documented exception to the open-prose, core-contract.md Store concept amended; shared-instance (two opens → one engine) rejected with the ADR-016 §4-style re-entry gate (consumer-inventory row), register stays closed; (5) wasm32 — compile-clean for wasm32-unknown-unknown plus the suite's mem column green on host under a current-thread runtime flavor are wave-6 acceptance items; on-target suite execution scoped out with a named collapse condition (wasm-bindgen-test adapter crate) — a scope decision, not a pending-client hedge; (6) mem is not a core dev-dependency — core's doc examples stay mock-based, ADR-001 §4's doctest posture declines.

engine-mem.md (draft): the in-process mapping — guarded per-mechanism state (no pool, no schema bootstrap, no forwarder, no spawn_blocking seam), the tx overlay (staged *_tx effects merged atomically at commit; read-your-own-writes easy, drop-=-rollback trivial; wake-at-commit structural — the seam pg needed pg-fix-tx-wake for, mem pins from birth via the existing tx-commit-atomicity rows), ADR-012 §2 third owner of the curve/opts/@every arithmetic (three-way equivalence, ADR-022's StoreFactory reused verbatim — born pinned).

deployment.md: host-semantics row (single-process, ephemeral, never fleet-valid, wasm32-clean), mem connection-budget subsection (none — nothing to budget), durability-knobs row (the honest row is the absence), toolchain row (wasm32 subset + current-thread-clean, acceptance-cited). README/overview: engine-mem.md row, ADR-024 row, family table entry (alkstore-mem, no driver deps), current-state updated to the implementation phase with engine specs' stable statuses reflected.

implementation.md: wave table gains wave 6 (mem engine, depends waves 1 + 5) and renumbers release readiness to wave 7 (fuzzing decision — the one remaining decided-at-implementation deferral — rides wave 7); wave-6 section records the born-pinned posture, acceptance items, the one suite-harness pre-task (past_stamp_sleep blocking sleep → async sleep, review 003's properties.rs:1683 note), and the window riders (review 003 Finding 1: the pg transient-fault seam, MEDIUM, rides wave 6; Findings 2–3 follow their park into wave 7's pre-release); wave-6 review gate defined; Decided points updated (mem discharged, fuzzing renumbered); stale wave-6 references swept across review docs and the landed tasks' notes (release-readiness items renumbered or annotated).

Docs-only change; verified by cross-reference audit (all ADR/OQ/anchor references resolve, residual wave-6 mentions are correct under the new numbering or carry dated annotations).
2026-10-10 09:42:40 +00:00

212 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
status: draft
last_updated: 2026-10-10 (ADR-024 — the mem engine joins the matrix:
host row, connection budget, durability posture, wasm32 toolchain note)
---
# Deployment
What a deployer must know to size, run, and reason about alkstore
engines: host semantics, connection budgets, durability knobs, and
where engine differences may honestly surface in the contract. The
capability-surface *decision* is resolved
([ADR-016](decisions/016-deployment-honesty.md), 2026-10-06): no
runtime capability surface — this document's matrix is (with the
engine crates' own docs) where the honest boundary lives; this
document holds the facts.
## Host semantics
| Engine | Host posture | Notes |
|---|---|---|
| SQLite | **single-machine**, file-backed | NFS two-writers unsupported (honker's honesty posture, inherited, [ADR-003](decisions/003-sqlite-driver.md)). Cross-process *on one host* is verified POC ground (`data_version` is cross-process by nature). |
| Postgres | **multi-host native** | Nothing assumes a shared host; POC #2 ran all-through-network (docker bridge) with the same properties ([ADR-004](decisions/004-postgres-driver.md)). |
| Mem | **single-process, ephemeral** ([ADR-024](decisions/024-mem-engine.md) §2) | In-memory, per-instance; no cross-process story at all — two engine instances share nothing by design. No durability, by design: loss on process exit is the documented posture, and the dependency on `alkstore-mem` *is* the ephemerality declaration (honest for workloads that accept it). Never fleet-valid — the shared-pool predicate ([ADR-010](decisions/010-queue-semantics-depth.md) §3) fails structurally; there is no pool. Compile-clean on `wasm32-unknown-unknown` ([ADR-024](decisions/024-mem-engine.md) §5) — the family's member that runs in the sandbox. |
The unified trait must not pretend SQLite is multi-host — and it does
not: [ADR-016](decisions/016-deployment-honesty.md) resolves that
honesty to compile-time engine identity (the engine crate a binary
depends on *is* the deployment statement) plus this documented matrix.
There is no `Store::capabilities()` — the trait's shape constrains
where capability differences can surface
([ADR-006](decisions/006-wake-and-delivery-contract.md)), and the
resolution is: nowhere at runtime.
Options for OQ-08, with their outcome
([ADR-016](decisions/016-deployment-honesty.md)):
1. **Compile-time only** — a consumer chooses an engine crate at
dependency time; the engine's docs carry its deployment facts.
Smallest contract; nothing runtime to match on. **Adopted** —
together with (3); the two compose (engine docs serve the consumer
choosing the dependency, the matrix serves the operator choosing
the topology).
2. **`Store::capabilities()`** — a runtime description
(payload limits, wake cadence knobs, host semantics). Lets a
consumer adapt (e.g., chunk large notify payloads) but adds a
contract surface all engines must keep honest. **Rejected** —
field-by-field under ADR-008 §5's act-differently rule, and no
consumer-inventory row names a runtime-adapt need
([ADR-016](decisions/016-deployment-honesty.md) §2).
3. **Deployment matrix only** (this document) — no API surface. The
honest-middle choice; matches the ecosystem's doc-first posture
but provides no programmatic guard. **Adopted** (with (1)) — the
"programmatic guard" gap is closed where it can honestly be:
the engine-crate dependency edge cannot drift out of sync with
the truth it states; the misconfiguration case (SQLite as shared
network storage) follows the family's
deployment-asserts-truth posture — documented detection symptom,
no fabricated runtime machinery
([ADR-016](decisions/016-deployment-honesty.md) §3).
## Connection budgets
### SQLite engine
- Connections are in-process (writer + reader pool + watcher thread
owning a connection). No external budget lines; the file lock is
the OS-level resource.
### Postgres engine
| Connection class | Count | Notes |
|---|---|---|
| Pool | `max_size` per process | claims/queries via deadpool |
| Listener | **+1** per LISTEN-ing process | non-pooled, dedicated; pooled connections cannot carry LISTEN (deadpool#360, test-pinned — [ADR-004](decisions/004-postgres-driver.md)) |
| — | — | Sizing rule: `max_size + 1` per process; verify end-to-end accounting (POC #2's pgdiag-st-4: pool 6 + 1 listener + probe conns = exact server-side count) |
Shared-server co-tenancy (the alkblobs ADR-008 precedent —
`/workspace/@alkdev/alkblobs/docs/architecture/decisions/` — consumer
tables co-tenant the pg instance) is supported and expected — the
[naming / reserved namespace contract](core-contract.md#naming--reserved-namespace)
protects reserved names; queue/stream/lock/schedule tables live in one
engine-owned PostgreSQL schema (default `alkstore`, per-engine option)
— layout per [ADR-010](decisions/010-queue-semantics-depth.md) §8
(resolved from [queues.md](queues.md)'s namespace bullet).
### Mem engine
- No connections — there is nothing to budget. The engine instance
owns its state in-process; sharing nothing is the isolation design
([ADR-024](decisions/024-mem-engine.md) §4's instance rule; the
two-opens-one-engine constructor is rejected, re-entry via a
consumer-inventory row).
### TLS posture (v1)
The pg engine hardwires `NoTls` on **every connection path** in v1 —
the pooled connections, the dedicated listener connection, and every
reconnect attempt alike; the engine does not ride the consumer's
`Config` sslmode setting, and a DSN carrying `sslmode=require` (or any
TLS demand) fails at connect. The listener's `NoTls` posture is
test-pinned in the type it carries
([ADR-004](decisions/004-postgres-driver.md)); v1 TLS should therefore
be treated as **effectively unavailable** — network confidentiality
between the process and the Postgres server must come from the
deployment topology itself (private network, egress rules, a local
unix socket or sidecar), not from driver TLS. Encrypting traffic
between engine and server is a **post-v1 deployment concern** (wire a
TLS connector through the pool and listener construction); until then
the boundary above is the honest statement, per
[ADR-016](decisions/016-deployment-honesty.md)'s spirit — the matrix
states the limitation rather than having code pretend otherwise.
## Consumer-obligation notes on engine options
Consumer-*constructed* numeric option values are **trusted as given —
the engine does not validate them against domain extents**. The
engine would be a second normative home for semantics the contract
deliberately leaves to the caller's constants; the
[ADR-023](decisions/023-fourth-review-round.md) §2 domain table
covers the trait-surface arguments, not these. This applies to the
queue stamps (`QueueOpts`: `visibility_timeout_s`,
`dead_letter_retention_s`, `max_attempts`) on both engines:
- Negative or zero `visibility_timeout_s` stamps claims whose deadline
is already past — every claim is instantly reclaimable (the
dual-execution window is the consumer's documented budget
[ADR-010](decisions/010-queue-semantics-depth.md) §2).
- Zero or negative `max_attempts` stamps rows the claim statement
never hands out — each is dead-lettered with reason "max attempts
exceeded" at the next claim call on that queue (the pre-claim
sweep's `attempts >= max_attempts` arm), i.e. the queue silently
discards its work.
- Negative `dead_letter_retention_s` deletes every dead row at the
next `sweep_expired` call (retention is driver-free — nothing runs
without a caller).
- The stamps ride *future enqueues only* (`QueueOpts`'s documented
shape) — repair the opts before enqueueing rather than repairing
rows afterwards.
The same trusted-as-given shape applies to `PgOpts::max_size`'s
*positive-integer* contract — 0 is typed-failed at open, the one knob
with an engine-side guard; everything above has none. The
consumer-obligation shape here is the visibility-budgeting precedent
([ADR-010](decisions/010-queue-semantics-depth.md) §2): the constant is
the consumer's, the behavior of a mis-set one is documented, nothing
runtime is fabricated.
## Durability knobs
| Engine | Knob | Shape |
|---|---|---|
| SQLite | `synchronous` | WAL + `NORMAL` shipped ([ADR-003](decisions/003-sqlite-driver.md)); FULL is available consumer-side for stricter durability; commit fsyncs land at WAL checkpoints (the ~1000-commit spike cadence, POC #1) |
| SQLite | `poll_interval` | the watcher's `data_version` poll cadence — 1 ms shipping default ([ADR-023](decisions/023-fourth-review-round.md) §4, the measured-wake-latency posture), carried on `SqliteOpts`; the idle cost is ~1000 poll reads/sec/instance; raising the interval trades wake latency (interval-bound) for idle CPU — the tuning recipe for latency-tolerant deployments |
| Postgres | `synchronous_commit` | per-session knob; `on` is ship config (p50 2.40 ms seam); `off` trades max-tail (40.9 ms) for slightly better p50 — measured, honest trade ([ADR-004](decisions/004-postgres-driver.md)); session-level SET mechanics POC-verified |
| Mem | — (none) | There is no durability knob and no durability: the honest row is the absence itself ([ADR-024](decisions/024-mem-engine.md) §2). Wake latency is sub-millisecond in-process; no tuning surface exists |
These are engine-configuration concerns, *not* trait surface. What
part of engine config is contract-level shape vs engine-crate docs is
decided: constructors and option structs live in the engine crates;
the contract is the trait the constructor returns
([ADR-008](decisions/008-contract-v1-pinning.md) §6).
## Toolchain / platform notes
| Note | Engine | Affects |
|---|---|---|
| rusqlite 0.40.x needs rustc ≥ 1.99 | SQLite | any binary linking `alkstore-sqlite` ([ADR-003](decisions/003-sqlite-driver.md)) |
| `bundled-sqlite` adds a C build (~10 s dev, cacheable) | SQLite | build/CI time |
| `libsqlite3-sys` collision with sqlx today | any mixed-driver binary | structurally avoided ([ADR-001](decisions/001-crate-split.md) single-driver rule) |
| Listener `application_name` set for diagnosability (kill-targetable) | Postgres | ops runbooks ([ADR-004](decisions/004-postgres-driver.md)) |
| `alkstore-mem` compiles clean on `wasm32-unknown-unknown` (wave-6 acceptance, [ADR-024](decisions/024-mem-engine.md) §5); tokio features limited to the wasm-supported subset (sync + time); current-thread-runtime-clean internals (the suite's mem column runs under that flavor) | Mem | wasm targets, sandboxes, downstream ffi/napi/python adapters — the only engine that can compile to wasm (rusqlite/tokio-postgres are structurally out) |
## Consumer-facing latency profile (indicative, POC-measured)
From both POCs (single-box, relative shapes are the deliverable —
[ADR-003](decisions/003-sqlite-driver.md) and
[ADR-004](decisions/004-postgres-driver.md) carry the full tables):
- Tx seam: SQLite ~0.35 ms p50; Postgres ~2.4 ms p50 (ship config).
- Wake: ~1.1–2.2 ms p50 both engines at default cadence.
- Queue claim: LISTEN-driven 3–6 ms p50 (pg); poll-only interval-bound
(32–50 ms at a 50 ms poll).
- Absolute numbers will differ per hardware/network; they set
*expectations of order*, not SLAs — contract docs must not bake
them in ([ADR-007](decisions/007-transactional-seam.md)'s pg-POC
note).
## Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| [001](decisions/001-crate-split.md) | Crate split | single-driver binaries shape the matrix |
| [003](decisions/003-sqlite-driver.md) | SQLite driver | bundling, toolchain floor |
| [004](decisions/004-postgres-driver.md) | Postgres driver | listener budget line, forwarder posture |
| [006](decisions/006-wake-and-delivery-contract.md) | Wake contract | where capability differences may surface |
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 | constructor/options in engine crates; no capability surface in v1 (OQ-08; resolved by [ADR-016](decisions/016-deployment-honesty.md)) |
| [016](decisions/016-deployment-honesty.md) | Deployment honesty | no runtime capability surface — compile-time identity + this matrix; `PayloadTooLarge` is the one runtime asymmetry carriage |
| [021](decisions/021-tx-reads-and-value-shape-fixes.md) | Third review round | drop = rollback keeps the connection budgets exact under error paths (SQLite lease releases; pg client re-pools) |
| [024](decisions/024-mem-engine.md) | Mem engine | the matrix's third engine — honest-ephemeral: single-process, no durability, never fleet-valid, wasm32 compile-clean; no connection budget, no knobs |
## Open Questions
Open questions are tracked in
[open-questions.md](open-questions.md). Key
questions affecting this document:
- **OQ-08**: capability-surface shape — **resolved**
(2026-10-06, [ADR-016](decisions/016-deployment-honesty.md)):
no runtime capability surface; compile-time engine identity +
this matrix; re-entry via a consumer-inventory row naming a
runtime-adapt need.