Skip to content

feat(python): expose spillway controls (open kwargs + runtime setters) - #8

Merged
Xof merged 2 commits into
mainfrom
claude/python-spillway-exposure
May 14, 2026
Merged

feat(python): expose spillway controls (open kwargs + runtime setters)#8
Xof merged 2 commits into
mainfrom
claude/python-spillway-exposure

Conversation

@Xof

@Xof Xof commented May 14, 2026

Copy link
Copy Markdown
Collaborator

Replaces #7 (which was auto-closed when its base branch `claude/python-docs-staleness-catchup` was deleted by PR #6's merge — same head branch, rebased onto current `main`).

Summary

Closes the deliberate v1 omission of spillway controls from the Python binding. The spillway feature shipped on Chisel's `main` 2026-05-04 and stabilized; the binding was holding back with `spillway_max_bytes = 0` (disabled) hard-coded in `python/src/db.rs`. That deferral is now redeemed.

What's exposed (commit-by-commit)

Commit 1: open-time kwargs

```python
chisel.open(
path,
cache_max_bytes=8_388_608, # unchanged
spillway_max_bytes=None, # NEW: None → 1024 × cache_max_bytes
drain_insertion=chisel.DrainInsertion.LruTail, # NEW
create_if_missing=True,
read_only=False,
superblock_count=2,
)
```

  • `spillway_max_bytes: int | None` — None resolves at runtime to `1024 × cache_max_bytes` (matches Rust's default). Explicit 0 disables.
  • `chisel.DrainInsertion` — PyO3 pyclass enum mirroring `chisel::DrainInsertion`. Variants `LruTail` (default) and `Mru`.

Commit 2: between-transaction setters on PyChisel

```python
db.set_cache_max_bytes(4 * 1024 * 1024)
db.set_spillway_max_bytes(0)
db.set_drain_insertion(chisel.DrainInsertion.Mru)
```

All three operate ONLY on between-transactions state — calling any of them mid-transaction raises `TransactionInProgressError` (which is finally a reachable error class on the Python side).

Design choices

Question Decision Rationale
Enum representation PyO3 `#[pyclass]` with `eq, eq_int` Type-safe, native to PyO3
Variant naming Keep Rust names (`LruTail`, `Mru`) Matches ADR-5 cross-references
`spillway_max_bytes` default Sentinel `None` → `1024 × cache_max_bytes` at runtime Matches Rust's computed default; scales correctly if user overrides cache
Setters use `with_inner_mut_io` Same helper as every other mutating method Centralises closed/poisoned check; uniform error semantics

Test plan

  • `maturin develop` — builds clean (after rebase onto main)
  • `pytest` — 82/82 pass (65 pre-existing + 17 new — 8 in test_open.py + 9 in test_runtime_config.py)
  • `cargo clippy --all-targets -- -D warnings` — 90 baseline + 3 new (all the same pre-existing `useless_conversion to the same type: pyo3::PyErr` PyO3/clippy version-boundary noise that every pymethod's `PyResult` return triggers; the 3 new instances are from the 3 new setters following the identical existing pattern)
  • `rustfmt --check src/db.rs` — clean
  • Stubs (`chisel.pyi`) updated with `DrainInsertion` class + new `open()` kwargs + new setter method declarations

Diff scope

```
python/README.md | +46 / -8
python/chisel/init.py | +2 / -1
python/chisel/chisel.pyi | +18 / -0
python/src/db.rs | +84 / -11
python/tests/test_open.py | +66 / -0
python/tests/test_runtime_config.py | +114 / -0 (new file)
```

Xof added 2 commits May 14, 2026 10:45
….open()

The spillway feature landed on the Rust side 2026-05-04 and stabilized;
the Python binding was holding back with `spillway_max_bytes = 0` (the
v1-conservative "disable spillway entirely") hard-coded in src/db.rs.
This exposes both open-time options that the Rust side has shipped,
matching Rust's effective defaults.

Three changes:

1. New PyO3 pyclass enum `DrainInsertion` in src/db.rs mirroring
   `chisel::DrainInsertion`. Two variants `LruTail` (default) and
   `Mru`. Variant names match Rust spelling so cross-referencing
   ARCHITECTURE.md ADR-5 stays mechanical. Registered on the module
   via `m.add_class::<PyDrainInsertion>()`.

2. `chisel.open()` accepts two new keyword-only args:
   - `spillway_max_bytes: int | None = None` — None resolves at
     runtime to `1024 * cache_max_bytes` (Rust default; 8 GiB at the
     8 MiB cache default). Explicit 0 disables and restores
     CacheFull-at-cap. Sentinel-based default is required because
     a literal default could not scale with a user-overridden
     cache_max_bytes.
   - `drain_insertion: DrainInsertion = DrainInsertion.LruTail` —
     the natural Rust default. PyO3 routes the enum value through
     a `From<PyDrainInsertion> for chisel::DrainInsertion` impl.

3. Removed the "v1 spillway disabled, deferred until Rust shipped
   and stabilized" comment in db.rs::open — it was itself stale
   the moment the spillway feature shipped on main.

Python-side updates:
- chisel/__init__.py: re-export DrainInsertion + add to __all__.
- chisel/chisel.pyi: add DrainInsertion class stub + update open()
  signature.
- README.md: update "Opening a database" snippet; new "Cache and
  spillway" section explaining the two new kwargs; flip the
  SpillwayFullError row note from "reserved for future use" to
  "now reachable when the spillway fills."

Tests (tests/test_open.py): 8 new pass — DrainInsertion smoke
checks (variants present, equality semantics) + open() acceptance
tests for spillway=None / spillway=0 / custom spillway / both
drain variants / in-memory with both kwargs.

Verification:
- maturin develop: clean (56 pre-existing PyO3 cfg warnings; no
  new ones from this diff)
- pytest: 73/73 pass (65 pre-existing + 8 new)
- cargo clippy: 90 errors, matching baseline (the 1 `too_many_arguments`
  introduced by adding two kwargs is suppressed by a narrow #[allow]
  on open() with an explanatory comment — keyword-only args can't be
  confused by position, and bundling 8 kwargs into a struct would
  be less ergonomic than kwargs in Python)
- rustfmt: src/db.rs clean (the transaction.rs fmt diff is
  pre-existing on main and untouched by this PR)

Runtime config setters (set_cache_max_bytes / set_spillway_max_bytes
/ set_drain_insertion) intentionally NOT exposed in this PR — the
review only asked about open-time kwargs. Defer that decision.
…n_insertion

Companion to the open-time kwarg exposure earlier in this PR — mirrors
the three between-transaction configuration mutators that already exist
on chisel::Chisel. The setters share the existing with_inner_mut_io
helper, so they inherit:

  - ClosedError if called on a closed handle (closed/poisoned check
    centralised in one place, matching every other mutating method)
  - PoisonedError if the manager is poisoned from an earlier fatal
  - TransactionInProgressError if called mid-transaction (which is
    finally a reachable error class on the Python side; until this
    commit, no API path could produce it)

Methods on PyChisel:
  - set_cache_max_bytes(bytes: int) -> None
  - set_spillway_max_bytes(bytes: int) -> None
  - set_drain_insertion(policy: chisel.DrainInsertion) -> None

Drain-insertion takes the same PyDrainInsertion pyclass enum already
introduced earlier in this PR; PyO3's From<PyDrainInsertion> for
chisel::DrainInsertion impl handles the conversion to the Rust side.

Tests in a new tests/test_runtime_config.py (9 cases): happy-path
for each setter, mid-transaction raises TransactionInProgressError
for each setter, a "failed setter doesn't disturb the active
transaction" check (operational error → engine survives), and a
"closed handle" check confirming the existing closed-detection path
applies uniformly.

README adds a "Runtime configuration" subsection under "Opening a
database" walking through the setter use case + the mid-transaction
restriction. The error-table row for TransactionInProgressError is
flipped from "reserved for future use" to citing the three setters
that now produce it.

Verification:
- maturin develop: clean
- pytest: 82/82 pass (73 prior + 9 new in test_runtime_config.py)
- cargo clippy: 93 errors. 90 are pre-existing PyO3/clippy version-
  boundary noise (`useless_conversion to the same type: pyo3::PyErr`
  on every pymethod's PyResult return type). The +3 from this commit
  are three more instances of the identical pattern — the new setters
  follow the exact same signature shape every existing pymethod uses.
  Real fix is at the PyO3 version layer, out of scope.
- rustfmt src/db.rs: clean
@Xof
Xof merged commit 212cee2 into main May 14, 2026
8 checks passed
@Xof
Xof deleted the claude/python-spillway-exposure branch May 14, 2026 17:47
@github-actions

Copy link
Copy Markdown

🚦 Bench results: PR vs main

⚠️ 6 regression(s) detected across 4 scenario/mode pair(s)

Scenario Mode Δ throughput Worst Δ
ycsb-a redb-strict +0.0% p99 +77.9% ⚠️
ycsb-b sqlite-strict -11.1% p99 +67.6% ⚠️
ycsb-a sqlite-strict -3.0% p99 +33.3% ⚠️
document-store chisel-strict +0.6% p50 +5.0% ⚠️
document-store redb-strict +0.2%
document-store sqlite-strict +0.1%
mutation-log chisel-strict +11.8%
mutation-log redb-strict -0.0%
mutation-log sqlite-strict -2.5%
ycsb-a chisel-strict +11.1%
ycsb-b chisel-strict +10.3%
ycsb-b redb-strict -0.7%
Per-scenario detail (4 metrics × cells)

document-store

Mode Throughput p50 p95 p99
chisel-strict 3211 ops/s → 3229 ops/s (+0.6%) 21.6 µs → 22.6 µs (+5.0%) ⚠️ 956.5 µs → 959.3 µs (+0.3%) 2.18 ms → 2.17 ms (-0.2%)
redb-strict 4125 ops/s → 4132 ops/s (+0.2%) 12.3 µs → 12.7 µs (+3.1%) 534.6 µs → 542.0 µs (+1.4%) 2.19 ms → 2.14 ms (-2.3%)
sqlite-strict 5119 ops/s → 5125 ops/s (+0.1%) 20.0 µs → 20.5 µs (+2.7%) 440.9 µs → 447.3 µs (+1.5%) 1.36 ms → 1.45 ms (+6.7%)

mutation-log

Mode Throughput p50 p95 p99
chisel-strict 1383 ops/s → 1546 ops/s (+11.8%) 831.0 µs → 727.6 µs (-12.4%) 1.18 ms → 1.10 ms (-6.7%) 2.39 ms → 2.07 ms (-13.3%)
redb-strict 1808 ops/s → 1807 ops/s (-0.0%) 197.0 µs → 193.9 µs (-1.6%) 343.7 µs → 317.4 µs (-7.7%) 28.54 ms → 29.46 ms (+3.2%)
sqlite-strict 4624 ops/s → 4510 ops/s (-2.5%) 127.3 µs → 131.6 µs (+3.4%) 343.8 µs → 357.2 µs (+3.9%) 535.1 µs → 588.1 µs (+9.9%)

ycsb-a

Mode Throughput p50 p95 p99
chisel-strict 1884 ops/s → 2093 ops/s (+11.1%) 534.2 µs → 494.4 µs (-7.4%) 1.24 ms → 1.12 ms (-9.4%) 1.85 ms → 1.69 ms (-8.7%)
redb-strict 2681 ops/s → 2682 ops/s (+0.0%) 155.8 µs → 154.6 µs (-0.8%) 282.5 µs → 259.6 µs (-8.1%) 519.7 µs → 924.4 µs (+77.9%) ⚠️
sqlite-strict 145653 ops/s → 141282 ops/s (-3.0%) 7.0 µs → 7.1 µs (+0.1%) 9.0 µs → 9.4 µs (+4.8%) 11.6 µs → 15.4 µs (+33.3%) ⚠️

ycsb-b

Mode Throughput p50 p95 p99
chisel-strict 18454 ops/s → 20363 ops/s (+10.3%) 6.9 µs → 7.1 µs (+3.9%) 530.3 µs → 495.8 µs (-6.5%) 1.09 ms → 997.3 µs (-8.8%)
redb-strict 27186 ops/s → 26986 ops/s (-0.7%) 2.9 µs → 2.8 µs (-1.4%) 158.8 µs → 155.5 µs (-2.1%) 241.2 µs → 238.1 µs (-1.2%)
sqlite-strict 179789 ops/s → 159811 ops/s (-11.1%) ⚠️ 5.6 µs → 5.8 µs (+3.7%) 7.6 µs → 10.3 µs (+36.1%) ⚠️ 10.0 µs → 16.8 µs (+67.6%) ⚠️
Generated by chisel-bench-diff at 2026-05-14T18:02:27Z. Compares PR HEAD against main. Never blocks merge — signal, not gate. Thresholds: throughput 5%, p50 5%, p95 10%, p99 10%.

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