fix(op-log): serialize SQLite adapter transactions on the shared connection (#8849)

* fix(op-log): serialize SQLite adapter transactions on shared connection

The SQLite op-log adapter issued raw BEGIN/COMMIT on the shared
connection with no serialization. Once both stores share one SqliteDb
(the staged native rollout), concurrent operations (capture append,
archive write, compaction) would interleave BEGINs: SQLite has no
nested transactions, so a second BEGIN throws and a bare statement
issued mid-transaction silently joins — and rolls back with — the
foreign transaction, corrupting op-log state.

Funnel every adapter entry point through an internal FIFO queue so a
transaction is exclusive on the connection for its whole BEGIN/COMMIT
and no bare operation interleaves. Transaction-internal work runs
directly on the connection (already holding the slot), so there is no
re-entrancy. Document the mutual-exclusion invariant in the port
contract and add a concurrent-transactions contract test that runs on
both the in-memory fake and real sql.js.

* docs: add complete architecture review report (2026-07-07)

Whole-app architecture review synthesized from eight parallel subsystem
reviews and an adversarial verification pass; findings filed as issues
#8832-#8843, with duplicates cross-referenced rather than re-filed.

* fix(op-log): key the SQLite transaction serializer to the connection

Multi-agent review found the serializer was keyed to the adapter
instance. The native rollout hands the op-log store and archive store
two separate SqliteOpLogAdapter instances over one shared SqliteDb, so
per-instance queues left an op-log BEGIN free to interleave with a
concurrent archive BEGIN on the shared connection — the exact hazard the
serializer targets.

Key the FIFO queue to the connection (WeakMap<SqliteDb>) so every adapter
over one SqliteDb shares one queue. Add a contract test that drives two
adapters over one connection concurrently (verified red with per-instance
keying, green with per-connection). Also: document the re-entrancy
precondition as unenforced (a lint rule, not a runtime flag, is the right
guard — a flag cannot distinguish a re-entrant call from a legal
concurrent one) and correct init/getLastSeq/port-contract doc drift.
This commit is contained in:
Johannes Millan 2026-07-07 18:00:25 +02:00 committed by GitHub
parent 0d7e011081
commit 05f6bd27d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 466 additions and 37 deletions

View file

@ -157,7 +157,24 @@ captured on the next tick.
**off**. The factory must hand **both** services' adapters the **same**
`SqliteDb` (one SQLite file, all tables) — mirroring how they share one IDB
connection today. Needs B1 (the native `SqliteDb` wrapper) first.
- **Size:** tiny token flip (init change done). **Risk:** gated by the flag.
- ✅ **Shared-connection safety (prerequisite for the flip, landed).** With both
services on one `SqliteDb`, concurrent op-log operations (capture append vs.
archive write vs. compaction) would otherwise interleave `BEGIN`s on the one
connection — SQLite has no nested transactions, so a second `BEGIN` throws and
a bare statement issued mid-transaction joins (and rolls back with) the foreign
transaction. `SqliteOpLogAdapter` now funnels every entry point through a FIFO
queue keyed to the **shared connection** (`WeakMap<SqliteDb, …>`, not the
adapter instance) — so the op-log store's and archive store's separate adapters
over the one `SqliteDb` serialize against **each other**, and a transaction is
exclusive on the connection for its whole `BEGIN…COMMIT`. The port contract
documents the invariant; contract tests cover concurrent transactions on both
engines **and** the two-adapters-one-connection topology. (Closes the H-6/#8746
rollout blocker.) Residual: the re-entrancy precondition (a `transaction()`
callback must use its `tx` handle, never re-enter an adapter method, or it
deadlocks on the queue) is documented but unenforced — a lint rule is the right
future guard (a runtime flag can't tell a re-entrant call from a legal
concurrent one).
- **Size:** tiny token flip (init + serialization done). **Risk:** gated by the flag.
---