fix(wal): correct slot-range endianness so pruning and lookups work#1045
Conversation
WAL keys store slot bytes big-endian (`ChainPoint::into_bytes`), so redb's
lexicographic key order matches slot order. But `DbChainPoint::slot_range`
built its scan bounds little-endian, so every slot-range scan over the WAL
returned empty for realistic slot numbers. This silently disabled WAL pruning
(`prune_history` -> `remove_before` always hit `SlotNotFound` and logged
"skipping"), causing unbounded WAL growth, and broke `dump-wal --from` /
`copy-wal --since/--until` ("slot not found").
- Add `min_bound`/`max_bound` helpers on `DbChainPoint` that encode the slot
big-endian; `slot_range` now uses them (the endianness fix).
- Harden `remove_before` to derive the cutoff bound directly from the target
slot via `min_bound(slot)` instead of locating a real nearby entry with
`approximate_slot(±180)`, which silently no-op'd in sparse chain regions.
This makes the newly-active pruning path deterministic; drop the now-dead
`SlotNotFound` catch in `prune_history`.
- Add regression tests: byte-boundary round-trip (2^8/2^16/2^32 + preprod
scale), full prune to exactly max_slots, batched prune convergence, sparse
WAL remove_before, and cutoff-slot retention.
The archive store is unaffected (native u64 slot keys, ordered numerically).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 29 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR changes WAL slot-bound encoding to big-endian bytes, makes pruning use deterministic cutoff-bound deletion, propagates prune errors directly, and adds regression tests for ordering and pruning behavior. ChangesWAL key ordering and pruning
Estimated code review effort: 4 (Complex) | ~40 minutes Sequence Diagram(s)sequenceDiagram
participant PruneHistory
participant RemoveBefore
participant DbChainPoint
participant WalStore
PruneHistory->>RemoveBefore: remove_before(prune_before)
RemoveBefore->>DbChainPoint: min_bound(target_slot)
DbChainPoint-->>RemoveBefore: exclusive cutoff bound
RemoveBefore->>WalStore: extract entries < bound
WalStore-->>RemoveBefore: removed entries
RemoveBefore-->>PruneHistory: result
Related PRs: None identified from the provided information. Suggested labels: wal, redb3, bugfix Suggested reviewers: None identified from the provided information. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/redb3/src/wal/mod.rs`:
- Around line 543-545: The rustdoc for prune_history is stale and still
describes warning/Ok behavior for a missing target slot, but the implementation
now always prunes by bound via remove_before and only propagates its errors.
Update the doc comment above prune_history to match the current behavior, using
the prune_history and remove_before symbols so it’s clear the old missing-slot
path no longer applies.
- Around line 688-690: In remove_before, the extract_from_if iterator error is
being swallowed by matching only Some(Ok(...)), which can allow wx.commit() to
persist a partial prune after an Err. Update the remove_before flow around
wal.extract_from_if and the to_remove iteration to explicitly process each
drained item and propagate any iterator error with ?, so the cleanup only
commits when all entries are handled successfully.
- Around line 88-104: The slot_range helper is adjusting BlockSlot bounds with
+1/-1, which can overflow for Excluded(BlockSlot::MAX) or underflow for
Excluded(BlockSlot::MIN). Update slot_range to construct the DbChainPoint range
directly from the incoming RangeBounds<BlockSlot> without shifting the slot
values, and keep the logic centered in slot_range with
DbChainPoint::min_bound/max_bound handling the bound conversion.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b5942144-baca-49ff-953a-bddce79a8882
📒 Files selected for processing (1)
crates/redb3/src/wal/mod.rs
Follow-up to the CodeRabbit review on the slot-range endianness fix:
- slot_range: map each RangeBounds<BlockSlot> bound to the enclosing key
bound directly (via DbChainPointRange + min_bound/max_bound) instead of
shifting slots with +1/-1, which overflowed at BlockSlot::MAX and
underflowed at BlockSlot::MIN for exclusive bounds. Add a test covering
the extreme/exclusive bounds. (Reuses the previously-unused
DbChainPointRange type.)
- remove_before: drain extract_from_if with `for entry in .. { entry?; }`
instead of `while let Some(Ok(..))`, so a storage error is propagated and
the write transaction aborts before commit rather than persisting a
partial prune. Matches the existing remove_entries pattern.
- prune_history: refresh the stale "Returns" rustdoc that still described
the removed SlotNotFound warn/skip path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Condense the private-item docs, inline comments, and test doc comments added for the slot-range fix. No behavior change; public API rustdoc unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…discontinuities The `dolos doctor wal-integrity` tool crashed on any instance bootstrapped from Mithril (or otherwise reseeded via `reset_to`): the first WAL entry is a synthetic `LogValue::origin()` marker whose empty `block` bytes can't be CBOR-decoded, so the tool died with "Unknown CBOR structure" before verifying anything. Changes to `src/bin/dolos/doctor/wal_integrity.rs`: - Extract testable `scan_chain_integrity()` that skips synthetic entries (`block.is_empty() || !point.is_fully_defined()`) and resets the prev-hash tracker so the chain check restarts cleanly from the next real block. - Replace `assert_eq!(previous, last)` (panic, no context, stops at first mismatch) with structured `IntegrityIssue` collection that reports slot, block hash, expected prev, and got prev. Scanning continues past mismatches and the tool exits non-zero at the end if any issues are found. - Add optional `--from`/`--to` slot args (unblocked by the recent endianness fix in #1045) that resolve via `locate_point` for fast tail checks without scanning the entire WAL. Regression tests covering: `reset_to` + chained blocks pass, Origin reset + blocks pass, manufactured discontinuity reports with context and continues scanning, mid-WAL rollback re-seed resets the chain.
Problem
WAL keys store slot bytes big-endian (
ChainPoint::into_bytes,crates/core/src/point.rs:137), so redb's lexicographic key comparison makes byte order equal to slot order. ButDbChainPoint::slot_rangebuilt its scan bounds little-endian, so for any realistic slot (e.g. preprod ~127,000,000 =0x0799…) the computed range excluded every real key. Every slot-range scan over the WAL returned empty.Consequences:
prune_history → remove_before → approximate_slot_with_retryalways returnedNone → SlotNotFound, whichprune_historycaught and logged"pruning target slot not found, skipping", returningOk. Housekeeping believed it had pruned. On long-running instances the WAL grew without bound (observed ~15× over the configuredmax_rollback).dolos data dump-wal --from <slot>anddolos data copy-wal --since/--untilreported "slot not found".Sync/rollback correctness was not affected (those paths build keys big-endian via
DbChainPoint::from). The archive store is also unaffected — it keys on nativeBlockSlot(u64), which redb orders numerically.Changes (
crates/redb3/src/wal/mod.rs)min_bound/max_boundhelpers onDbChainPointthat encode the slot withto_be_bytes()(min padded with the zero hash, max with0xFF);slot_rangenow uses them. These were the only twoto_le_bytes()calls in the crate.remove_before. The endianness fix turnsremove_beforeinto a live deletion path for the first time. It previously located the cutoff via anapproximate_slot(±180)search that silently no-op'd in sparse chain regions. It now derives the cutoff bound directly from the target slot viamin_bound(slot)andextract_from_if(..bound)— deterministically removing everything withslot < targetwhile retaining real entries at the cutoff slot (nonzero hash). The now-deadSlotNotFoundcatch inprune_historyis removed.locate_point/approximate_slotare unchanged (correct for the dump/copy tooling).TestStore/TestDeltascaffolding): byte-boundary round-trip (2⁸/2¹⁶/2³² + preprod-scale slot), full prune shrinking to exactlymax_slots, batched prune convergence without pruning into the protected window, sparse-WALremove_before, and cutoff-slot retention.Verification
cargo test -p dolos-redb3 wal::→ 10/10 pass (the round-trip test fails without the byte-order fix).cargo test -p dolos-core point::→ BE key-order proptest holds.cargo build(full workspace) andcargo clippy -p dolos-redb3→ clean.Rollout note
This activates WAL pruning for the first time. On long-running instances the first housekeeping prunes a large backlog in 10,000-slot batches (
MAX_PRUNE_SLOTS_PER_HOUSEKEEPING); thedata housekeepingCLI loops until done and the periodic sync-loop path catches up over intervals. Consider runningdolos data housekeepingonce explicitly on affected instances after upgrade.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests