Add bencher_replica: in-process SQLite replication to replace Litestream#929
Add bencher_replica: in-process SQLite replication to replace Litestream#929epompeii wants to merge 1 commit into
Conversation
🤖 Claude Code ReviewPR: #929 Here is my consolidated review. PR Review:
|
|
| Project | Bencher |
| Branch | u/ep/bencher-replica |
| Testbed | intel-v1 |
🚨 3 Alerts
| Benchmark | Measure Units | View | Benchmark Result (Result Δ%) | Upper Boundary (Limit %) |
|---|---|---|---|---|
| Adapter::Magic (Rust) | Latency microseconds (µs) | 📈 plot 🚷 threshold 🚨 alert (🔔) | 26.78 µs(+4.24%)Baseline: 25.69 µs | 26.72 µs (100.20%) |
| Adapter::Rust | Latency microseconds (µs) | 📈 plot 🚷 threshold 🚨 alert (🔔) | 3.87 µs(+10.30%)Baseline: 3.51 µs | 3.71 µs (104.31%) |
| Adapter::RustBench | Latency microseconds (µs) | 📈 plot 🚷 threshold 🚨 alert (🔔) | 3.86 µs(+10.10%)Baseline: 3.50 µs | 3.71 µs (104.07%) |
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.85 µs(+4.02%)Baseline: 4.66 µs | 4.92 µs (98.55%) |
| Adapter::Magic (JSON) | 📈 view plot 🚷 view threshold | 4.72 µs(+4.16%)Baseline: 4.53 µs | 4.73 µs (99.77%) |
| Adapter::Magic (Rust) | 📈 view plot 🚷 view threshold 🚨 view alert (🔔) | 26.78 µs(+4.24%)Baseline: 25.69 µs | 26.72 µs (100.20%) |
| Adapter::Rust | 📈 view plot 🚷 view threshold 🚨 view alert (🔔) | 3.87 µs(+10.30%)Baseline: 3.51 µs | 3.71 µs (104.31%) |
| Adapter::RustBench | 📈 view plot 🚷 view threshold 🚨 view alert (🔔) | 3.86 µs(+10.10%)Baseline: 3.50 µs | 3.71 µs (104.07%) |
4c2c1c5 to
58ac5e2
Compare
58ac5e2 to
473737e
Compare
473737e to
dcd02f4
Compare
dcd02f4 to
203d96b
Compare
203d96b to
24a3762
Compare
24a3762 to
6d14320
Compare
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).
6d14320 to
176b289
Compare
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