Skip to content

feat(wal): checkpoint sidecar, retain-floor housekeeping, offset-carrying sink (RFC0008.7) - #186

Merged
jensholdgaard merged 3 commits into
mainfrom
feat/rfc0008-7-checkpoint
Jun 12, 2026
Merged

feat(wal): checkpoint sidecar, retain-floor housekeeping, offset-carrying sink (RFC0008.7)#186
jensholdgaard merged 3 commits into
mainfrom
feat/rfc0008-7-checkpoint

Conversation

@jensholdgaard

@jensholdgaard jensholdgaard commented Jun 12, 2026

Copy link
Copy Markdown
Owner

What

PR 2 of the snapshot-restore workstream (spec: #185). Implements RFC 0008 §6.7 in ourios-wal and flips all four RFC0008.7 red-gate arms:

  • Wal::checkpoint(durable_to) — persists the 32 B OWCK v1 sidecar atomically (CHECKPOINT.tmpfsyncrename → parent-dir fsync). Monotonic advance; idempotent on re-assert; the in-memory offset is not advanced on error (conservatively keeps all segments).
  • Invalid sidecar aborts at open (§6.6 step 1): wrong magic / unknown version / non-zero flags / size ≠ 32 B → OpenError::Corrupt, before any recovery. Silently treating it as None would drop the Parquet suppression horizon and duplicate every published record.
  • Wal::last_checkpoint() — the recovery driver's Parquet-side suppression horizon.
  • Wal::housekeeping(retain_floor) — unlinks whole segments wholly below min(checkpoint, floor); never the current append segment; segment identity from the in-file header (a renamed file is judged by its true identity). The floor is the lagging-snapshot guard from the docs(rfc-0008,rfc-0001): specify snapshot restore v2 — offset sink, retain floor, recovery driver #185 amendment.
  • FrameSink::consume carries the frame's append-offset, and replay delivers every well-formed surviving frame — suppression moved out of replay into the driver, per consumer. An in-replay skip would make floor-retained (S, X] frames undeliverable (the inconsistency Copilot caught on docs(rfc-0008,rfc-0001): specify snapshot restore v2 — offset sink, retain floor, recovery driver #185).
  • metrics() implemented: exact counters (appends_total, syncs_total, unflushed_bytes, corrupt_frames_total), best-effort disk_bytes/segment_count directory walk (dashboard read, documented), checkpoint fields.
  • wal_crash_fixture gains a CHECKPOINT op for the SIGKILL arm.

Tests

All four RFC0008.7 arms are live (tests/rfc0008_7_checkpoint.rs), plus sidecar codec unit tests (round-trip, pinned 32 B layout, per-field rejection, absent-vs-invalid read). Multi-segment roots are minted via the public API in scratch roots and moved in (UUIDv7 monotonicity ⇒ chronological order) — rotation (RFC0008.6) is still red and untouched.

Arm 2 is a real SIGKILL: the fixture checkpoints, echoes the offset, signals READY, and is killed before housekeeping; the restart asserts last_checkpoint() == Some(X) and that partitioning the delivered frames on X yields exactly the already-published prefix.

Invariants / hazards (§3.4 / H3, hazard #5)

  • §3.4 WAL-before-ack: untouched — append/sync semantics unchanged; the new counters are passive. Replay still delivers every surviving frame, so no acknowledged data is dropped; the checkpoint affects only what the driver's Parquet path suppresses (already-durable-on-object-storage records) and what housekeeping may reclaim.
  • H3 (durability vs. latency): the sidecar write is on the Parquet-publish callback path, not the ack path — no new fsync between append and ack. The crash-recovery CI gate (RFC0008.2) still passes, now exercising the offset-carrying sink.
  • Hazard docs(rfc-0001): fill in drafted-bar content for the template miner #5 (template drift): the retain floor is the mechanism — truncation can never destroy a frame no miner snapshot has captured (arm 4 pins it).
  • Sink signature change is a compile-time-visible breaking change inside the workspace only (the trait has no external implementors; all five impls updated in this PR). Wal::replay docs updated to the delivers-everything contract.

Checks run

cargo fmt --all --check, cargo clippy --all-targets --all-features -- -D warnings, cargo test --all-features (80 test binaries, zero failures), cargo doc, cargo bench --no-run — all green locally.

Part of the #185 plan; PR 3 (miner restore v2 + serve() recovery driver, RFC0008.10 / §3.5.3 / §3.5.4) follows.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added persistent WAL checkpoint functionality with atomic durability guarantees
    • Introduced runtime metrics tracking (append/sync counts, unflushed bytes, corruption counters)
    • Implemented automatic segment housekeeping to remove data below checkpoint boundaries
    • Enhanced crash recovery with checkpoint sidecar integration
  • Tests

    • Updated WAL integration tests and fixtures to support checkpoint operations and recovery verification

jensholdgaard and others added 2 commits June 12, 2026 14:45
…ying sink (RFC 0008 §6.7)

Implements the §6.7 contract as amended 2026-06-12:

- Wal::checkpoint persists the 32 B OWCK v1 sidecar atomically
  (CHECKPOINT.tmp -> fsync -> rename -> parent-dir fsync); advance is
  monotonic, re-asserting the current value is an idempotent no-op,
  and the in-memory offset is not advanced on error.
- Wal::open reads the sidecar; a present-but-invalid one (wrong
  magic / version / flags / size) is OpenError::Corrupt and aborts
  before any recovery — silently treating it as None would drop the
  Parquet suppression horizon and duplicate published records.
- Wal::last_checkpoint exposes the offset as the recovery driver's
  Parquet-side suppression horizon.
- Wal::housekeeping(retain_floor) unlinks whole segments wholly
  below min(checkpoint, floor) — never the current append segment;
  segment identity comes from the in-file header, not the filename.
  The floor is the lagging-miner-snapshot guard (hazard #5).
- FrameSink::consume now receives the frame's append-offset and
  replay delivers every well-formed surviving frame — suppression
  moved out of replay into the driver, per consumer (an in-replay
  skip would make floor-retained frames undeliverable).
- metrics() implemented: exact counters (appends, syncs, unflushed
  bytes, corrupt frames) + best-effort disk_bytes / segment_count
  directory walk + checkpoint fields.
- wal_crash_fixture gains a CHECKPOINT op (checkpoint at the last
  append's offset, echoed to stdout) for the RFC0008.7 SIGKILL arm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Normal-flow truncation (wholly-below unlinked, straddler kept,
wal_disk_bytes drops by the unlinked bytes); SIGKILL between
checkpoint(X) and housekeeping (sidecar survives, replay delivers
everything, partitioning on X yields exactly the published prefix);
surviving-segments offsets (fresh open + checkpoint(Y > X) with no
global counter); retain floor (S < X holds the (S, X] segment back
until the floor advances). Multi-segment roots are minted in scratch
roots and moved in — rotation (RFC0008.6) is still red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jensholdgaard, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 50 minutes and 51 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c16765f6-cb73-4f1b-a4b1-902e34573ab9

📥 Commits

Reviewing files that changed from the base of the PR and between 9ce28c4 and 3b75f3f.

📒 Files selected for processing (2)
  • crates/ourios-wal/src/lib.rs
  • crates/ourios-wal/tests/rfc0008_7_checkpoint.rs
📝 Walkthrough

Walkthrough

This PR implements WAL checkpoint persistence and offset-driven housekeeping (RFC 0008): adds a 32-byte sidecar codec to durably store checkpoint offsets, updates the FrameSink::consume trait to include frame offsets during replay, implements Wal::checkpoint/last_checkpoint/housekeeping to truncate stale segments, tracks runtime counters (appends_total, syncs_total, corrupt_frames_total), and provides comprehensive crash-recovery and housekeeping tests with a fixture harness.

Changes

Checkpoint sidecar, offset tracking, and segment truncation

Layer / File(s) Summary
Checkpoint sidecar codec and persistence
crates/ourios-wal/src/checkpoint.rs
New module implements a fixed 32-byte sidecar format with magic/version/flags/UUID/offset. encode() serializes WalOffset, decode() validates strictly, read() loads from disk returning Ok(None) when absent and OpenError::Corrupt for malformed data, and write() atomically persists via temp file + fsync + rename + parent-dir fsync. Unit tests verify codec invariants and error cases.
WAL struct with checkpoint and counter state initialization
crates/ourios-wal/src/lib.rs (lines 4–10, 26, 191–203, 231–236, 254–258)
Wal struct gains checkpoint: Option<WalOffset> and runtime counters (appends_total, syncs_total, unflushed_bytes, corrupt_frames_total). Wal::open reads the CHECKPOINT sidecar up front and initializes counters to zero, propagating any sidecar corruption as an OpenError. Crate docs updated to reflect checkpoint/metrics status.
Checkpoint API and segment housekeeping
crates/ourios-wal/src/lib.rs (lines 409–520, 1103–1131)
Wal::checkpoint(offset) enforces monotonicity/idempotency and writes the sidecar. Wal::last_checkpoint() returns persisted state. Wal::housekeeping(floor) unlinks whole segments below the checkpoint (and optional floor), skips the current append segment, and fsyncs the WAL root only if segments were deleted. New HousekeepingError enum provides error handling.
Append and sync counter updates
crates/ourios-wal/src/lib.rs (lines 343–344, 399–400)
Wal::append increments appends_total and updates unflushed_bytes. Wal::sync increments syncs_total and clears unflushed_bytes.
Frame offset propagation through replay trait and scan
crates/ourios-wal/src/lib.rs (lines 957–974, 876–888, 573–581, 546–555)
FrameSink::consume signature updated to accept offset: WalOffset as the first parameter (the frame's append-offset). replay_segment computes the post-frame WalOffset and passes it to the sink. Wal::replay increments corrupt_frames_total on corruption errors. Replay docs updated to reflect correct frame delivery semantics.
Metrics collection and observability
crates/ourios-wal/src/lib.rs (lines 168–169, 615–649)
Wal::metrics() returns all counters, checkpoint state, and best-effort disk_bytes/segment_count via directory scanning.
Test helpers, fixture, and comprehensive checkpoint tests
crates/ourios-ingester/tests/ingest_support/mod.rs, crates/ourios-server/tests/rfc0003_16_served_binary.rs, crates/ourios-wal/tests/recovery.rs, crates/ourios-wal/tests/rfc0008_2_crash_recovery.rs, crates/ourios-wal/tests/fixtures/wal_crash_fixture.rs, crates/ourios-wal/tests/rfc0008_7_checkpoint.rs
Test adapters update FrameSink::consume implementations across all test files to accept the new _offset: WalOffset parameter. WAL crash fixture extended with deterministic CHECKPOINT operations. RFC0008.7 adds helpers (build_closed_segment, segment_files, OffsetSink, run_fixture_then_sigkill) and four test arms: (1) housekeeping segment unlinking; (2) crash durability and offset-filtered replay; (3) offset reconstruction after truncation; (4) retain-floor semantics with lagging retention.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • jensholdgaard/ourios#80: These changes implement core RFC0008 WAL checkpoint and housekeeping features (persistent checkpoint state, segment truncation, frame offset tracking, and metrics) that are central to the WAL epic.

Possibly related PRs

  • jensholdgaard/ourios#134: This PR introduces the shared ingester tests/ingest_support WAL replay helpers, and the main PR updates that same helper's FrameSink::consume implementation to accept the new WalOffset parameter.
  • jensholdgaard/ourios#123: Both PRs modify crates/ourios-wal/src/lib.rs's WAL replay path—feat(wal): implement sync + replay — crash recovery §6.3/§6.6 #123 implements segment and frame scanning, while the main PR adjusts the same replay delivery to pass WalOffset into FrameSink::consume.
  • jensholdgaard/ourios#185: The main PR implements the WAL checkpoint/offset/housekeeping/retain-floor contract described in the retrieved docs PR, updating FrameSink::consume to include WalOffset and adding checkpoint-driven segment truncation.

Poem

🐰 A checkpoint sidecar, durably sealed,
Frame offsets traced through replay's appeal,
Old segments unlinked when the floor's passed below,
Metrics collected to measure the flow,
Crash tests confirm: the WAL's strong and true!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main changes: checkpoint sidecar implementation, retain-floor housekeeping, and offset-carrying sink updates aligned with RFC0008.7.
Description check ✅ Passed The description provides comprehensive context including objectives, implementation details, test coverage, and invariant considerations. The template's checklist items (fmt, clippy, tests, docs, RFC) are all addressed in the description body.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rfc0008-7-checkpoint

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements RFC 0008 §6.7 “checkpoint sidecar + truncation bound + offset-carrying replay” in ourios-wal, enabling checkpoint persistence, retain-floor-aware housekeeping, and per-consumer replay suppression via delivered WalOffsets.

Changes:

  • Add durable CHECKPOINT sidecar codec + atomic persistence and wire it into Wal::open / Wal::checkpoint / Wal::last_checkpoint.
  • Implement Wal::housekeeping(retain_floor) truncation logic and Wal::metrics() counters + best-effort disk stats.
  • Update FrameSink to receive (WalOffset, FrameKind, payload) and adapt integration tests + crash fixture accordingly.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
crates/ourios-wal/src/lib.rs Implements checkpointing, housekeeping, metrics, and offset-carrying replay; updates FrameSink trait.
crates/ourios-wal/src/checkpoint.rs New sidecar codec + atomic read/write implementation with validation.
crates/ourios-wal/tests/rfc0008_7_checkpoint.rs Flips RFC0008.7 arms live (normal flow, crash window, offset semantics, retain floor).
crates/ourios-wal/tests/fixtures/wal_crash_fixture.rs Adds CHECKPOINT operation for SIGKILL crash arm.
crates/ourios-wal/tests/rfc0008_2_crash_recovery.rs Updates sink signature to accept WalOffset.
crates/ourios-wal/tests/recovery.rs Updates sink signature to accept WalOffset.
crates/ourios-server/tests/rfc0003_16_served_binary.rs Updates sink signature to accept WalOffset.
crates/ourios-ingester/tests/ingest_support/mod.rs Updates sink signature to accept WalOffset.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/ourios-wal/src/lib.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/ourios-wal/src/lib.rs (1)

849-863: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Classify invalid replay-time segment headers as corruption, not I/O.

This branch documents bad magic/version on a *.wal file as corruption, but it wraps those errors in RecoveryError::Io. That changes the public replay contract and also suppresses corrupt_frames_total, because Wal::replay only increments that counter on RecoveryError::Corrupt.

🤖 Prompt for 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.

In `@crates/ourios-wal/src/lib.rs` around lines 849 - 863, The branch that handles
invalid segment headers currently returns RecoveryError::Io by wrapping the
HeaderError in an std::io::Error; change it to return the corruption variant
instead so replay-time header failures are classified as corruption and
corrupt_frames_total still increments. Specifically, replace the Err(other) =>
return Err(RecoveryError::Io { ... source: std::io::Error::new(..., other) })
with returning RecoveryError::Corrupt (use the same op string
"validate_header(segment for replay)" and pass the original HeaderError/typed
`other` as the structured source) so the HeaderError is preserved in the error
chain and the error is reported as corruption rather than I/O.
🤖 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/ourios-wal/src/lib.rs`:
- Around line 490-512: The loop currently skips the active segment only by path
(path == self.current_segment_path), which fails if the active segment was
renamed; instead, after reading each file's header (header.segment_uuid), skip
removal when that UUID equals the active segment's UUID by comparing
header.segment_uuid to the in-memory active segment identity (e.g.
self.current_segment.header.segment_uuid or a stored self.current_segment_uuid).
Update the guard to check UUID equality (using header.segment_uuid) before
removing the file so the live append segment is never unlinked even if renamed.

---

Outside diff comments:
In `@crates/ourios-wal/src/lib.rs`:
- Around line 849-863: The branch that handles invalid segment headers currently
returns RecoveryError::Io by wrapping the HeaderError in an std::io::Error;
change it to return the corruption variant instead so replay-time header
failures are classified as corruption and corrupt_frames_total still increments.
Specifically, replace the Err(other) => return Err(RecoveryError::Io { ...
source: std::io::Error::new(..., other) }) with returning RecoveryError::Corrupt
(use the same op string "validate_header(segment for replay)" and pass the
original HeaderError/typed `other` as the structured source) so the HeaderError
is preserved in the error chain and the error is reported as corruption rather
than I/O.
🪄 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 Plus

Run ID: bb1336a0-59ff-4097-98e7-b9981d4b5170

📥 Commits

Reviewing files that changed from the base of the PR and between f75d001 and 9ce28c4.

📒 Files selected for processing (8)
  • crates/ourios-ingester/tests/ingest_support/mod.rs
  • crates/ourios-server/tests/rfc0003_16_served_binary.rs
  • crates/ourios-wal/src/checkpoint.rs
  • crates/ourios-wal/src/lib.rs
  • crates/ourios-wal/tests/fixtures/wal_crash_fixture.rs
  • crates/ourios-wal/tests/recovery.rs
  • crates/ourios-wal/tests/rfc0008_2_crash_recovery.rs
  • crates/ourios-wal/tests/rfc0008_7_checkpoint.rs

Comment thread crates/ourios-wal/src/lib.rs
Both reviewers caught it: a renamed live append segment slipped the
path-based guard, and unlinking it leaves the writer appending into
an unlinked inode no later open would see. The identity check now
reads the candidate's header first and skips on uuid equality,
consistent with the pass's rename-resilient identity rule. Pinned by
a test that renames the live segment, checkpoints past its every
frame, and asserts housekeeping leaves it alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@jensholdgaard
jensholdgaard merged commit e9322ed into main Jun 12, 2026
12 checks passed
@jensholdgaard
jensholdgaard deleted the feat/rfc0008-7-checkpoint branch June 12, 2026 13:23
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.

2 participants