OQ-06 resolved: honker-core quality read fires the fork trigger (ADR-011)
Quality read of honker-core's watcher/transactional core cross-checked against the published crates.io artifact: the core itself is clean (Writer/Readers, polling-watcher failure handling, WatcherDeathGuard all verified), but published 0.5.0 predates upstream's unreleased fix train carrying the issue-#133 savepoint hardening (silent job loss in the dead-letter paths) and five .ok() error swallows — and ADR-010's queue depth requires engine-owned queue SQL in any posture. Resolution: fork honker-core at the reference revision, inherit the clean machinery and test suites, re-derive queue ops on contract v1, rename tables to __alkstore_*. - docs/research/quality-read-honker-core.md — full evidence - docs/architecture/decisions/011-sqlite-substrate-fork.md — decision - OQ-06 resolved in open-questions.md; ADR-003/005/008, engine-sqlite, queues, README annotated for consistency
This commit is contained in:
1 parent
79a135c934
commit
befbe2e714
10 files changed
+586
-74
No files matched your search
@@ -25,9 +25,9 @@ pending architecture review and OQ resolution.
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| [overview.md](overview.md) | draft | Crate family, feature surface, non-goals, evidence base | — |
|
| [overview.md](overview.md) | draft | Crate family, feature surface, non-goals, evidence base | — |
|
||||||
| [core-contract.md](core-contract.md) | draft | The unified trait surface, delivery guarantees, tx seam | OQ-08, OQ-10 |
|
| [core-contract.md](core-contract.md) | draft | The unified trait surface, delivery guarantees, tx seam | OQ-08, OQ-10 |
|
||||||
| [engine-sqlite.md](engine-sqlite.md) | draft | SQLite engine: honker-core/rusqlite mapping | OQ-05 (resolved), OQ-06, OQ-09 (resolved) |
|
| [engine-sqlite.md](engine-sqlite.md) | draft | SQLite engine: forked-substrate/rusqlite mapping | OQ-05, OQ-06, OQ-09 (all resolved) |
|
||||||
| [engine-postgres.md](engine-postgres.md) | draft | Postgres engine: tokio-postgres/LISTEN mapping | OQ-05 (resolved), OQ-08, OQ-09 (resolved) |
|
| [engine-postgres.md](engine-postgres.md) | draft | Postgres engine: tokio-postgres/LISTEN mapping | OQ-05 (resolved), OQ-08, OQ-09 (resolved) |
|
||||||
| [queues.md](queues.md) | draft | Queue/scheduler/outbox semantics depth (resolved: ADR-009/ADR-010) | OQ-06 (rides) |
|
| [queues.md](queues.md) | draft | Queue/scheduler/outbox semantics depth (resolved: ADR-009/ADR-010) | OQ-06 (resolved) |
|
||||||
| [deployment.md](deployment.md) | draft | Host semantics, connection budgets, knobs, matrix | OQ-08 |
|
| [deployment.md](deployment.md) | draft | Host semantics, connection budgets, knobs, matrix | OQ-08 |
|
||||||
| [open-questions.md](open-questions.md) | draft | OQ tracker (promoted from OQ-ST register) | — |
|
| [open-questions.md](open-questions.md) | draft | OQ tracker (promoted from OQ-ST register) | — |
|
||||||
|
|
||||||
@@ -51,12 +51,12 @@ pending architecture review and OQ resolution.
|
|||||||
Tracked in [open-questions.md](open-questions.md) (OQ-01..NN; the
|
Tracked in [open-questions.md](open-questions.md) (OQ-01..NN; the
|
||||||
Phase 0 register's OQ-ST-01..08 promote one-to-one — OQ-NN mirrors
|
Phase 0 register's OQ-ST-01..08 promote one-to-one — OQ-NN mirrors
|
||||||
OQ-ST-NN — with new Phase 1 questions appended after). Highlights,
|
OQ-ST-NN — with new Phase 1 questions appended after). Highlights,
|
||||||
in suggested resolution order (OQ-04, OQ-09, OQ-05 resolved):
|
in suggested resolution order (OQ-04, OQ-09, OQ-05, OQ-06 resolved):
|
||||||
|
|
||||||
- **OQ-06** (high): honker-core quality read — fork-trigger gate;
|
- **OQ-06** (high): honker-core quality read — **resolved**
|
||||||
now carries two concrete fork candidates from the queue-depth
|
(2026-10-05, [ADR-011](decisions/011-sqlite-substrate-fork.md)): the
|
||||||
design (the no-stranded-rows sweep fix, dead-row `get_job`
|
fork trigger fired; SQLite substrate is owned code forked from
|
||||||
visibility — complement vs fork is the read's call).
|
honker-core's lineage, queue ops re-derived on contract v1.
|
||||||
- **OQ-08** (medium): capability-surface shape.
|
- **OQ-08** (medium): capability-surface shape.
|
||||||
- **OQ-10** (medium): contract versioning across engine crates.
|
- **OQ-10** (medium): contract versioning across engine crates.
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ comparable reuse.
|
|||||||
|
|
||||||
- honker-core pins rusqlite ^0.40.1; version movement in honker-core
|
- honker-core pins rusqlite ^0.40.1; version movement in honker-core
|
||||||
moves our rusqlite. A honker-core quality read is the recorded fork
|
moves our rusqlite. A honker-core quality read is the recorded fork
|
||||||
trigger ([ADR-005], OQ-06).
|
trigger ([ADR-005], OQ-06) — *(resolved 2026-10-05: the trigger
|
||||||
|
fired, [ADR-011](011-sqlite-substrate-fork.md) forks the substrate;
|
||||||
|
its rusqlite pin is then ours to move deliberately.)*
|
||||||
- rustc ≥ 1.99 required by rusqlite 0.40.x — binaries linking this
|
- rustc ≥ 1.99 required by rusqlite 0.40.x — binaries linking this
|
||||||
engine carry that toolchain floor (deployment-matrix row,
|
engine carry that toolchain floor (deployment-matrix row,
|
||||||
[deployment.md](../deployment.md)).
|
[deployment.md](../deployment.md)).
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ when a named trigger fires. Per subsystem:
|
|||||||
|
|
||||||
| Subsystem | Posture | Notes |
|
| Subsystem | Posture | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| honker-core (SQLite engine machinery) | published library, `honker-core = 0.5` | Fork trigger: the Phase 1 quality read of the watcher/transactional core finds a defect, OR a needed change upstream won't take. OQ-06 tracks the read. |
|
| honker-core (SQLite engine machinery) | published library, `honker-core = 0.5` — **trigger fired 2026-10-05**: forked per [ADR-011] | Fork trigger: the Phase 1 quality read of the watcher/transactional core finds a defect, OR a needed change upstream won't take. OQ-06 tracked the read; the read fired the trigger (published 0.5.0 carries the unreleased-fix-train defect class + ADR-010's queue depth requires engine-owned queue SQL in any posture). |
|
||||||
| rusqlite | published, riding honker-core's pin | Pin movement is honker-core's; we ride it ([ADR-003]). |
|
| rusqlite | published, riding honker-core's pin | Pin movement is honker-core's; we ride it ([ADR-003]). |
|
||||||
| tokio-postgres + deadpool-postgres | published library, as-is | Clean, zero conflicts, actively maintained (POC #2). |
|
| tokio-postgres + deadpool-postgres | published library, as-is | Clean, zero conflicts, actively maintained (POC #2). |
|
||||||
| postgres-notify | **not adopted** (derive-not-adopt) | Lazy reconnect, connect_script skipped at initial connect, unquoted-identifier LISTENs, single maintainer. Hand-rolled forwarder instead ([ADR-004]). Fallback if upstream improves materially. |
|
| postgres-notify | **not adopted** (derive-not-adopt) | Lazy reconnect, connect_script skipped at initial connect, unquoted-identifier LISTENs, single maintainer. Hand-rolled forwarder instead ([ADR-004]). Fallback if upstream improves materially. |
|
||||||
@@ -43,7 +43,9 @@ to this posture.
|
|||||||
**Positive**
|
**Positive**
|
||||||
|
|
||||||
- Zero vendored code in the tree by default; upgrades are
|
- Zero vendored code in the tree by default; upgrades are
|
||||||
`cargo update` work, not patch-management work.
|
`cargo update` work, not patch-management work. *(Exception now
|
||||||
|
live: the [ADR-011] fork is a vendored family crate — the named
|
||||||
|
trigger, not a posture change.)*
|
||||||
- The fork triggers are concrete and named *before* the quality read,
|
- The fork triggers are concrete and named *before* the quality read,
|
||||||
so the read produces a decision, not a debate.
|
so the read produces a decision, not a debate.
|
||||||
- Per-subsystem votes are recorded with evidence, so no future
|
- Per-subsystem votes are recorded with evidence, so no future
|
||||||
@@ -53,6 +55,10 @@ to this posture.
|
|||||||
|
|
||||||
- honker-core 0.5 remains alpha-quality software per its own README in
|
- honker-core 0.5 remains alpha-quality software per its own README in
|
||||||
the link graph; the quality read (OQ-06) is the mitigation gate.
|
the link graph; the quality read (OQ-06) is the mitigation gate.
|
||||||
|
*(Resolved 2026-10-05: the read fired the trigger — [ADR-011].
|
||||||
|
This row's negative consequence is retired with it; the
|
||||||
|
"zero vendored code" positive consequence now carries ADR-011's
|
||||||
|
named exception.)*
|
||||||
- The pg queue machinery is written by us — the pg-boss family's
|
- The pg queue machinery is written by us — the pg-boss family's
|
||||||
battle-tested edge cases must be re-earned by design + tests
|
battle-tested edge cases must be re-earned by design + tests
|
||||||
([queues.md]).
|
([queues.md]).
|
||||||
|
|||||||
@@ -208,8 +208,9 @@ struct Wake { channel: String }
|
|||||||
- SQLite: honker's machinery owns two categories of internal names,
|
- SQLite: honker's machinery owns two categories of internal names,
|
||||||
and the contract treats them differently. Its `_honker_*` *table*
|
and the contract treats them differently. Its `_honker_*` *table*
|
||||||
family (`_honker_dead`, `_honker_locks`, …) is storage-internal —
|
family (`_honker_dead`, `_honker_locks`, …) is storage-internal —
|
||||||
not part of any consumer namespace; its fate rides the quality
|
not part of any consumer namespace; its fate resolved with the
|
||||||
read (OQ-06, [ADR-005](005-dependency-ownership.md)). Its
|
quality read (OQ-06, [ADR-011](011-sqlite-substrate-fork.md) — the
|
||||||
|
fork re-owns the names as `__alkstore_*`). Its
|
||||||
**consumer-namespace derived names** are a real collision surface:
|
**consumer-namespace derived names** are a real collision surface:
|
||||||
honker-rs materializes an outbox's backing queue as
|
honker-rs materializes an outbox's backing queue as
|
||||||
`_outbox:{name}` inside the queue-name namespace — a consumer
|
`_outbox:{name}` inside the queue-name namespace — a consumer
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# ADR-011: SQLite substrate — fork honker-core into owned code
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted (2026-10-05, Phase 1 — OQ-06's resolution; the fork trigger
|
||||||
|
ADR-005 named, fired)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
ADR-005 fixed the dependency posture: published honker-core 0.5 by
|
||||||
|
default, with the fork trigger being the Phase 1 quality read finding a
|
||||||
|
defect the POCs wouldn't surface. ADR-010 handed the read two concrete
|
||||||
|
fork candidates (the expired-processing-row zombie hole; dead-row
|
||||||
|
`get_job` visibility) and made the SQLite realization of contract v1's
|
||||||
|
queue depth contingent on it. OQ-06 is the tracked assessment; the
|
||||||
|
evidence is `docs/research/quality-read-honker-core.md` (full read of
|
||||||
|
all five honker-core sources at the reference revision, cross-checked
|
||||||
|
against the published crates.io artifact).
|
||||||
|
|
||||||
|
Three facts drive this decision:
|
||||||
|
|
||||||
|
1. **The published artifact carries confirmed silent-job-loss defects.**
|
||||||
|
crates.io's newest honker-core is 0.5.0 (2026-08-23). The upstream
|
||||||
|
fix train for its own documented defect class — issue #133's
|
||||||
|
savepoint hardening of the DELETE→INSERT dead-letter paths (a job
|
||||||
|
stranded in neither table on mid-flight error = silent job loss) and
|
||||||
|
the five `.ok()` error-swallows that map every SQLite error to "no
|
||||||
|
row"/"not our claim"/"lock held" — sits in the repository
|
||||||
|
*unreleased*, alongside `claimed_at` and six weeks of hardening. The
|
||||||
|
ride posture's core value ("defects inherited for free" cuts both
|
||||||
|
ways: fixes do too) currently inherits the defects, not the fixes.
|
||||||
|
2. **Contract v1 requires re-deriving the queue-op surface in any
|
||||||
|
posture.** Per-job option stamps (ADR-010 §3a — visibility/backoff/
|
||||||
|
retention stamped at enqueue, honored per-row in the claim
|
||||||
|
statement), the no-stranded-rows sweep (§5), and dead-visible
|
||||||
|
`get_job` with stamps (§1) all mean the engine owns enqueue / claim
|
||||||
|
/ retry / fail / sweep / get_job SQL whether riding or forking. The
|
||||||
|
post-fork ride would cover only ack/heartbeat/cancel, locks,
|
||||||
|
streams, notify transport, scheduler storage, and the watcher/
|
||||||
|
connection plumbing — the minority of what the engine touches, dual-
|
||||||
|
owning the `_honker_*` schema with our migrations in the complement
|
||||||
|
posture.
|
||||||
|
3. **The read's original target — the watcher/transactional core — is
|
||||||
|
clean.** Writer, Readers, and the polling watcher's three-layer
|
||||||
|
failure handling (transient-BUSY-vs-fatal classification, conservative
|
||||||
|
wakes, re-baseline discipline, the stat-identity dead-man's switch)
|
||||||
|
and `WatcherDeathGuard`'s death-closes-subscribers property all
|
||||||
|
verified in source, present in 0.5.0. This fact steers the fork's
|
||||||
|
*scope* (inherit that machinery almost verbatim), not the posture.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The fork trigger fires.** The SQLite engine's substrate becomes owned
|
||||||
|
code: fork honker-core at the reference revision
|
||||||
|
(`/workspace/honker` @ `f4e53c6`, MIT OR Apache-2.0, provenance
|
||||||
|
recorded per AGENTS.md §3) into the alkstore family, ported to the
|
||||||
|
family standard (tokio-facing consumers, no comments discipline, panics
|
||||||
|
out of library code), re-derived on contract v1 where ADR-010 pinned
|
||||||
|
semantics honker's functions don't provide.
|
||||||
|
|
||||||
|
Per-scope (the full register with keeps/re-derivations/drops is
|
||||||
|
`docs/research/quality-read-honker-core.md` §6):
|
||||||
|
|
||||||
|
- **Inherited near-verbatim:** the PRAGMA/WAL open posture,
|
||||||
|
`Writer`, `Readers`, the polling watcher + `SharedUpdateWatcher` +
|
||||||
|
`WatcherDeathGuard` + dead-man's switch (with two port fixes the read
|
||||||
|
named: reconnect backoff, fallible watcher spawn), the
|
||||||
|
`in_savepoint` mutation-discipline machinery, arg-coercion helpers,
|
||||||
|
the notify scalar + notifications table (renamed, with engine-
|
||||||
|
internal hygiene per ADR-010 §6), streams, locks.
|
||||||
|
- **Re-derived on contract v1:** enqueue + opt stamping; single-
|
||||||
|
statement claim with per-row visibility from the job's stamps;
|
||||||
|
savepoint-guarded retry/fail/dead-letter; the both-states
|
||||||
|
no-stranded-rows `sweep_expired` with retention deletion; dead-
|
||||||
|
visible `get_job`; scheduler tick with `@every` boundary math.
|
||||||
|
- **Dropped:** cron parsing (ADR-009 rejects cron strings), the
|
||||||
|
experimental watcher backends and their optional deps, the rate-limit
|
||||||
|
and result tables (cut-flags), the superseded queue functions.
|
||||||
|
- **Table naming:** `_honker_*` → `__alkstore_*` — storage-internal
|
||||||
|
(ADR-008 §4); ADR-010 §8 pre-authorized the fork re-owning names.
|
||||||
|
- **Tests:** honker-core's suites inherited as the floor (watcher
|
||||||
|
lifecycle/failure, savepoint, multiprocess pressure) + the contract-
|
||||||
|
property tests (no-stranded-rows, dead-visible `get_job`, stamps).
|
||||||
|
|
||||||
|
Packaging follows ADR-001's split: the forked substrate is the
|
||||||
|
SQLite engine crate's dependency — vendored as a workspace-family
|
||||||
|
crate (provenance + license recorded), not re-published.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive**
|
||||||
|
|
||||||
|
- The confirmed defect class (D-1..D-5) is owned, fixed, and testable
|
||||||
|
in-tree; no release cadence gates correctness we already know how to
|
||||||
|
fix.
|
||||||
|
- Contract v1's queue depth realizes directly in owned SQL — no
|
||||||
|
two-statement re-stamp bridge, no engine migrations over a
|
||||||
|
dual-owned `_honker_*` table family.
|
||||||
|
- The fork inherits ~4,000 lines of upstream hardening tests plus the
|
||||||
|
watcher machinery the POCs measured as the quality win of the ride
|
||||||
|
posture — most of the fork cost is pre-paid.
|
||||||
|
|
||||||
|
**Negative**
|
||||||
|
|
||||||
|
- We own the SQLite machinery's future: upstream fixes (rare and
|
||||||
|
incremental, but real — the #80/#133 train proves the stream is
|
||||||
|
alive) must be deliberately cherry-picked rather than inherited by
|
||||||
|
`cargo update`.
|
||||||
|
- A vendored workspace-family crate breaks "zero vendored code in the
|
||||||
|
tree by default" (ADR-005's positive consequence) — this is the
|
||||||
|
exception the posture always named, not a renegotiation.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- OQ-06 (`docs/architecture/open-questions.md`) — the resolved
|
||||||
|
assessment; `docs/research/quality-read-honker-core.md` — the
|
||||||
|
evidence (published-artifact fact, watcher-core verdict, defect
|
||||||
|
register, fork calculus and scope).
|
||||||
|
- [ADR-005](005-dependency-ownership.md) — the posture and trigger this
|
||||||
|
ADR fires; fork-is-normal-work applies.
|
||||||
|
- [ADR-003](003-sqlite-driver.md) — driver, seam, and watcher
|
||||||
|
architecture (unchanged in ownership; the fork inherits the measured
|
||||||
|
machinery).
|
||||||
|
- [ADR-010](010-queue-semantics-depth.md) — §3a/§5/§1's realization
|
||||||
|
is now owned-code work; §8's naming authorization applies.
|
||||||
|
- [ADR-001](001-crate-split.md) — packaging (engine crate's
|
||||||
|
single-driver substrate).
|
||||||
|
- [engine-sqlite.md](../engine-sqlite.md) — the living spec, updated
|
||||||
|
to the forked substrate.
|
||||||
@@ -6,20 +6,24 @@ last_updated: 2026-10-05
|
|||||||
# SQLite engine
|
# SQLite engine
|
||||||
|
|
||||||
The `alkstore-sqlite` engine implements
|
The `alkstore-sqlite` engine implements
|
||||||
[core-contract.md](core-contract.md) on rusqlite + published
|
[core-contract.md](core-contract.md) on rusqlite over the
|
||||||
honker-core. This spec records WHAT the engine is internally (its
|
[forked honker-core substrate](decisions/011-sqlite-substrate-fork.md).
|
||||||
|
This spec records WHAT the engine is internally (its
|
||||||
connection architecture, seam, and wake plumbing) — not code-level
|
connection architecture, seam, and wake plumbing) — not code-level
|
||||||
HOW. Decisions live in ADRs; per-contract obligations live in the core
|
HOW. Decisions live in ADRs; per-contract obligations live in the core
|
||||||
spec and are not restated here.
|
spec and are not restated here.
|
||||||
|
|
||||||
## Identity and posture
|
## Identity and posture
|
||||||
|
|
||||||
- Single driver: rusqlite (riding
|
- Single driver: rusqlite, riding the
|
||||||
[honker-core's pin](decisions/003-sqlite-driver.md)),
|
[forked honker-core substrate](decisions/011-sqlite-substrate-fork.md)
|
||||||
`bundled-sqlite` for hermetic builds. No `.so` runtime artifacts, no
|
(fork of published honker-core 0.5.0's lineage at the reference
|
||||||
vendored patches
|
revision),
|
||||||
([ADR-003](decisions/003-sqlite-driver.md),
|
`bundled-sqlite` for hermetic builds. No `.so` runtime artifacts
|
||||||
[ADR-001](decisions/001-crate-split.md)).
|
([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
|
- Sync machinery, async-facing trait: honker-core is sync (std
|
||||||
threads, blocking iterators); the engine bridges at the trait seam
|
threads, blocking iterators); the engine bridges at the trait seam
|
||||||
per the family-standard posture (alktty REQ-TTY-01 precedent).
|
per the family-standard posture (alktty REQ-TTY-01 precedent).
|
||||||
@@ -52,9 +56,9 @@ spec and are not restated here.
|
|||||||
|---|---|
|
|---|---|
|
||||||
| notify / listen | honker's notify functions inside the caller's tx; `listen()` bridges the watcher's fanout into a tokio receiver (one `spawn_blocking` thread per subscription doing `blocking_send`) |
|
| notify / listen | honker's notify functions inside the caller's tx; `listen()` bridges the watcher's fanout into a tokio receiver (one `spawn_blocking` thread per subscription doing `blocking_send`) |
|
||||||
| streams | honker's stream machinery; explicit offset saves through the tx seam |
|
| streams | honker's stream machinery; explicit offset saves through the tx seam |
|
||||||
| queues | honker's queue functions ([ADR-002](decisions/002-feature-scope.md)); semantics depth per [ADR-010](decisions/010-queue-semantics-depth.md) — ride + pin, except two contract properties honker's machinery doesn't provide (the no-stranded-rows `sweep_expired`, dead-row `get_job` visibility — OQ-06's concrete fork candidates) |
|
| 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 | honker's lock machinery |
|
| named locks | honker's lock machinery |
|
||||||
| scheduler / outbox | collapse shape ([ADR-009](decisions/009-scheduler-collapse.md)): honker's scheduler machinery (`_honker_scheduler_tasks`) carries `@every` specs (its cron boundary machinery unused — cron strings are contract-rejected); honker's 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) |
|
| 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)) |
|
| 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)) |
|
| 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 |
|
| commit/rollback | releases the writer slot |
|
||||||
@@ -72,24 +76,28 @@ spec and are not restated here.
|
|||||||
- Wake coalescing: bursts inside one poll tick produce one wake;
|
- Wake coalescing: bursts inside one poll tick produce one wake;
|
||||||
correctness is preserved by the re-read contract
|
correctness is preserved by the re-read contract
|
||||||
(POC-pinned: missed-wake stress with correct post-burst re-reads).
|
(POC-pinned: missed-wake stress with correct post-burst re-reads).
|
||||||
- **Deployment note**: honker-core 0.5 pins rusqlite ^0.40.1, whose
|
- **Deployment note**: the substrate's rusqlite generation
|
||||||
rustc floor is ≥ 1.99; binaries linking this engine carry that
|
(^0.40.1) has a rustc floor ≥ 1.99; binaries linking this engine
|
||||||
requirement ([deployment.md](deployment.md) matrix).
|
carry that requirement ([deployment.md](deployment.md) matrix).
|
||||||
- The optimization path, if seam throughput ever demands it: a
|
- The optimization path, if seam throughput ever demands it: a
|
||||||
dedicated std-thread bridge (one thread owning the writer conn, ops
|
dedicated std-thread bridge (one thread owning the writer conn, ops
|
||||||
over mpsc — measured ~2× the spawn_blocking shape at p50 in POC #1),
|
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.
|
or a raised watcher cadence for idle CPU. Neither is the default.
|
||||||
|
|
||||||
## What rides on the fork question
|
## What the fork question resolved
|
||||||
|
|
||||||
Nearly all of honker-core's surface is consumed (Writer/Readers/
|
The [quality read (OQ-06)](decisions/005-dependency-ownership.md) —
|
||||||
SharedUpdateWatcher/attach_*). The [quality read
|
this engine's dependency gate — resolved 2026-10-05: ADR-005's fork
|
||||||
(OQ-06)](decisions/005-dependency-ownership.md) is this engine's only
|
trigger **fired** ([ADR-011](decisions/011-sqlite-substrate-fork.md)).
|
||||||
open dependency gate: if it names a defect or an upstream-unwon't
|
The substrate is owned code forked from honker-core's lineage; the
|
||||||
change, the fork posture
|
watcher/connection architecture this spec describes is inherited
|
||||||
([ADR-005](decisions/005-dependency-ownership.md)) fires and this
|
verbatim where kept ([ADR-003](decisions/003-sqlite-driver.md)
|
||||||
engine's substrate becomes owned code. Until then, published-library
|
unchanged in architecture). The
|
||||||
consumption stands.
|
[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 forked
|
||||||
|
substrate's table family is `__alkstore_*` (ADR-010 §8's naming
|
||||||
|
authorization).
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
|
|
||||||
@@ -97,13 +105,14 @@ consumption stands.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| [001](decisions/001-crate-split.md) | Crate split | single-driver engine crate |
|
| [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 |
|
| [002](decisions/002-feature-scope.md) | Feature scope | which rows this engine serves |
|
||||||
| [003](decisions/003-sqlite-driver.md) | Driver | rusqlite + honker-core, bridged seam, inherited watcher |
|
| [003](decisions/003-sqlite-driver.md) | Driver | rusqlite + honker-core lineage, bridged seam, inherited watcher — ownership resolved by 011 |
|
||||||
| [005](decisions/005-dependency-ownership.md) | Ownership | published honker-core; named fork triggers |
|
| [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 |
|
| [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 |
|
| [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) |
|
| [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 | honker scheduler machinery with `@every` specs; `__alkstore_scheduler` leadership; cron machinery unused |
|
| [009](decisions/009-scheduler-collapse.md) | Scheduler collapse | `@every` specs; `__alkstore_scheduler` leadership; cron rejected |
|
||||||
| [010](decisions/010-queue-semantics-depth.md) | Queue depth | ride + pin over honker's functions; §5/§3a properties are OQ-06 fork candidates |
|
| [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 |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
@@ -111,10 +120,9 @@ Open questions are tracked in
|
|||||||
[open-questions.md](open-questions.md). Key
|
[open-questions.md](open-questions.md). Key
|
||||||
questions affecting this document:
|
questions affecting this document:
|
||||||
|
|
||||||
- **OQ-06**: honker-core quality read — fork-trigger assessment
|
- **OQ-06**: honker-core quality read — fork-trigger assessment —
|
||||||
(open); now carrying two concrete candidates from the queue-depth
|
**resolved** (2026-10-05,
|
||||||
design (no-stranded-rows sweep, dead-row visibility —
|
[ADR-011](decisions/011-sqlite-substrate-fork.md); trigger fired).
|
||||||
complement vs fork is the read's call)
|
|
||||||
|
|
||||||
Resolved: **OQ-09** (scheduler collapse —
|
Resolved: **OQ-09** (scheduler collapse —
|
||||||
[ADR-009](decisions/009-scheduler-collapse.md)) and **OQ-05** (queue
|
[ADR-009](decisions/009-scheduler-collapse.md)) and **OQ-05** (queue
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ so far: **OQ-04 resolved** (2026-10-05,
|
|||||||
everything else hangs off); **OQ-09 + OQ-05 resolved** (2026-10-05,
|
everything else hangs off); **OQ-09 + OQ-05 resolved** (2026-10-05,
|
||||||
[ADR-009](decisions/009-scheduler-collapse.md) and
|
[ADR-009](decisions/009-scheduler-collapse.md) and
|
||||||
[ADR-010](decisions/010-queue-semantics-depth.md) — the queue semantics
|
[ADR-010](decisions/010-queue-semantics-depth.md) — the queue semantics
|
||||||
track and the scheduler collapse/guarantee row). Next: OQ-06
|
track and the scheduler collapse/guarantee row); **OQ-06 resolved**
|
||||||
(the SQLite dependency gate — now carrying two concrete fork
|
(2026-10-05, [ADR-011](decisions/011-sqlite-substrate-fork.md) — the
|
||||||
candidates from the queue-depth design), OQ-08 (rides the now-pinned
|
fork trigger fired on the published-artifact facts). Next: OQ-08
|
||||||
trait shape), OQ-10 (versioning discipline for contract extensions).
|
(rides the now-pinned trait shape and the ADR-011 substrate fork),
|
||||||
|
OQ-10 (versioning discipline for contract extensions; note ADR-011
|
||||||
|
changes its substrate-side facts for SQLite — the forked crate is
|
||||||
|
versioned in the family workspace, so the engine/core contract pairing
|
||||||
|
is what the discipline must track).
|
||||||
|
|
||||||
Resolved questions stay listed with their resolution; they are not
|
Resolved questions stay listed with their resolution; they are not
|
||||||
deleted.
|
deleted.
|
||||||
@@ -182,24 +186,34 @@ narrowed to the pinning work its own record already scoped.)*
|
|||||||
|
|
||||||
## Theme: Engines and dependencies
|
## Theme: Engines and dependencies
|
||||||
|
|
||||||
### OQ-06: honker-core quality read — does the default posture hold? *(== OQ-ST-06)*
|
### OQ-06: honker-core quality read — does the default posture hold? *(== OQ-ST-06)* — **RESOLVED**
|
||||||
|
|
||||||
- **Origin**: [ADR-005](decisions/005-dependency-ownership.md),
|
- **Origin**: [ADR-005](decisions/005-dependency-ownership.md),
|
||||||
[engine-sqlite.md](engine-sqlite.md)
|
[engine-sqlite.md](engine-sqlite.md)
|
||||||
- **Status**: open
|
- **Status**: resolved (2026-10-05, Phase 1 —
|
||||||
|
[ADR-011](decisions/011-sqlite-substrate-fork.md))
|
||||||
- **Priority**: high (fork trigger is a gate on the SQLite engine's
|
- **Priority**: high (fork trigger is a gate on the SQLite engine's
|
||||||
dependency posture)
|
dependency posture)
|
||||||
- **Resolution**: open. ADR-005 fixed the calculus and the trigger: the
|
- **Resolution**: **The fork trigger fires.** Evidence:
|
||||||
Phase 1 quality read of honker-core 0.5's watcher/transactional core
|
[quality-read-honker-core.md](../research/quality-read-honker-core.md).
|
||||||
(Writer/Readers/SharedUpdateWatcher/attach_*) — looking for defects
|
The read's original target is clean (Writer/Readers, the polling
|
||||||
the POCs wouldn't surface, unsafe assumptions in the watcher
|
watcher's three-layer failure handling, `WatcherDeathGuard` — all
|
||||||
failure-handling, and schema-migration brittleness. Outcomes: posture
|
verified in source, present in 0.5.0; schema migrations minor). But
|
||||||
holds (no ADR change), or a fork/patch need is named (fork is normal
|
the published artifact is materially behind the reference revision:
|
||||||
work per ADR-005).
|
crates.io's honker-core 0.5.0 predates upstream's own fix train for
|
||||||
- **Cross-references**: ADR-003, ADR-005, OQ-05 (the queue-depth read
|
its documented defect class (issue #133's savepoint hardening —
|
||||||
handed it two concrete fork candidates: the expired-
|
silent job loss on mid-flight errors in the dead-letter paths; five
|
||||||
processing-row zombie fix and dead-row `get_job` visibility —
|
`.ok()` error-swallows mapping every SQLite error to "no row"/"lock
|
||||||
complement-over-machinery vs fork is exactly its calculus).
|
held"), stranded unreleased with no announced date. And contract v1's
|
||||||
|
queue depth ([ADR-010](decisions/010-queue-semantics-depth.md) §3a/§5)
|
||||||
|
requires engine-owned enqueue/claim/sweep/get_job SQL in any posture
|
||||||
|
— making a post-fix ride a minority-shape, dual-owning the schema.
|
||||||
|
Resolution: fork honker-core at the reference revision into owned
|
||||||
|
code, inherit the clean watcher/transactional core + test suites,
|
||||||
|
re-derive the queue ops on contract v1, drop cron/experimental
|
||||||
|
backends, rename `_honker_*` → `__alkstore_*`. Recorded in
|
||||||
|
[ADR-011](decisions/011-sqlite-substrate-fork.md); ADR-005's posture
|
||||||
|
calculus applied, not renegotiated.
|
||||||
|
|
||||||
## Theme: Deployment and capabilities
|
## Theme: Deployment and capabilities
|
||||||
|
|
||||||
@@ -240,6 +254,5 @@ narrowed to the pinning work its own record already scoped.)*
|
|||||||
## Deferred / Blocked
|
## Deferred / Blocked
|
||||||
|
|
||||||
None currently. Every open OQ above is actionable Phase 1 architecture
|
None currently. Every open OQ above is actionable Phase 1 architecture
|
||||||
work (quality read, capability-surface shape, versioning discipline)
|
work (capability-surface shape, versioning discipline) with its
|
||||||
with its evidence base complete — no external arrivals are being
|
evidence base complete — no external arrivals are being waited on.
|
||||||
waited on.
|
|
||||||
+17
-11
@@ -133,8 +133,8 @@ made under; the ADRs carry the WHY.
|
|||||||
([ADR-010](decisions/010-queue-semantics-depth.md) §5): a job with
|
([ADR-010](decisions/010-queue-semantics-depth.md) §5): a job with
|
||||||
an `expires` deadline is eventually in exactly one of
|
an `expires` deadline is eventually in exactly one of
|
||||||
pending/processing/dead, never stuck unreachable. (SQLite-side
|
pending/processing/dead, never stuck unreachable. (SQLite-side
|
||||||
realization over honker's machinery rides OQ-06 — the zombie hole
|
realization resolved with OQ-06 — the fork fires ([ADR-011](decisions/011-sqlite-substrate-fork.md)),
|
||||||
is that read's concrete fork candidate.)
|
the zombie fix lands in owned code.)
|
||||||
- **No ambient sweeper**: the engine ships no background maintenance,
|
- **No ambient sweeper**: the engine ships no background maintenance,
|
||||||
no default cadence (the no-ambient-timers posture). Correctness of
|
no default cadence (the no-ambient-timers posture). Correctness of
|
||||||
transitions is the engine's; **timeliness is the consumer's**.
|
transitions is the engine's; **timeliness is the consumer's**.
|
||||||
@@ -199,8 +199,9 @@ Collapsed into queues: no `Scheduler` handle, no schedule objects.
|
|||||||
proven shape and keeps `queue(name)` a name, not a DDL operation).
|
proven shape and keeps `queue(name)` a name, not a DDL operation).
|
||||||
- **SQLite**: honker's `_honker_*` family — storage-internal
|
- **SQLite**: honker's `_honker_*` family — storage-internal
|
||||||
([ADR-008](decisions/008-contract-v1-pinning.md) §4); no new tables
|
([ADR-008](decisions/008-contract-v1-pinning.md) §4); no new tables
|
||||||
minted by this design; the family's fate rides OQ-06 (a fork
|
minted by this design; the family's fate resolved with OQ-06 — the
|
||||||
re-owns the names; nothing consumer-visible changes).
|
fork ([ADR-011](decisions/011-sqlite-substrate-fork.md)) re-owns the
|
||||||
|
names (`__alkstore_*`); nothing consumer-visible changes.
|
||||||
- Partial indexes pinned identically on both engines: partial indexes
|
- Partial indexes pinned identically on both engines: partial indexes
|
||||||
matching the claim hot path; dead rows outside it; single clock
|
matching the claim hot path; dead rows outside it; single clock
|
||||||
source (second-precision timestamps); savepoint-guarded
|
source (second-precision timestamps); savepoint-guarded
|
||||||
@@ -208,12 +209,16 @@ Collapsed into queues: no `Scheduler` handle, no schedule objects.
|
|||||||
|
|
||||||
## Reference material
|
## Reference material
|
||||||
|
|
||||||
- **honker's queue design** — the SQLite-side incumbent
|
- **honker's queue design** — the SQLite-side incumbent design
|
||||||
(`/workspace/honker`, its honker-core machinery; the engine rides it
|
reference (`/workspace/honker`; ownership resolved by
|
||||||
directly, so the SQLite side's depth is largely "inherit + pin").
|
[ADR-011](decisions/011-sqlite-substrate-fork.md) — forked, queue ops
|
||||||
|
re-derived on this design).
|
||||||
Full read: `docs/research/reference-honker-machinery.md` (schema,
|
Full read: `docs/research/reference-honker-machinery.md` (schema,
|
||||||
claim/visibility/retry/dead-letter mechanics with file/line cites;
|
claim/visibility/retry/dead-letter mechanics with file/line cites;
|
||||||
the defect list §8 feeds OQ-06).
|
the defect list §8 fed OQ-06 and is the fork's fix/inheritance
|
||||||
|
register), plus
|
||||||
|
`docs/research/quality-read-honker-core.md` (the dependency-gate
|
||||||
|
read).
|
||||||
- **pg-boss family** — `/workspace/pgboss-rs` @ 98f7d9e (design
|
- **pg-boss family** — `/workspace/pgboss-rs` @ 98f7d9e (design
|
||||||
reference only, [ADR-005](decisions/005-dependency-ownership.md)).
|
reference only, [ADR-005](decisions/005-dependency-ownership.md)).
|
||||||
Full read: `docs/research/reference-pgboss-rs-semantics.md` (states,
|
Full read: `docs/research/reference-pgboss-rs-semantics.md` (states,
|
||||||
@@ -245,9 +250,10 @@ Open questions are tracked in
|
|||||||
[open-questions.md](open-questions.md). Key
|
[open-questions.md](open-questions.md). Key
|
||||||
questions affecting this document:
|
questions affecting this document:
|
||||||
|
|
||||||
- **OQ-06**: honker-core quality read — the SQLite-side zombie fix and
|
- **OQ-06**: honker-core quality read — **resolved** (2026-10-05,
|
||||||
dead-row visibility are concrete fork candidates for it to weigh
|
[ADR-011](decisions/011-sqlite-substrate-fork.md)): the zombie fix
|
||||||
(open).
|
and dead-row visibility — this document's two named SQLite-side gaps
|
||||||
|
— land in owned code via the fork.
|
||||||
|
|
||||||
Resolved on this document's surface: **OQ-05** and **OQ-09**
|
Resolved on this document's surface: **OQ-05** and **OQ-09**
|
||||||
(2026-10-05, [ADR-010](decisions/010-queue-semantics-depth.md) /
|
(2026-10-05, [ADR-010](decisions/010-queue-semantics-depth.md) /
|
||||||
|
|||||||
@@ -0,0 +1,336 @@
|
|||||||
|
---
|
||||||
|
status: draft
|
||||||
|
last_updated: 2026-10-05
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quality read: honker-core — OQ-06 (the SQLite dependency gate)
|
||||||
|
|
||||||
|
OQ-06's mandate ([ADR-005](../architecture/decisions/005-dependency-ownership.md)):
|
||||||
|
a Phase 1 quality read of honker-core 0.5's watcher/transactional core
|
||||||
|
(Writer / Readers / SharedUpdateWatcher / attach_*), looking for defects
|
||||||
|
the POCs wouldn't surface, unsafe assumptions in the watcher
|
||||||
|
failure-handling, and schema-migration brittleness — and a weighing of
|
||||||
|
the two concrete fork candidates ADR-010 handed it (the expired-
|
||||||
|
processing-row zombie fix, dead-row `get_job` visibility) as
|
||||||
|
complement-over-machinery vs fork.
|
||||||
|
|
||||||
|
**Verdict: the fork trigger fires** — [ADR-011](../architecture/decisions/011-sqlite-substrate-fork.md).
|
||||||
|
The core the read was originally for is clean, but the read surfaced
|
||||||
|
that the *published artifact* is materially behind the reference
|
||||||
|
revision and carries confirmed silent-job-loss defects in exactly the
|
||||||
|
queue operations the ride posture points at, while contract v1's queue
|
||||||
|
depth ([ADR-010](../architecture/decisions/010-queue-semantics-depth.md))
|
||||||
|
requires re-deriving that surface in any posture. The fork is normal
|
||||||
|
work per ADR-005. This document is the evidence; the ADR is the
|
||||||
|
decision.
|
||||||
|
|
||||||
|
## 1. Read scope and the published-artifact fact
|
||||||
|
|
||||||
|
Read in full: honker-core at the reference checkout
|
||||||
|
(`/workspace/honker` @ `f4e53c6`, 2026-10-02) — all five source files
|
||||||
|
(8,833 lines): `lib.rs` (PRAGMA/WAL handling, `Writer`, `Readers`,
|
||||||
|
`UpdateWatcher` + `run_poll_loop`, `SharedUpdateWatcher` +
|
||||||
|
`WatcherDeathGuard`, bootstrap + migrations), `honker_ops.rs`
|
||||||
|
(savepoint machinery, every queue / lock / stream / scheduler /
|
||||||
|
notifications function), `kernel_watcher.rs`, `shm_watcher.rs`,
|
||||||
|
`cron.rs` (skimmed — contract-rejected per ADR-009).
|
||||||
|
|
||||||
|
Cross-checked against **crates.io**: honker-core's newest published
|
||||||
|
version is **0.5.0** — published 2026-08-23 (commit `78dec24` = tag
|
||||||
|
`v0.5.0`; same commit as `rust-v0.5.0`), single maintainer, 8 versions
|
||||||
|
total, no yanks. The reference checkout HEAD is ~6 weeks ahead and
|
||||||
|
contains a body of correctness work **no published release carries**
|
||||||
|
(the `v0.6.0` tag is a package-versioning tag; the honker-core crate at
|
||||||
|
even `v0.6.0` is still 0.5.0 and still lacks the fixes).
|
||||||
|
|
||||||
|
What 0.5.0 lacks (the upstream "Unreleased" fix train, all honker-core,
|
||||||
|
verified by diffing the tag against HEAD):
|
||||||
|
|
||||||
|
1. **Issue #133's savepoint hardening** (`in_savepoint`/`UnwindUndo`,
|
||||||
|
`honker_ops.rs:118-254` at f4e53c6) — absent from 0.5.0 entirely
|
||||||
|
(grep: 0 occurrences). Five dead-letter/sweep paths in 0.5.0 are
|
||||||
|
bare `DELETE … RETURNING` + decode + INSERT sequences with no
|
||||||
|
savepoint (commits `3ab43aa`, `753f6d0`).
|
||||||
|
2. **Five `.ok()` error-swallows** in 0.5.0's `retry`, `fail`,
|
||||||
|
`get_job`, `lock_acquire`, `result_get` — every SQLite error
|
||||||
|
(I/O, corruption, disk full) maps to "no row"/"not our claim"/
|
||||||
|
"lock held", not just `QueryReturnedNoRows` (commit `4881f27`).
|
||||||
|
3. **`claimed_at`** (schema column + migration) — absent from 0.5.0's
|
||||||
|
`BOOTSTRAP_HONKER_SQL`; added at HEAD only (`6780cae`/PR #140).
|
||||||
|
|
||||||
|
Present in 0.5.0 already (i.e. the watch-side hardening predates the
|
||||||
|
release): issue #80's `-shm`-descriptor registry and the kqueue
|
||||||
|
lock-bearing-path guards (verified in the tag's `shm_watcher.rs` /
|
||||||
|
`kernel_watcher.rs`), and the full `WatcherDeathGuard` machinery.
|
||||||
|
|
||||||
|
**Consequence of the artifact fact:** riding published 0.5.0 means
|
||||||
|
shipping the confirmed job-loss windows of §3 below inside the exact
|
||||||
|
functions ADR-010's ride posture points at; the fixes exist upstream,
|
||||||
|
are documented in honker's own CHANGELOG as real defects, and are
|
||||||
|
stranded unreleased with no announced date.
|
||||||
|
|
||||||
|
## 2. Watcher / transactional core — the read's original mandate
|
||||||
|
|
||||||
|
Verdict: **clean.** Per component (line cites at f4e53c6 unless
|
||||||
|
prefixed `v0.5.0:`, which cite the published tag):
|
||||||
|
|
||||||
|
- **`Writer`** (`lib.rs:549-617`; identical in `v0.5.0:lib.rs`):
|
||||||
|
single-connection write slot over `Mutex<Option<Connection>>` +
|
||||||
|
condvar + `closed` AtomicBool. Correct — the `acquire` wait loop
|
||||||
|
re-checks `closed` after each wakeup (close wakes blocked acquirers
|
||||||
|
with `None`), `release` after `close` drops the connection instead of
|
||||||
|
pooling it, `close` is idempotent. The explicit-`close` design
|
||||||
|
(binding-GC pressure relief) is sound for our shape too.
|
||||||
|
- **`Readers`** (`lib.rs:629-716`): the subtle race handling is right —
|
||||||
|
capacity slot is re-checked *after* `open_conn` returns (a close-race
|
||||||
|
drops the brand-new connection and releases the slot it reserved), and
|
||||||
|
the failed-open path decrements `outstanding` so transient open
|
||||||
|
failures cannot permanently shrink the pool. The closed sentinel
|
||||||
|
(`SQLITE_MISUSE` + "Database is closed") is a hack, but a documented
|
||||||
|
and harmless one.
|
||||||
|
- **Polling watcher** (`run_poll_loop`, `lib.rs:832-937`): the
|
||||||
|
three-layer defense is genuinely good and is the quality POC #1
|
||||||
|
measured. (a) `PRAGMA data_version` fast path (~3.5 µs/poll). (b)
|
||||||
|
**Transient-vs-fatal error classification**: `SQLITE_BUSY`/`LOCKED`
|
||||||
|
are retried in place — critically *not* treated as connection death,
|
||||||
|
because a reconnect would silently re-baseline `last_version` and skip
|
||||||
|
pending wakes; fatal errors drop the connection, fire one conservative
|
||||||
|
`on_change()`, and reconnect (re-baselining again — the conservative
|
||||||
|
wake covers the gap). (c) The dead-man's switch: `stat` identity
|
||||||
|
`(dev, ino)` ~every 100 ms; a replaced file (atomic rename, litestream
|
||||||
|
restore, remount) panics the watcher thread with a precise message
|
||||||
|
rather than watching stale data. All present in 0.5.0.
|
||||||
|
- **`SharedUpdateWatcher`** (`lib.rs:1063-1176`; death guard at
|
||||||
|
`:1053-1061`, in 0.5.0): one poll thread, N subscribers, capacity-1
|
||||||
|
channels (bursts coalesce), disconnected-subscriber pruning on
|
||||||
|
`TrySendError::Disconnected`, and **`WatcherDeathGuard`** — thread
|
||||||
|
exit or panic → closure drops → guard's `Drop` clears every sender →
|
||||||
|
each subscriber's next `recv()` returns `Err`. The failure-handling
|
||||||
|
property ADR-006 pins ("watcher death closes receivers, never a
|
||||||
|
silent hang") is mechanically verified in source, present in 0.5.0.
|
||||||
|
- **Experimental backends** (`kernel_watcher.rs`, `shm_watcher.rs`):
|
||||||
|
separate opt-in Cargo features with explicitly documented weaker
|
||||||
|
contracts (missed wakes possible; init failure = stderr + a backend
|
||||||
|
that produces no wakes). Their issue-#80 handling — never close a
|
||||||
|
descriptor on a lock-bearing inode (`-shm`/main db under kqueue),
|
||||||
|
with the reasoning stated at `kernel_watcher.rs:393-423` — is
|
||||||
|
exemplary systems work. **We never enable these features**; in the
|
||||||
|
fork they are dropped (§6).
|
||||||
|
- **`attach_*`** (`honker_ops.rs:259+`): standard rusqlite
|
||||||
|
scalar-function registration; documented as *not* idempotent
|
||||||
|
("call exactly once per connection") — the engine's open path must
|
||||||
|
respect that (it already does, per ADR-003's wiring).
|
||||||
|
|
||||||
|
Minor findings — none trigger anything, recorded as fork-port notes:
|
||||||
|
|
||||||
|
- **W-1 (reconnect backoff):** the reconnect loop retries one open
|
||||||
|
attempt + one `eprintln` per poll tick (1 ms). A db file that
|
||||||
|
disappears mid-flight produces ~1000 log lines/sec until closure.
|
||||||
|
In our engine the watcher closes with subscribers on db loss anyway,
|
||||||
|
but the fork's port should add bounded backoff.
|
||||||
|
- **W-2 (thread-build panic):** `spawn_with_config` ends in
|
||||||
|
`.expect("spawn update-poll thread")` — a panic in library code
|
||||||
|
(thread-budget exhaustion). Our crate rules ban panics outside tests;
|
||||||
|
the fork's port makes watcher spawn fallible. Trivial.
|
||||||
|
- **W-3 (`data_version` u32 wrap):** at sustained max commit rates the
|
||||||
|
counter can wrap and alias the last seen value — one missed wake per
|
||||||
|
2³² commits. Hint-signal semantics absorb it (consumers re-read;
|
||||||
|
subsequent commits re-wake). Not actionable.
|
||||||
|
- **W-4 (watcher connection opens RW):** `run_poll_loop` opens with
|
||||||
|
`READ_WRITE`; a `data_version` read is readable RO. Harmless (the
|
||||||
|
watcher legitimately needs write access on non-WAL journals' busy
|
||||||
|
paths? unproven) — noted so the fork's port makes a deliberate
|
||||||
|
choice rather than inheriting one.
|
||||||
|
|
||||||
|
## 3. The published-0.5.0 defect register (what the fork fires on)
|
||||||
|
|
||||||
|
Cites prefixed `v0.5.0:` are into the published tag's sources (read
|
||||||
|
directly from the tag); HEAD cites are into the reference checkout.
|
||||||
|
"Would-be engine impact" assumes the ADR-003/ADR-010 ride posture
|
||||||
|
(consuming these functions through the tx seam).
|
||||||
|
|
||||||
|
- **D-1 — `fail()` destroys a job and reports "not our claim."**
|
||||||
|
(`v0.5.0:honker-core/src/honker_ops.rs:911-948`) `fail` runs
|
||||||
|
`DELETE … RETURNING` and then swallows the read with `.ok()` (:934):
|
||||||
|
a decode/mapper failure is converted to `Ok(0)` *after the DELETE has
|
||||||
|
executed inside that statement* — the row is gone from `_honker_live`,
|
||||||
|
the `_honker_dead` INSERT never runs, the caller is told the claim
|
||||||
|
wasn't theirs. Silent job loss on any decode failure or I/O error
|
||||||
|
mid-statement. Upstream: `4881f27` (error propagation) + `3ab43aa`
|
||||||
|
(savepoint) — fixed at HEAD, unreleased. The upstream CHANGELOG
|
||||||
|
states it outright: "a failed `fail()` leaves the job claimable
|
||||||
|
instead of destroyed."
|
||||||
|
- **D-2 — dead-letter moves are not atomic.**
|
||||||
|
(`v0.5.0:honker_ops.rs:607-650` `dead_letter_exhausted_claimable`,
|
||||||
|
`:1079-1119` `sweep_expired`, and retry's dead-letter branch
|
||||||
|
`:890-907`) DELETE-then-INSERT with no savepoint; a mid-loop INSERT
|
||||||
|
failure leaves rows deleted from `_honker_live` whose `_honker_dead`
|
||||||
|
INSERT never came — stranded between tables. (The pre-claim path in
|
||||||
|
`claim_batch` :663 uses it too.) Upstream fixed all four sites with
|
||||||
|
the `in_savepoint` train.
|
||||||
|
- **D-3 — `retry` swallow.** (`v0.5.0:honker_ops.rs:866`) The claim
|
||||||
|
read ends `.ok()`: a SQLite error maps to `Ok(0)` ("not our claim")
|
||||||
|
while the row sits `processing` with a dead worker — silently
|
||||||
|
extending the dual-execution window until a reclaimer arrives.
|
||||||
|
- **D-4 — `lock_acquire` swallow.** (`v0.5.0:honker_ops.rs:1146`) The
|
||||||
|
owner read-back ends `.ok()`: a broken database reports the lock as
|
||||||
|
*not acquired* — if leadership acquisition rides it, the leader
|
||||||
|
silently never starts (a stall, not an error). The scheduler's
|
||||||
|
`run_schedules` would have inherited this via the leadership-lock
|
||||||
|
path.
|
||||||
|
- **D-5 — `get_job` swallow.** (`v0.5.0:honker_ops.rs:1016`) Error
|
||||||
|
reported as job-miss. Diagnosis-by-API under a broken stored value
|
||||||
|
lies.
|
||||||
|
- **D-6 — no zombie reachability** (both 0.5.0 and HEAD): an expired
|
||||||
|
*processing* row whose worker died is unreachable by the claim
|
||||||
|
predicate (HEAD :878), by pre-claim dead-lettering (HEAD :792), and by
|
||||||
|
`sweep_expired` (pending-only, HEAD :1346) — the ADR-010 §5
|
||||||
|
no-stranded-rows property fails upstream of anything we control.
|
||||||
|
(This is fork candidate #1.)
|
||||||
|
- **D-7 — schema gaps vs contract v1** (0.5.0; mostly HEAD too): no
|
||||||
|
`claimed_at` in 0.5.0's schema; no per-job stamps anywhere
|
||||||
|
(visibility/backoff/retention columns of ADR-010 §3a); `get_job`
|
||||||
|
reads live-rows only (HEAD :1237-1240 — fork candidate #2) and
|
||||||
|
exposes neither stamps nor `claimed_at` in its JSON (HEAD's #136
|
||||||
|
tracks claimed_at exposure as open).
|
||||||
|
|
||||||
|
The POCs measured happy paths, commit/rollback atomicity, concurrency,
|
||||||
|
and latency; D-1..D-5 are failure-interrupted windows and
|
||||||
|
corrupted-value paths — exactly the class POC measurement does not
|
||||||
|
surface.
|
||||||
|
|
||||||
|
## 4. Schema / migration brittleness — verdict: minor
|
||||||
|
|
||||||
|
- **Bootstrap** (`lib.rs:441-510`): `CREATE TABLE IF NOT EXISTS` +
|
||||||
|
`ALTER TABLE ADD COLUMN` migrations guarded by
|
||||||
|
`pragma_table_info`. The concurrent-bootstrap "duplicate column"
|
||||||
|
race is handled honestly (SQLite serializes writes file-wide; the
|
||||||
|
loser swallows that specific error) — but the swallow keys on a
|
||||||
|
lowercase **error-string match** (`.contains("duplicate column")`),
|
||||||
|
brittle to SQLite message rewording. A rewording turns a benign race
|
||||||
|
into a loud bootstrap error (recoverable by retry) — not a
|
||||||
|
correctness hole.
|
||||||
|
- No `schema_version` table; migrations are append-column-only,
|
||||||
|
per-table, in-code. Adequate at this scale (three columns added
|
||||||
|
over the project's life).
|
||||||
|
- **The co-migration exposure:** realizing contract v1's `claimed_at`
|
||||||
|
(get_job, ADR-010 §1) and opt stamps (§3a) over honker's tables means
|
||||||
|
the engine runs its own migrations over `_honker_*` — two codebases
|
||||||
|
(ours and honker-core's) migrating one table family, with only an
|
||||||
|
exact version pin holding the seam stable. Either posture must
|
||||||
|
resolve this; the fork resolves it by unifying ownership.
|
||||||
|
|
||||||
|
## 5. The fork calculus — the two candidates, plus the §3a stamps
|
||||||
|
|
||||||
|
Framing per OQ-06: complement-over-machinery vs fork, on the concrete
|
||||||
|
candidates.
|
||||||
|
|
||||||
|
- **Zombie fix (D-6):** complement is genuinely small — one engine-side
|
||||||
|
SQL function over honker's own tables (move expired-processing rows
|
||||||
|
to dead; the pending half exists, the processing half doesn't).
|
||||||
|
Achievable over the machinery.
|
||||||
|
- **Dead-visible `get_job` (D-7):** complement is small *nominally* —
|
||||||
|
but contract `get_job` also carries `claimed_at` (absent from 0.5.0's
|
||||||
|
schema → engine `ALTER TABLE` over honker's table, or a sidecar) and
|
||||||
|
the stamps. Achievable, but it crosses schema ownership.
|
||||||
|
- **Stamps (ADR-010 §3a):** per-row visibility deadlines honored in a
|
||||||
|
**single claim statement** (the pinned deadline semantics) require
|
||||||
|
rows carrying their stamps and a claim statement that reads them —
|
||||||
|
i.e. engine-owned enqueue + claim SQL in *any* posture. The
|
||||||
|
over-machinery alternative (claim with the uniform queue timeout, then
|
||||||
|
re-stamp per row post-claim) is a two-statement bridge that bends the
|
||||||
|
pinned "deadline = the job's stamp" semantics for one bounded window —
|
||||||
|
the kind of contract-honesty fudge ADR-010 explicitly worked to
|
||||||
|
avoid.
|
||||||
|
|
||||||
|
**The pivot:** once §3a and the contract `get_job` land, the engine
|
||||||
|
owns enqueue / claim / retry / fail / sweep / get_job — the majority of
|
||||||
|
the queue-op surface — in **both** postures. The ride then covers
|
||||||
|
ack/heartbeat/cancel/locks/streams/notify/scheduler plus plumbing
|
||||||
|
(watcher, Writer/Readers, pragmas, bootstrap). The fork question
|
||||||
|
reduces to: *own the remaining half, or keep depending on it?*
|
||||||
|
|
||||||
|
The artifact fact (§1) decides it: the half you'd keep riding carries
|
||||||
|
confirmed silent-job-loss windows in its published form (D-1..D-5),
|
||||||
|
its fixes are stranded unreleased on a bindings' cadence, its crate
|
||||||
|
description says "not intended for direct use" (its intended consumers
|
||||||
|
are three language bindings, not direct library consumption — no
|
||||||
|
upstream support contract for our posture), and the co-migration
|
||||||
|
exposure keeps the schema dual-owned in the complement posture. The
|
||||||
|
fork also inherits ~4,000 lines of upstream tests (savepoint, watcher,
|
||||||
|
multiprocess pressure suites) — a large share of the fork cost is
|
||||||
|
pre-paid.
|
||||||
|
|
||||||
|
**Rejected postures, recorded for revisit:**
|
||||||
|
|
||||||
|
- *Wait for upstream 0.5.1+:* unbounded timing; the read's facts stand
|
||||||
|
for the state actually measured.
|
||||||
|
- *Git-dependency pinned to upstream main:* pins an unreleased branch
|
||||||
|
as a production substrate, still lacks every contract delta and the
|
||||||
|
zombie fix (both present at f4e53c6 — verified), still dual-owned
|
||||||
|
schema; adds workspace/monorepo resolution gymnastics. Rejected.
|
||||||
|
- *Complement v2 (engine-owned queue SQL over honker's tables, keep
|
||||||
|
honker for the rest):* the strongest alternative — recorded with its
|
||||||
|
genuine merits (zero vendored code; the defect functions are simply
|
||||||
|
never called) — and rejected on the dual-owner schema, dead-weight
|
||||||
|
dependency, release-cadence hostage, and direct-use-unnovation
|
||||||
|
factors above.
|
||||||
|
|
||||||
|
## 6. Fork scope (feeds ADR-011)
|
||||||
|
|
||||||
|
- **Fork at the f4e53c6 state** (the full fix train included), not the
|
||||||
|
published tag — provenance: honker-core, MIT OR Apache-2.0, upstream
|
||||||
|
commit `f4e53c6` (package `node-v0.5.1-10-gf4e53c6`), recorded per
|
||||||
|
AGENTS.md §3.
|
||||||
|
- **Keep as-ported:** the PRAGMA block + `set_journal_mode_wal` retry
|
||||||
|
logic; `Writer`; `Readers`; the polling watcher + `SharedUpdateWatcher`
|
||||||
|
+ `WatcherDeathGuard` + `stat_identity` dead-man's switch (W-1
|
||||||
|
backoff, W-2 fallible spawn applied in port); the `in_savepoint` /
|
||||||
|
`UnwindUndo` machinery as the mutation-transition discipline; the
|
||||||
|
REAL-coercion arg helpers; the `notify()` scalar + notifications
|
||||||
|
table (renamed, plus an at-attach pruning cap implementing ADR-010
|
||||||
|
§6's engine-internal hygiene); stream functions; lock functions
|
||||||
|
(with the same-owner-TTL-not-refreshed behavior documented as-is —
|
||||||
|
contract pinning already covers it via the locks guarantee row).
|
||||||
|
- **Re-derive on contract v1 (ADR-010):** enqueue + stamping;
|
||||||
|
single-statement claim with per-row visibility from stamps;
|
||||||
|
savepoint-guarded retry/fail/dead-letter/sweep; both-states
|
||||||
|
no-stranded-rows `sweep_expired` (+ `dead_letter_retention_s`
|
||||||
|
deletion); dead-visible `get_job` (stamps + `claimed_at` +
|
||||||
|
`last_error`/`died_at`); scheduler tick over the new enqueue with
|
||||||
|
`@every` next-boundary math (numeric, a few lines) — cron boundary
|
||||||
|
machinery not ported.
|
||||||
|
- **Drop, do not port:** `cron.rs` (ADR-009 rejects cron strings); the
|
||||||
|
`kernel-watcher` / `shm-fast-path` features and their optional deps
|
||||||
|
(`notify`, `memmap2`, `libc`); the rate-limit and result tables
|
||||||
|
(cut-flags, ADR-002); the superseded queue functions.
|
||||||
|
- **Table naming:** `_honker_*` → `__alkstore_*` across the fork's
|
||||||
|
storage surface — ADR-010 §8 pre-authorized the fork re-owning names
|
||||||
|
("nothing consumer-visible changes"); storage-internal per ADR-008 §4.
|
||||||
|
- **Tests:** inherit honker-core's suites as the floor (PRAGMA/WAL,
|
||||||
|
watcher lifecycle + failure handling, savepoint + multiprocess
|
||||||
|
pressure) and add the contract-property tests (no-stranded-rows,
|
||||||
|
dead-visible `get_job`, stamps immutability, wake-latency floor) —
|
||||||
|
the core-contract verification backlog's SQLite column.
|
||||||
|
|
||||||
|
## 7. Follow-through
|
||||||
|
|
||||||
|
- [ADR-011](../architecture/decisions/011-sqlite-substrate-fork.md) —
|
||||||
|
the fork decision and packaging.
|
||||||
|
- [ADR-005](../architecture/decisions/005-dependency-ownership.md) —
|
||||||
|
posture row for honker-core annotated (trigger fired); the calculus
|
||||||
|
is applied, not renegotiated.
|
||||||
|
- [ADR-003](../architecture/decisions/003-sqlite-driver.md) —
|
||||||
|
"published honker-core" is superseded on ownership only; driver,
|
||||||
|
seam, and watcher architecture are unchanged (the fork inherits them
|
||||||
|
verbatim where kept).
|
||||||
|
- [engine-sqlite.md](../architecture/engine-sqlite.md) — living spec
|
||||||
|
updated to the forked substrate.
|
||||||
|
- OQ-06 marked resolved in [open-questions.md](../architecture/open-questions.md)
|
||||||
|
with a short form + reference to this document.
|
||||||
|
- Revisit note for whoever picks this up later: upstream fixed the
|
||||||
|
defect class once pressed (issues #80/#133 → commits at HEAD). If a
|
||||||
|
future upstream release ships the train *and* moves toward contract
|
||||||
|
shapes we re-derived, re-adoption is conceivable — but by then the
|
||||||
|
fork is owned, tested, and ahead on contract deltas; re-adoption
|
||||||
|
would have to beat it on maintenance, not on novelty.
|
||||||
@@ -484,4 +484,15 @@ idle-poll fallback (default 5s) alongside data_version wake
|
|||||||
honker_ops.rs:1002, 1119-1120, 1553-1562).
|
honker_ops.rs:1002, 1119-1120, 1553-1562).
|
||||||
- `queue_next_claim_at` as the sleep-computing claim counterpart
|
- `queue_next_claim_at` as the sleep-computing claim counterpart
|
||||||
(zero-poll idle workers).
|
(zero-poll idle workers).
|
||||||
- Savepoint-safe multi-statement mutations.
|
- Savepoint-safe multi-statement mutations.
|
||||||
|
|
||||||
|
## 10. Aftermath — where these findings landed
|
||||||
|
|
||||||
|
OQ-06 consumed this document and went further: a follow-on quality
|
||||||
|
read ([quality-read-honker-core.md](quality-read-honker-core.md))
|
||||||
|
cross-checked the *published* crates.io artifact against this checkout
|
||||||
|
and found the savepoint/error-propagation fix train (§8 items 11, the
|
||||||
|
CHANGELOG "Unreleased" work) unpublished — firing ADR-005's fork
|
||||||
|
trigger ([ADR-011](../architecture/decisions/011-sqlite-substrate-fork.md)).
|
||||||
|
The defect list in §8 is now the fork's fix/inheritance register; this
|
||||||
|
document remains the accurate read of the reference revision.
|
||||||
Reference in new issue
Block a user