224 lines
15 KiB
Markdown
224 lines
15 KiB
Markdown
---
|
||
status: stable
|
||
last_updated: "2026-10-11 (the notify prune-cadence note — the
|
||
notifications table's at-attach-only hygiene sharpened, accepted at
|
||
the growth-posture review)"
|
||
---
|
||
|
||
# SQLite engine
|
||
|
||
The `alkstore-sqlite` engine implements
|
||
[core-contract.md](core-contract.md) on rusqlite over the
|
||
[forked honker-core substrate](decisions/011-sqlite-substrate-fork.md) —
|
||
carried in-tree as the engine crate's substrate module subtree
|
||
([ADR-013](decisions/013-fold-substrate-into-sqlite.md)). This spec
|
||
records WHAT the engine is internally (its connection architecture,
|
||
seam, and wake plumbing) — not code-level HOW. Decisions live in ADRs;
|
||
per-contract obligations live in the core spec and are not restated here.
|
||
|
||
## Identity and posture
|
||
|
||
- Single driver: rusqlite, riding the
|
||
[forked honker-core substrate](decisions/011-sqlite-substrate-fork.md)
|
||
— the forked lineage machinery carried in-tree as this crate's
|
||
`src/substrate/` module subtree
|
||
([ADR-012](decisions/012-forked-substrate-design.md) design,
|
||
[ADR-013](decisions/013-fold-substrate-into-sqlite.md) packaging) —
|
||
with `bundled-sqlite` for hermetic builds. No `.so` runtime artifacts
|
||
([ADR-003](decisions/003-sqlite-driver.md) for the driver decision;
|
||
ownership resolved by
|
||
[ADR-011](decisions/011-sqlite-substrate-fork.md) — OQ-06's read
|
||
fired ADR-005's fork trigger).
|
||
- Sync machinery, async-facing trait: honker-core is sync (std
|
||
threads, blocking iterators); the engine bridges at the trait seam
|
||
per the family-standard posture (alktty REQ-TTY-01 precedent).
|
||
Every engine call runs in `spawn_blocking`.
|
||
- Single-host by nature: file-backed, one machine, NFS-two-writers
|
||
unsupported (honker's honesty posture, inherited). See
|
||
[deployment.md](deployment.md); the boundary's surface location is
|
||
decided ([ADR-016](decisions/016-deployment-honesty.md)) — this
|
||
crate's identity and docs state the posture; no runtime descriptor
|
||
exists.
|
||
|
||
## Connection architecture
|
||
|
||
- **Writer** — one dedicated connection; the only one permitted to
|
||
write. Serializes all mutations (WAL single-writer, modeled
|
||
honestly, not fought).
|
||
- **Readers** — a small pool of read connections for lookups and
|
||
claim/ack work.
|
||
- **Watcher** — honker-core's `SharedUpdateWatcher`: a dedicated
|
||
thread polling `PRAGMA data_version` at the default 1 ms cadence,
|
||
fanning out to listeners, overtriggering on purpose (waking all
|
||
subscribers per poll tick, even when several commits coalesced
|
||
inside one tick — wake is a hint; consumers re-read indexed state,
|
||
[ADR-006](decisions/006-wake-and-delivery-contract.md)). The 1 ms
|
||
default is the shipping default — the POC's published wake-latency
|
||
numbers were measured at it
|
||
([ADR-023](decisions/023-fourth-review-round.md) §4); the cadence
|
||
is tunable via `SqliteOpts::poll_interval`
|
||
([ADR-008](decisions/008-contract-v1-pinning.md) §6's config split
|
||
— engine opts carry cadence), the documented idle cost and tuning
|
||
recipe in [deployment.md](deployment.md).
|
||
- Each connection runs the substrate's bootstrap at open: pragmas
|
||
(WAL, `synchronous=NORMAL`, busy timeout), `attach_notify`,
|
||
the substrate's function attachments and schema bootstrap (the
|
||
alknet-filesystem POC's wiring shape). Watcher spawn is fallible in
|
||
the substrate ([ADR-012](decisions/012-forked-substrate-design.md)
|
||
§4); open fails if the watcher cannot start.
|
||
- **The `open` path argument is a plain filesystem path**
|
||
([ADR-023](decisions/023-fourth-review-round.md) §3): the
|
||
substrate's `open_conn` drops the inherited `SQLITE_OPEN_URI` flag
|
||
(a registered fork delta), so `?name=value` suffixes in the path
|
||
are literal filenames — no URI interpretation, no connection-
|
||
semantics mutation outside the constructor's opts (`:memory:` works
|
||
without URI mode; URI-only features like shared-cache memory DBs
|
||
re-enter with a consumer-inventory row).
|
||
|
||
## Mapping the contract
|
||
|
||
| Contract piece | Engine realization |
|
||
|---|---|
|
||
| notify / listen | honker's notify functions inside the caller's tx; `listen()` bridges the watcher's fanout into a tokio receiver (one dedicated `std::thread` per subscription doing `blocking_send` — the stream bridge's aligned shape, review 004 item 8; the same thread is not a `spawn_blocking` task, so parked bridges never consume the blocking pool the engine's seams share) |
|
||
| streams | the forked substrate's stream machinery (inherited near-verbatim — the `__alkstore_stream` table's field set is already the contract's shape (its `topic` column carries the contract's `stream` field — the one name delta, ADR-012 §3's fidelity posture), the offset-ASC read path, keyed publish's nullable key column, monotone offset saves ([ADR-015](decisions/015-streams-depth.md)); explicit offset saves through the tx seam; `trim_to` is a `DELETE FROM … WHERE offset <= ?` inside the writer-slot lease) |
|
||
| queues | owned queue machinery (forked substrate re-derived on [ADR-010](decisions/010-queue-semantics-depth.md) — stamps, no-stranded-rows sweep, dead-visible `get_job`); ADR-003's ride posture superseded on ownership by [ADR-011](decisions/011-sqlite-substrate-fork.md) |
|
||
| named locks | the substrate's lock machinery (`lock_renew` carries the renew semantics the guarantee row pins; re-acquire does not refresh TTL — inherited deliberately, [ADR-012](decisions/012-forked-substrate-design.md) §4) |
|
||
| scheduler / outbox | collapse shape ([ADR-009](decisions/009-scheduler-collapse.md)): owned scheduler storage (`__alkstore_scheduler_tasks`, forked substrate) carrying `@every` specs (cron machinery not ported); the leader loop pattern (TTL lock, heartbeat, exit-before-tick-on-loss); the leadership lock is `__alkstore_scheduler`; outbox = helper over queues with the derived backing-queue name ([ADR-008](decisions/008-contract-v1-pinning.md) §4) |
|
||
| begin_tx | acquires the writer slot, opens `BEGIN IMMEDIATE`, returns the caller-held handle ([ADR-007](decisions/007-transactional-seam.md)) |
|
||
| handle ops | each `*_tx` op round-trips `spawn_blocking` to the same connection (thread-affinity note in [ADR-007](decisions/007-transactional-seam.md)) |
|
||
| commit/rollback | releases the writer slot |
|
||
|
||
## Obligations and constraints
|
||
|
||
- **A long transaction parks the writer** (the slot lease is the
|
||
honest model). Contract docs must surface this so consumers budget
|
||
transactions ([ADR-007](decisions/007-transactional-seam.md) negative
|
||
consequence).
|
||
- Watcher failure handling is inherited: on watcher death, every
|
||
subscriber's receiver closes (`WatcherDeathGuard` behavior) —
|
||
consumers see the close, never a silent hang
|
||
([ADR-006](decisions/006-wake-and-delivery-contract.md)). New
|
||
subscriptions issued *after* a watcher death (a `listen()` or a
|
||
stream `subscribe()`) fail closed with the opaque `Error::Database`
|
||
fallback — they never enroll onto a dead feed to park forever
|
||
(review 004 item 2); recovery is a fresh store open. The watcher
|
||
also treats the db file's identity as unknown until its first
|
||
successful stat: a failed startup stat does not seed a false
|
||
identity that the first later stat would spuriously trip into the
|
||
death path (review 004 item 3 — a transient stat hiccup at start
|
||
closes nothing).
|
||
- Wake coalescing: bursts inside one poll tick produce one wake;
|
||
correctness is preserved by the re-read contract
|
||
(POC-pinned: missed-wake stress with correct post-burst re-reads).
|
||
- **Deployment note**: the lineage's rusqlite generation
|
||
(^0.40.1) — this engine crate's own dependency — has a rustc floor
|
||
≥ 1.99; binaries linking this engine
|
||
carry that requirement ([deployment.md](deployment.md) matrix).
|
||
- The optimization path, if seam throughput ever demands it: a
|
||
dedicated std-thread bridge (one thread owning the writer conn, ops
|
||
over mpsc — measured ~2× the spawn_blocking shape at p50 in POC #1),
|
||
or a raised watcher cadence for idle CPU. Neither is the default.
|
||
|
||
### Notifications-table hygiene
|
||
|
||
The `__alkstore_notifications` table (this engine's notify wake
|
||
transport) is pruned to its newest 10,000 rows, oldest-first —
|
||
the at-attach cap (ADR-010 §6, register D-16) — and the attach is
|
||
the **only** prune site: the prune runs at every connection open (the
|
||
substrate's bootstrap); there is no background sweeper, no
|
||
time-based retention, and no consumer-visible prune operation — the
|
||
table stayed off the contract surface by ADR-008 §4's disposition
|
||
(upstream's `prune_notifications*` helpers are not carried).
|
||
|
||
The consequence a deployer should size honestly: an idle-but-listening
|
||
process — one that opens its connections at startup and holds them —
|
||
attaches never, so nothing trims the table; every committed notify
|
||
across the fleet lands a row and the table grows past the cap until
|
||
the next attach (a reconnect, a second `open` of the same file by any
|
||
process, or a restart). Attach-prune is hygiene at open, not retention
|
||
at runtime. Long-lived, notify-heavy single-process deployments should
|
||
either budget an attach event into their runbook or size the table
|
||
deliberately. Accepted with that caveat at the growth-posture review;
|
||
the per-engine record lives in [deployment.md](deployment.md)'s growth
|
||
postures.
|
||
|
||
## What the fork question resolved
|
||
|
||
The [quality read (OQ-06)](../research/quality-read-honker-core.md) —
|
||
this engine's dependency gate — resolved 2026-10-05: ADR-005's fork
|
||
trigger **fired** ([ADR-011](decisions/011-sqlite-substrate-fork.md)),
|
||
and the fork's design is pinned by
|
||
[ADR-012](decisions/012-forked-substrate-design.md): the substrate is a
|
||
vendored contract-blind lineage module folded into this engine crate
|
||
([ADR-013](decisions/013-fold-substrate-into-sqlite.md)); the
|
||
watcher/connection architecture this spec describes is inherited
|
||
verbatim where kept ([ADR-003](decisions/003-sqlite-driver.md)
|
||
unchanged in architecture), with the port deltas ADR-012 §4 decides
|
||
(fallible watcher spawn, bounded reconnect backoff, panic-free
|
||
death-closes-subscribers). The
|
||
[evidence](../research/quality-read-honker-core.md): published
|
||
0.5.0's unreleased-fix-train defect class, plus ADR-010's queue depth
|
||
requiring engine-owned queue SQL in any posture. The substrate's table
|
||
family is `__alkstore_*` (ADR-010 §8's naming authorization).
|
||
|
||
## Design Decisions
|
||
|
||
| ADR | Decision | Summary |
|
||
|---|---|---|
|
||
| [001](decisions/001-crate-split.md) | Crate split | single-driver engine crate |
|
||
| [002](decisions/002-feature-scope.md) | Feature scope | which rows this engine serves |
|
||
| [003](decisions/003-sqlite-driver.md) | Driver | rusqlite + the forked substrate lineage, bridged seam, inherited watcher — ownership resolved by 011 |
|
||
| [005](decisions/005-dependency-ownership.md) | Ownership | published honker-core; fork trigger fired by OQ-06 (ADR-011) |
|
||
| [006](decisions/006-wake-and-delivery-contract.md) | Wake contract | data_version watcher, coalescing, death-closes-receivers |
|
||
| [007](decisions/007-transactional-seam.md) | Tx seam | writer-slot lease, `BEGIN IMMEDIATE`, `spawn_blocking` round-trips |
|
||
| [008](decisions/008-contract-v1-pinning.md) | Contract v1 | pinned surface; outbox backing queue derived under the reserved prefix; wake payload transport unused (non-contract) |
|
||
| [009](decisions/009-scheduler-collapse.md) | Scheduler collapse | `@every` specs; `__alkstore_scheduler` leadership; cron rejected |
|
||
| [010](decisions/010-queue-semantics-depth.md) | Queue depth | semantics pinned; §5/§3a realization resolved by ADR-011 (owned code) |
|
||
| [011](decisions/011-sqlite-substrate-fork.md) | Substrate fork | OQ-06's trigger fired; substrate owned, queue ops re-derived on contract v1 |
|
||
| [012](decisions/012-forked-substrate-design.md) | Fork design | contract-blind substrate, fidelity posture, port deltas, panic-free watcher death |
|
||
| [013](decisions/013-fold-substrate-into-sqlite.md) | Substrate packaging | folded into `alkstore-sqlite` (`src/substrate/`); no fourth crate |
|
||
| [014](decisions/014-outbox-tx-enqueue.md) | Outbox tx enqueue | `outbox_enqueue_tx` on `TxHandle`; derivation engine-side through the writer-slot lease |
|
||
| [015](decisions/015-streams-depth.md) | Streams depth | key = carried metadata (the inherited nullable column); global-FIFO ordering; `trim_to` as a writer-lease delete; event shape already the substrate's |
|
||
| [016](decisions/016-deployment-honesty.md) | Deployment honesty | no runtime capability surface; single-host posture stated by crate identity + docs; this engine never produces `PayloadTooLarge` (pinned in the contract suite) |
|
||
| [017](decisions/017-contract-versioning.md) | Contract versioning | engine pins core `alkstore = "1.y"` in its manifest; substrate cherry-picks are class-4 non-events; adoption duties per its §5 |
|
||
| [018](decisions/018-provenance-register-and-cherry-picks.md) | Substrate provenance | `src/substrate/PROVENANCE.md` (identity, delta register — cherry-picks as tagged entries); cherry-picks recorded at adoption, non-versioning |
|
||
| [019](decisions/019-mechanism-handle-surfaces.md) | Handle surfaces | boxed handle traits (`Queue`/`StreamHandle`/`Outbox`/`Lock`/`JobHandle`); `worker_id` stamps the row's claimant column; `StopToken` core-owned |
|
||
| [020](decisions/020-enqueue-opt-semantics-and-bridges.md) | Enqueue options | delay-over-`run_at` in the substrate's re-derived enqueue; scheduler fires stamp from derived defaults; serde_json byte encoding |
|
||
| [021](decisions/021-tx-reads-and-value-shape-fixes.md) | Third review round | tx-read methods route through the writer-slot lease; `Job.claimed_at` (the fork's claimed_at column, surfaced in `Job`); `schedule()` queue argument validated; drop = rollback releases the lease with `ROLLBACK` |
|
||
| [023](decisions/023-fourth-review-round.md) | Fourth review round | `open` path is a plain filesystem path (URI flag dropped — registered fork delta); 1 ms watcher default stands, cadence carried onto `SqliteOpts::poll_interval`; `encode_payload` typed (`Codec`); numeric-argument domains pinned contract-side (extents clamp empty — enforced at this engine's trait-impl entry over the substrate) |
|
||
|
||
## Open Questions
|
||
|
||
Open questions are tracked in
|
||
[open-questions.md](open-questions.md). Key
|
||
questions affecting this document:
|
||
|
||
- **OQ-06**: honker-core quality read — fork-trigger assessment —
|
||
**resolved** (2026-10-05,
|
||
[ADR-011](decisions/011-sqlite-substrate-fork.md); trigger fired).
|
||
- **OQ-12**: streams depth — **resolved**
|
||
(2026-10-05, [ADR-015](decisions/015-streams-depth.md)): key =
|
||
carried metadata; global-FIFO-by-offset ordering row;
|
||
`StreamEvent` shape (honker's, `topic` → `stream` in the contract
|
||
type — the substrate's column name stays per the ADR-012 §3
|
||
fidelity posture); `publish_with_key_tx` routes through the
|
||
writer-slot lease; `trim_to` is a plain delete in the same lease.
|
||
- **OQ-13**: transactional outbox enqueue shape — **resolved**
|
||
(2026-10-05, [ADR-014](decisions/014-outbox-tx-enqueue.md)):
|
||
`outbox_enqueue_tx(outbox, opts, payload)` on the `TxHandle` trait;
|
||
the SQLite handle routes the op through the writer-slot lease
|
||
([ADR-007](decisions/007-transactional-seam.md)) into the forked
|
||
substrate's `__alkstore_outbox:{name}` backing queue.
|
||
|
||
Resolved: **OQ-09** (scheduler collapse —
|
||
[ADR-009](decisions/009-scheduler-collapse.md)), **OQ-05** (queue
|
||
semantics depth — [ADR-010](decisions/010-queue-semantics-depth.md)),
|
||
**OQ-12** (streams depth — [ADR-015](decisions/015-streams-depth.md)),
|
||
**OQ-08** (capability surface — none, by default ever;
|
||
[ADR-016](decisions/016-deployment-honesty.md)), **OQ-10** (contract
|
||
versioning — [ADR-017](decisions/017-contract-versioning.md)), and
|
||
**OQ-11**
|
||
(fork follow-through — the substrate's provenance register and the
|
||
cherry-pick procedure;
|
||
[ADR-018](decisions/018-provenance-register-and-cherry-picks.md)),
|
||
2026-10-05/06. |