Skip to content

refactor(plus): extract Litestream into a shared bencher_litestream crate#901

Open
epompeii wants to merge 2 commits into
develfrom
u/ep/bencher-litestream
Open

refactor(plus): extract Litestream into a shared bencher_litestream crate#901
epompeii wants to merge 2 commits into
develfrom
u/ep/bencher-litestream

Conversation

@epompeii

Copy link
Copy Markdown
Member

What

Extracts the Litestream integration into a new Bencher Plus crate, plus/bencher_litestream, shared by the API server (and, next, an external consumer by path). The code is moved, not rewritten, so behavior and the OpenAPI schema are unchanged.

Today the Litestream code lives in three places:

  • Config types + JSON-to-YAML rendering in bencher_json (system/config/plus/litestream.rs)
  • Restore/replicate runtime in services/api/src/main.rs (run_litestream, LitestreamError)
  • The autocheckpoint PRAGMA in bencher_config (config_tx.rs)

This PR consolidates all three into one crate.

The crate

bencher_litestream is feature-gated so config-only consumers stay light:

  • default: config types (JsonLitestream, JsonReplica, ...) + Sanitize
  • schema: JsonSchema derives for the OpenAPI spec
  • yaml: into_yaml rendering
  • runtime: run_litestream restore-then-replicate supervision (implies yaml)

It also exposes DISABLE_AUTOCHECKPOINT_PRAGMA and LitestreamLevel (with From<LogLevel> kept in bencher_json).

Consumers

  • bencher_json re-exports the config types at their existing paths, so bencher_json::system::config::JsonLitestream still resolves and the schema is unchanged.
  • services/api calls bencher_litestream::run_litestream, computing the database path, config path, and log level at the call site.
  • bencher_config uses bencher_litestream::DISABLE_AUTOCHECKPOINT_PRAGMA.

Notes

  • run_litestream now takes its inputs directly (db_path, config_path: Utf8PathBuf, log_level: LitestreamLevel) instead of &Config. A new LitestreamError::DatabasePathNotUtf8 variant covers the call-site path conversion.
  • All four Dockerfiles are updated for the new crate (the API copies it; cli/console/bench stub it for workspace resolution). CI path filters already glob plus/**.

Verification

  • cargo gen-spec and cargo gen-ts produce no diff (schema neutral).
  • cargo clippy --no-deps --all-targets --all-features -- -Dwarnings clean.
  • cargo nextest run --all-features (the moved unit tests pass) and cargo test --doc green.
  • cargo check --no-default-features compiles.

Not covered by CI: a live restore/replicate smoke test against an S3/R2 bucket.

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

PR: #901
Base: devel
Head: u/ep/bencher-litestream
Commit: 9fc0f32ab66547e66f0481f2a5035fabd5878885


Code Review: Extract Litestream into shared bencher_litestream crate

A clean, well-executed refactor. The Litestream wire types, YAML rendering, and process supervision move out of bencher_json/services/api into a new plus/bencher_litestream crate, shared by path. I verified that services/api/openapi.json and services/console/src/types/bencher.ts are unchanged, confirming the wire/schema contract was preserved. The historical bencher_json::system::config::... paths are kept via re-exports, and the From<LogLevel> impl stays in bencher_json to respect the orphan rule. All four Dockerfiles were updated per CLAUDE.md.

Findings

1. CI path filter gap (should fix) — .github/workflows/ci.yml
CLAUDE.md requires updating the changes job path filters when adding a crate. The rust filter covers the new crate via plus/**, but the cli filter enumerates individual crates and is missing plus/bencher_litestream/**. The CLI does compile it: services/cli enables bencher_json/plusdep:bencher_litestream, and services/cli/Dockerfile COPYs the real source (not a stub). A change isolated to bencher_litestream would therefore skip CLI checks. Add:

            cli:
              - 'services/cli/**'
              - 'lib/bencher_adapter/**'
              ...
              - 'plus/bencher_litestream/**'

2. bencher_adapter default features dropped (note)
Changing bencher_json = { features = ["server"] } to bencher_json.workspace = true (with new server/client features, default empty) is correct for the workspace build thanks to feature unification, and the bench.Dockerfile was rightly updated with --features server for its isolated stub build. Just note that cargo nextest run -p bencher_adapter (or cargo test --doc -p bencher_adapter) in isolation now needs --features server or it fails with the undeclared type Regex error documented in CLAUDE.md. Consistent with the project's stated pattern, so acceptable.

3. Error type spans the crate boundary (minor)
LitestreamError::{Database, DatabasePathNotUtf8, RestoreRecv, JoinHandle} are defined in bencher_litestream::runtime but only ever constructed by the caller in services/api/src/main.rs (path absolutization and handle-join moved to the caller). This works since they're pub, but the error enum now mixes crate-internal and caller-only variants. Reasonable as a shared error vocabulary; worth a one-line comment noting some variants are caller-constructed.

4. Incomplete re-export set (minor) — litestream.rs
pub use bencher_litestream::{JsonCheckpoint, JsonLitestream, JsonReplica, LitestreamLevel}; omits JsonSnapshot and JsonValidation, which are public fields of JsonLitestream. They're only populated via deserialization today, so nothing breaks, but re-exporting all six keeps the public surface consistent.

5. Mixed path types in yaml.rs (pre-existing, low priority)
LitestreamDb.path is now Utf8PathBuf (good, per CLAUDE.md), but LitestreamReplica::File { path } and Sftp { key_path } remain std::path::PathBuf. These come straight from JsonReplica's wire types, so converting them has a schema cost and is fine to leave; just flagging the inconsistency.

Compliance / positives

  • Tests moved with the code; pretty_assertions correctly relocated to the new crate's dev-deps and dropped from bencher_json; stale use serde_yaml as _; removed.
  • Feature gating (yaml, runtime, server/client/schema) is granular and the ?/feature optional-propagation wiring in bencher_json is correct.
  • Errors use thiserror and wrap original error types; no anyhow/Box<dyn Error> introduced; structs are destructured in into_yaml.
  • run_litestream's new "db_path must be absolute" precondition is documented, and the caller upholds it.

Note: I could not run cargo clippy/nextest (build commands require approval here), so the above is from static analysis plus the confirmed-unchanged generated artifacts. Recommend running cargo clippy --all-features -- -Dwarnings, cargo check --no-default-features, and cargo nextest run -p bencher_litestream --all-features before merge.


Model: claude-opus-4-8

@epompeii epompeii force-pushed the u/ep/bencher-litestream branch 2 times, most recently from 91aca73 to a268bc6 Compare June 17, 2026 03:54
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchu/ep/bencher-litestream
Testbedintel-v1
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
microseconds (µs)
(Result Δ%)
Upper Boundary
microseconds (µs)
(Limit %)
Adapter::Json📈 view plot
🚷 view threshold
4.69 µs
(+2.24%)Baseline: 4.59 µs
4.84 µs
(97.01%)
Adapter::Magic (JSON)📈 view plot
🚷 view threshold
4.56 µs
(+2.04%)Baseline: 4.47 µs
4.69 µs
(97.26%)
Adapter::Magic (Rust)📈 view plot
🚷 view threshold
25.50 µs
(+0.16%)Baseline: 25.45 µs
26.44 µs
(96.42%)
Adapter::Rust📈 view plot
🚷 view threshold
3.51 µs
(+0.99%)Baseline: 3.47 µs
3.59 µs
(97.68%)
Adapter::RustBench📈 view plot
🚷 view threshold
3.51 µs
(+1.15%)Baseline: 3.47 µs
3.58 µs
(98.00%)
🐰 View full continuous benchmarking report in Bencher

@epompeii epompeii force-pushed the u/ep/bencher-litestream branch 2 times, most recently from 6361e77 to b318ee5 Compare June 17, 2026 04:30
…rate

Move the Litestream config types, YAML rendering, restore/replicate
runtime, and the autocheckpoint PRAGMA out of bencher_json and the API
server into a new Bencher Plus crate, plus/bencher_litestream, so the
code can be shared by path with other consumers.

- bencher_json re-exports the config types from the new crate at their
  existing paths and keeps the From<LogLevel> conversion, so the public
  API and the OpenAPI schema are unchanged.
- services/api calls bencher_litestream::run_litestream, computing the
  database path, config path, and log level at the call site.
- bencher_config uses bencher_litestream::DISABLE_AUTOCHECKPOINT_PRAGMA.

The crate is feature-gated so config-only consumers stay light: the
bencher_valid backend is selected via server/client features (threaded
from bencher_json, not hardcoded), and yaml/runtime/schema are opt-in.
The moved unit tests pass and cargo gen-spec / gen-ts produce no diff.
@epompeii epompeii force-pushed the u/ep/bencher-litestream branch from b318ee5 to 3a027aa Compare June 17, 2026 04:55
bencher_adapter hardcoded bencher_json = { features = ["server"] }, which
forced the server bencher_valid backend (regex, sha2, hex, rand) into
every consumer via feature unification, including the client-side CLI
(which links bencher_adapter only for `bencher mock`).

Replace the hardcoded backend with server/client features that thread
bencher_json's choice, mirroring bencher_boundary. Consumers select the
backend through their own bencher_json dependency: the CLI and
bencher_noise resolve to client, the server crates to server. The
standalone bencher_adapter bench build selects server explicitly.

The CLI's bencher_valid now compiles with client only (regex-lite); the
server backend no longer ships in the CLI binary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant