Skip to content

bench(W13): Phase 11.11b — concurrent-writers workload (SQLR-22 / SQLR-16)#133

Merged
joaoh82 merged 1 commit into
mainfrom
worktree-phase-11-11b-bench
May 11, 2026
Merged

bench(W13): Phase 11.11b — concurrent-writers workload (SQLR-22 / SQLR-16)#133
joaoh82 merged 1 commit into
mainfrom
worktree-phase-11-11b-bench

Conversation

@joaoh82
Copy link
Copy Markdown
Owner

@joaoh82 joaoh82 commented May 11, 2026

Summary

  • Adds the headline Phase-11 differentiator workload (W13 — concurrent writers, mostly-disjoint rows) to the SQLR-16 benchmark harness under a new Group D.
  • 4 worker threads × 50 BEGIN/UPDATE/COMMIT cycles each, random rowid in 1..=1000 (~ 0.4% collision per op). SQLRite drives BEGIN CONCURRENT across sibling Connection::connect handles; SQLite drives BEGIN IMMEDIATE across separate rusqlite::Connection handles serializing through the WAL writer lock.
  • The Driver trait grows four optional methods (each with a sensible default so W1..W12 are untouched): connect_sibling, enable_concurrent_mode, concurrent_begin_sql, is_retryable_busy. SQLRite overrides all four; SQLite uses the defaults plus a new busy_timeout = 5s pragma at open.
  • Correctness gate: after a 4×10 burst, SUM(n) over the counters table must equal n_workers × txs_per_worker. Two new lib tests (w13_sqlrite_correctness, w13_sqlite_correctness) exercise it in ~1.2s without the criterion harness overhead.

Why this is split from Phase 11.11

The original 11.11 bundled the REPL .spawn work, this bench workload, and a Go SDK cross-pool sibling shape into one slice. They diverged in scope:

  • 11.11a (shipped) — REPL .spawn for interactive demos.
  • 11.11b (this PR) — W13 bench workload.
  • 11.11c (deferred) — Go SDK cross-pool sibling shape; touches the cgo + database/sql driver architecture and is genuinely independent of either.

Per-engine specifics

Method (defaults shown) SQLRite override SQLite override
connect_sibling(primary, path) → open(path) primary.connect() (in-process sibling, shared Arc<Mutex<Database>>) default — fresh rusqlite::Connection::open(path)
enable_concurrent_mode(conn) → () PRAGMA journal_mode = mvcc; default — WAL + busy_timeout already set at open
concurrent_begin_sql() → "BEGIN" "BEGIN CONCURRENT" "BEGIN IMMEDIATE"
is_retryable_busy(err) → false downcast to SQLRiteError::is_retryable() walk anyhow chain for DatabaseBusy / DatabaseLocked

Both engines run the same retry-on-busy outer loop. Only SQLRite actually exercises the retry path under this workload's collision rate; SQLite's busy_timeout makes its BEGIN block rather than fail.

Subtle pitfalls hit during implementation

  1. Anyhow source preservation. The SQLRite driver's existing .map_err(|e| anyhow::anyhow!("…{e}…")) pattern in execute / execute_with_params was dropping the typed SQLRiteError source — is_retryable_busy couldn't downcast. Switched to .map_err(anyhow::Error::new).with_context(...) so the typed source survives. (Other callers don't depend on the downcast; the change is invisible to them.)
  2. SQLRite's exclusive flock. SQLRite's Connection::open takes flock(LOCK_EX) on the WAL sidecar. Workers 2-N can't open() the same path — they'd fail to acquire the lock. Solved via connect_sibling, which SQLRite overrides to call primary.connect(). Default opens a fresh connection (works for SQLite under WAL).
  3. MVCC mode is in-memory state. PRAGMA journal_mode doesn't persist to disk; every fresh Connection::open resets to the default (Wal). The W13 setup enables it once, then run_concurrent re-opens the file at each criterion sample — so we re-enable on every primary acquisition before any worker issues a BEGIN CONCURRENT. (Caught by the correctness gate failing with "BEGIN CONCURRENT requires PRAGMA journal_mode = mvcc; first".)

Docs

  • docs/benchmarks-plan.md — new Group D section; sub-phase 9.7; post-9.7 ideas list with W13 concurrency-curve sweep + W13b hot-row contention follow-ups.
  • benchmarks/README.md — Group D entry; new W13 section explaining per-engine BEGIN flavour + connection model + correctness gate.
  • docs/roadmap.md — Phase 11.11b promoted to ✅ shipped; new Phase 11.11c entry for the deferred Go SDK work.

Test plan

  • cargo build --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --exclude sqlrite-benchmarks --all-targets
  • cargo test --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --exclude sqlrite-benchmarks — 615/615
  • cargo test -p sqlrite-benchmarks --lib w13 — both drivers' correctness gates pass in ~1.2s
  • cargo build -p sqlrite-benchmarks --benches — W13 registers cleanly
  • cargo fmt --all -- --check
  • cargo clippy --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --exclude sqlrite-benchmarks --all-targets
  • cargo clippy -p sqlrite-benchmarks --all-targets — no new errors
  • cargo doc --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --exclude sqlrite-benchmarks --no-deps
  • CI green
  • Pinned-host run + commit headline numbers (make bench locally on the owner's host; lands in a follow-up PR alongside the regular bench-republish cadence)

🤖 Generated with Claude Code

…R-16)

Adds the headline Phase-11 differentiator workload to the SQLR-16
benchmark harness. Splits the original 11.11 scope: this slice ships
the bench workload only; the Go SDK cross-pool sibling shape is now
Phase 11.11c (a separate slice because it touches the Go binding
architecture, not the bench harness).

Workload shape (`benchmarks/src/workloads/concurrent_writers.rs`):

  CREATE TABLE counters (id INTEGER PRIMARY KEY, n INTEGER NOT NULL);
  -- preload 1_000 rows with n=0
  -- 4 worker threads × 50 BEGIN/UPDATE/COMMIT cycles each
  --   BEGIN <CONCURRENT|IMMEDIATE>;
  --   UPDATE counters SET n = n + 1 WHERE id = ?;  -- random in 1..=1000
  --   COMMIT;                                     -- retry on Busy/Locked

Per-engine specifics flow through four new `Driver` trait methods,
each with a sensible default so existing workloads (W1..W12) and
drivers are untouched:

- `connect_sibling(primary, path)` — SQLRite overrides to call
  `Connection::connect()` so workers share the primary's
  `Arc<Mutex<Database>>` (SQLRite's `Connection::open` takes
  `flock(LOCK_EX)`, so workers can't re-open). Default opens a
  fresh connection at `path` — works for SQLite (separate
  connections coexist under WAL).
- `enable_concurrent_mode(conn)` — SQLRite runs
  `PRAGMA journal_mode = mvcc;`. Default no-op (SQLite's WAL +
  busy_timeout setup happens at `open`).
- `concurrent_begin_sql()` — SQLRite returns `"BEGIN CONCURRENT"`,
  SQLite returns `"BEGIN IMMEDIATE"`. Default `"BEGIN"`.
- `is_retryable_busy(err)` — SQLRite downcasts to
  `SQLRiteError::is_retryable()`; SQLite walks the anyhow chain for
  rusqlite's `DatabaseBusy` / `DatabaseLocked`. Default false.

SQLite driver gains a `busy_timeout = 5s` pragma at open so its
`BEGIN IMMEDIATE` blocks rather than fails on contention. SQLRite
driver's `.map_err(|e| anyhow::anyhow!(...))` pattern in `execute`
/ `execute_with_params` was dropping the typed source — switched to
`.map_err(anyhow::Error::new).with_context(...)` so the
`SQLRiteError` survives anyhow wrapping and `is_retryable_busy`
can downcast.

Correctness gate: after a 4×10 burst, `SUM(n)` over the counters
table must equal `n_workers × txs_per_worker`. Catches lost
commits, double-counted retries, mis-handled Busy boundaries.
Two new tests (`w13_sqlrite_correctness`, `w13_sqlite_correctness`)
exercise the gate without the criterion harness overhead — useful
because the full criterion suite has a long setup gauntlet for
W1-W12 even when filtered to W13 only.

Registered in `benches/suite.rs` under sample-size 20; criterion's
JSON envelope will pick up `W13.v1` rows for both drivers.
Pinned-host headline numbers will land with the first
re-publication; v1 ships the workload + correctness gate so any
future numbers stand on a verified base.

Docs:
- `docs/benchmarks-plan.md` — new Group D section with the W13 row;
  new sub-phase 9.7; post-9.6 ideas section moves to post-9.7 with
  W13 concurrency-curves + W13b hot-row contention added.
- `benchmarks/README.md` — Group D added to the workload taxonomy;
  W13 section explains the per-engine BEGIN flavour + connection
  model + correctness gate.
- `docs/roadmap.md` — Phase 11.11b promoted to ✅ shipped; new
  Phase 11.11c entry for the deferred Go SDK cross-pool sibling
  shape.

Workspace: 615/615 Rust tests still pass; cargo fmt + clippy + doc
all clean. `cargo test -p sqlrite-benchmarks --lib w13` passes both
drivers in ~1.2s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rust-sqlite Ready Ready Preview, Comment May 11, 2026 0:23am

Request Review

@joaoh82 joaoh82 merged commit b3cd155 into main May 11, 2026
17 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant