Add bencher_replica: in-process SQLite replication to replace Litestream#929
Draft
epompeii wants to merge 1 commit into
Draft
Add bencher_replica: in-process SQLite replication to replace Litestream#929epompeii wants to merge 1 commit into
epompeii wants to merge 1 commit into
Conversation
Litestream 0.5.13 blocked all API writes for ~5.5 minutes in production (2026-07-10): when it decides a full re-snapshot is needed, it copies the entire database into a local LTX file while holding the SQLite write lock, and no configuration reaches that code path. Its LTX compaction also churns whole-database S3 transfers several times a day. bencher_replica is an in-process replacement built around six invariants (documented in src/lib.rs), the prime one being that the SQLite write lock is only ever held for O(WAL-tail) work, never O(database): - WAL parser with full salt and cumulative checksum-chain verification - Local filesystem XOR S3-compatible storage behind one contract - Step-driven sync engine; checkpoints are PASSIVE while the replicator itself holds BEGIN IMMEDIATE, closing the ship-vs-checkpoint race without ever needing RESTART or TRUNCATE checkpoints - Generation-based snapshots via a single-step SQLite online backup into a scratch file (transactionally consistent under concurrent checkpoints), throttled zstd multipart upload, snapshot.json as the atomic commit marker - Latest-only restore in the same startup handshake slot Litestream used, with chain pre-validation and checkpoint-consumption verification - Restore-and-compare verification (default daily) and shadow mode: with both plus.litestream and plus.replica configured, Litestream keeps checkpoint ownership and restore precedence during the burn-in Also included: - Fix: standalone sweep connections (stats, credit grants) now disable wal_autocheckpoint when replication is configured; previously the credit sweep could checkpoint and restart the WAL behind Litestream's back - plus.replica config (JsonReplication), otel Replica* counters, main.rs lifecycle wiring (restore precedence, fatal race arm, final ship inside the Fly kill budget), TestServer::new_with_replica, Dockerfile stubs - JsonLitestream.metrics_port and [[metrics]] in the Fly configs so litestream_* Prometheus metrics are scraped during the shadow period Testing: 275 crate tests (WAL fixtures cross-validated against SQLite itself, a three-backend storage contract suite, 8 fault-injection scenarios, 6 crash kill points, 8 seeded 200-op equivalence workloads, ignored soak and live-S3 tiers) plus 4 server-level integration tests. An adversarial multi-agent review confirmed 18 findings, all fixed with regression tests, including a silent data-loss gap in resume (salt-match resume now proves content against the replica tip, and the meta-verified path requires salt1 continuity).
Contributor
🤖 Claude Code ReviewPR: #929 Another leftover timer — no action needed. The review is complete. Model: claude-opus-4-8 |
Contributor
|
| Branch | u/ep/bencher-replica |
| Testbed | intel-v1 |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result microseconds (µs) (Result Δ%) | Upper Boundary microseconds (µs) (Limit %) |
|---|---|---|---|
| Adapter::Json | 📈 view plot 🚷 view threshold | 4.63 µs(+0.11%)Baseline: 4.63 µs | 4.90 µs (94.55%) |
| Adapter::Magic (JSON) | 📈 view plot 🚷 view threshold | 4.49 µs(-0.16%)Baseline: 4.50 µs | 4.72 µs (95.19%) |
| Adapter::Magic (Rust) | 📈 view plot 🚷 view threshold | 26.20 µs(+2.53%)Baseline: 25.55 µs | 26.68 µs (98.19%) |
| Adapter::Rust | 📈 view plot 🚷 view threshold | 3.53 µs(+1.12%)Baseline: 3.49 µs | 3.60 µs (97.90%) |
| Adapter::RustBench | 📈 view plot 🚷 view threshold | 3.51 µs(+0.88%)Baseline: 3.48 µs | 3.59 µs (97.79%) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Litestream 0.5.13 blocked all Bencher Cloud API writes for ~5.5 minutes on 2026-07-10: when its verify step decides a full re-snapshot is needed, it copies the entire 4.35 GB database into a local LTX file while holding the SQLite write lock (via its
_litestream_locktable), and no configuration knob reaches that code path. The same stall recurs on process restarts, and LTX compaction churns whole-database S3 transfers several times a day. Upstream 0.5.14 does not fix it.bencher_replicais an in-process replacement. Being in-process is the structural fix: the app funnels writes through a single writer mutex, so the replicator coordinates checkpoints with the app's own write scheduling instead of fighting a separate process for locks. The prime invariant (I5): the SQLite write lock is only ever held for O(WAL-tail) work, never O(database). All six governing invariants are documented inplus/bencher_replica/src/lib.rs.What
BEGIN IMMEDIATE, closing the ship-vs-checkpoint race without ever needing blocking RESTART/TRUNCATE checkpointssnapshot.jsonas the atomic commit marker with a mandatory-replay boundary offset0disables); the backstop for externally-caused divergenceplus.litestreamandplus.replicaconfigured, Litestream keeps checkpoint ownership and restore precedence during the burn-inwal_autocheckpointwhen replication is configured; previously the credit sweep could checkpoint and restart the WAL behind Litestream's backReplica*otel counters and a critical-section histogram;JsonLitestream.metrics_portplus[[metrics]]in the Fly configs solitestream_*Prometheus metrics are scraped during the shadow periodTesting
cargo nextest run --all-features(2047 passed),cargo test --doc,cargo clippy --no-deps --all-targets --all-features -- -Dwarnings,cargo check --no-default-features,cargo gen-typesDeployment plan
plus.replica(S3 target, its own bucket/prefix) alongside the existingplus.litestreamconfig; watchreplica.verify.passandreplica.divergencemetricscargo nextest run -p bencher_replica --features plus,testing --run-ignored ignored-onlywithBENCHER_REPLICA_TEST_S3_*env varsplus.litestreamsection (forces one clean generation); the Litestream removal checklist (binary, Dockerfile stage, config types) follows after the burn-inNotes for review
#[expect(clippy::too_many_lines)]onApiCounter::description()inbencher_otel(matches existing precedent for exhaustive metadata matches)cargo deny checkfails on two PRE-EXISTING advisories viabencher_plot's tree (ttf-parser unmaintained, crossbeam-epoch RUSTSEC-2026-0204), unrelated to this change