Files
alkstore/docs/architecture/deployment.md
T
glm-5.3-flash 49face898d docs alignment: v1 TLS posture owned by deployment.md, QueueOpts numeric consumer-obligation notes (task pg-fix-docs-alignment, review 002 Finding 6 remainder)
- forwarder.rs's ListenerConnection doc corrected: NoTls is hardwired on
  every connection path (pooled, listener, reconnect) — the pooled path
  never rode the consumer's Config sslmode (a sslmode=require DSN fails
  at connect); grep-audited no other in-crate doc repeats the claim
- PgOpts doc carries the corrected one-line TLS pointer (engine-crate-
  docs posture, ADR-016 §2)
- deployment.md: new 'TLS posture (v1)' subsection (NoTls everywhere,
  sslmode=require DSN fails at connect, topology-level confidentiality
  is the v1 substitute, TLS a post-v1 deployment concern) and a new
  'Consumer-obligation notes on engine options' section carrying the
  QueueOpts trusted-as-given note with code-verified per-field symptoms
  (max_attempts <= 0: never claimed, dead-lettered at the next claim
  call's pre-claim sweep; negative visibility: instantly-reclaimable
  claims; negative retention: every dead row at the next sweep_expired)
  plus the PgOpts::max_size 0-guard counter-case; frontmatter advanced
- alkstore/src/opts.rs: QueueOpts struct doc mirrors the
  consumer-obligation note (ADR-023 §2 scoping: the domain table covers
  trait-surface arguments, not consumer-constructed constants)
- cross-file doc sweep over the fix batch's touched files (forwarder,
  tx, scheduler, store) found no further doc-behavior mismatch
- gates: cargo build / clippy --all-targets -D warnings / fmt --check
  all green (doc-only, no test touched)
2026-10-10 05:08:27 +00:00

199 lines
11 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 (docs alignment — v1 TLS posture stated; QueueOpts numeric consumer-obligation notes)
---
# 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)). |
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).
### 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 |
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)) |
## 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) |
## 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.