Skip to content

fix(shard): gate autovacuum Pass C WAL recycle on checkpoint-backed mode (task #43, P1)#288

Merged
pilotspacex-byte merged 3 commits into
mainfrom
fix/passc-wal-recycle-floor
Jul 11, 2026
Merged

fix(shard): gate autovacuum Pass C WAL recycle on checkpoint-backed mode (task #43, P1)#288
pilotspacex-byte merged 3 commits into
mainfrom
fix/passc-wal-recycle-floor

Conversation

@pilotspacex-byte

@pilotspacex-byte pilotspacex-byte commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

P1 data loss (task #43, storage audit 2026-07-12): autovacuum Pass C recycled WAL v3 segments that were the SOLE durable copy of graph/temporal/workspace/MQ history in legacy (non-disk-offload) mode.

Pass C ran in every appendonly-enabled configuration and called recycle_aggressive(redo_lsn = wal.current_lsn()) — the current WAL head, not a checkpoint-verified floor — deleting every sealed segment once total WAL exceeded --max-wal-size (256MB default). In disk-offload mode this is safe (the checkpoint protocol maintains a real redo_lsn and snapshots graph write-buffers). In legacy mode there is no periodic plane snapshot (save_graph_store runs only at graceful shutdown), so GRAPH.*/TEMPORAL/WS/MQ WAL records were the only durable form of that history — Pass C deleting them meant permanent loss on the next kill-9. KV was never at risk (AOF-authority).

Design: skip-and-warn in legacy mode (option B)

Snapshot-then-recycle (option A) was investigated and rejected: workspace/MQ/temporal registries have no snapshot format at all — their replay functions rebuild purely by WAL rescan — so a graph-only snapshot floor would still lose WS/MQ/temporal records, while per-segment record-type scanning adds real complexity. Instead:

  • AutovacuumDaemon::run_tick now takes disk_offload_enabled. True → byte-for-byte unchanged (checkpoint-backed recycle_aggressive). False → Pass C skips the recycle, logs a rate-limited (5 min) warning ("WAL exceeds --max-wal-size but cannot be recycled without a checkpoint; run BGSAVE or enable disk-offload"), and increments the new reclamation_wal_recycle_blocked_no_checkpoint_total INFO counter.
  • Trade-off accepted and documented: legacy mode loses the bounded-WAL-growth guarantee in exchange for zero data loss; operators get a loud, countable signal.

Verification (red/green)

  • RED: with the fix stashed, the new kill-9 test failed at "the WorkspaceCreate record for 'legacy-ws-t43' was deleted from every on-disk WAL segment — Pass C recycled it in legacy mode with no checkpoint/snapshot floor". GREEN after (macOS + Linux VM, MOON_BIN pinned to the ELF build).
  • Disk-offload behavior proven unchanged: new unit test drives a real WalWriterV3 over the ceiling with disk_offload_enabled=true and asserts segments are recycled; the untouched checkpoint-path suite (crash_recovery_graph_durability g4/g5) passes unmodified.
  • Gates: lib 4145 (monoio) + 3341 (tokio); shardslice_live 6/6; clippy -D warnings both matrices; fmt.
  • g1/g2/g3 of crash_recovery_graph_durability fail pre-existing on an unrelated harness bug (polls the stale v2 shard-0.wal path; zero diff on this branch) — root cause recorded in task GPU: device memory pool for zero-allocation compaction cycles #31.

Follow-ups

Summary by CodeRabbit

  • Bug Fixes

    • Prevented a legacy-mode WAL recycling issue from deleting the only durable WAL history copy.
    • In legacy mode, WAL recycling is now skipped when checkpoint/graph snapshot support isn’t available, with a rate-limited warning and WAL segment preservation.
    • Disk-offload mode keeps its existing recycling behavior.
  • Monitoring

    • Added an INFO metric/counter for times WAL recycling is blocked due to missing checkpoint support.
  • Tests

    • Added an integration crash-recovery test validating survival across repeated recycle passes and restart in legacy mode.

…de WAL history (task #43)

Adds tests/crash_recovery_wal_recycle_legacy.rs, a live kill-9 + restart
integration test proving that autovacuum's Pass C (`src/shard/autovacuum.rs`)
deletes WAL v3 segments that are the SOLE durable copy of graph/workspace
history when disk-offload is disabled (legacy mode).

Pass C calls `WalWriterV3::recycle_aggressive(redo_lsn = wal.current_lsn())`
whenever total WAL bytes exceed `--max-wal-size`, with no gate on whether a
checkpoint protocol backs that floor. In legacy mode there is no periodic
plane snapshot: `save_graph_store` only runs at graceful shutdown, and the
workspace/MQ/temporal registries have no snapshot format at all — they
replay purely from the WAL. Once the ceiling is breached, every sealed
segment is deleted regardless of whether its records survive anywhere
else.

The test forces the condition deterministically: tiny --wal-segment-size /
--max-wal-size plus a fast --autovacuum-interval-secs make Pass C run
within seconds. It asserts two things: (1) byte-level survival — the
earliest WorkspaceCreate and graph-node WAL records are still present on
disk after Pass C has had several chances to run, and (2) round-trip
survival — after a real kill -9 + restart, WS LIST still returns the
workspace. Against current main, both fail: assertion (1) panics with
"the WorkspaceCreate record ... was deleted from every on-disk WAL
segment."

Confirmed RED via `git stash` of the (separately staged) fix and rerunning
this test: it fails with exactly that assertion. Restoring the fix turns
it green. Disk-offload mode is a pre-existing pass (`g4`/`g5` in
crash_recovery_graph_durability.rs, unmodified) — this test targets only
the legacy-mode gap.

Full graph reconstruction via GRAPH.QUERY after restart is deliberately
not asserted in legacy mode: independent probing found graph command
replay from WAL v3 does not reconstruct the graph in that mode even with
zero Pass C interference (default --max-wal-size), a separate, pre-existing
gap unrelated to WAL recycling and out of scope here.

author: Tin Dang
…ode (task #43, P1)

`AutovacuumDaemon::run_tick`'s Pass C called `WalWriterV3::recycle_aggressive`
against `wal.current_lsn()` — the LSN about to be assigned, i.e. "every
sealed segment is durable elsewhere" — whenever total WAL bytes exceeded
`--max-wal-size`, regardless of whether disk-offload (the only mode with a
real checkpoint protocol) was enabled. That assumption only holds when a
checkpoint maintains a genuine redo_lsn and `persist_graph_at_checkpoint`
snapshots the graph write-buffer before the floor advances. In legacy
(non-disk-offload) mode there is no periodic plane snapshot at all:
`save_graph_store` runs only at graceful shutdown, and the workspace/MQ/
temporal registries have no snapshot format whatsoever — they replay
purely from the WAL. Pass C therefore deleted the sole durable copy of
that history once the ceiling was breached; a kill -9 afterwards lost it
permanently.

Design choice (documented in autovacuum.rs and CHANGELOG): rather than
inventing a snapshot format for workspace/MQ/temporal (explicitly out of
scope, and the guidance's own fallback for exactly this case), Pass C now
takes a `disk_offload_enabled` flag:
- true (disk-offload / checkpoint-backed): unchanged — same
  `recycle_aggressive(current_lsn())` call as before this fix.
- false (legacy mode): skip the recycle entirely. Emit a rate-limited
  (5 min) tracing::warn! and increment the new
  `RECL_WAL_RECYCLE_BLOCKED_NO_CHECKPOINT_TOTAL` counter (exposed as
  `reclamation_wal_recycle_blocked_no_checkpoint_total` in
  `INFO # Reclamation`) so operators can observe unbounded WAL growth in
  this mode instead of silently losing data.

A partial snapshot-then-floor design (freeze+persist the graph plane via
the already-callable `persist_graph_at_checkpoint`, then recycle only
segments proven free of un-snapshotted workspace/MQ/temporal records) was
considered and rejected: it would still leave those three planes
unprotected without inventing a snapshot format for them, adding
complexity without closing the gap. "Never trade data loss for disk
space" wins; operators needing bounded legacy-mode WAL growth should
enable `--disk-offload enable`.

Both call sites in `event_loop.rs` (tokio select! and monoio tick loop)
now pass `server_config.disk_offload_enabled()`.

Verification:
- tests/crash_recovery_wal_recycle_legacy.rs (previous commit) goes green.
- Two new fast unit tests in autovacuum.rs
  (test_pass_c_legacy_mode_skips_recycle_and_counts_blocked,
  test_pass_c_disk_offload_mode_recycles_unchanged) exercise Pass C
  directly against a real WalWriterV3 without spawning a server, proving
  disk-offload mode's segment count still drops (byte-for-byte unchanged
  behavior) while legacy mode's does not.
- crash_recovery_graph_durability.rs g4/g5 (disk-offload / checkpoint path)
  pass unmodified, confirming no behavior change outside legacy mode.
- Full `cargo test --release --lib` (4145 passed), shardslice_live (6
  passed), `cargo clippy` default and `--no-default-features --features
  runtime-tokio,jemalloc` (zero warnings), `cargo fmt --check` clean.

CHANGELOG updated under [Unreleased].

author: Tin Dang
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 492c1718-34a8-4f93-a124-0df553d65085

📥 Commits

Reviewing files that changed from the base of the PR and between 6b0dc5d and d7e99f4.

📒 Files selected for processing (1)
  • tests/crash_recovery_wal_recycle_legacy.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/crash_recovery_wal_recycle_legacy.rs

📝 Walkthrough

Walkthrough

Legacy-mode autovacuum no longer recycles WAL without checkpoint support. Disk-offload mode retains existing recycling, while legacy mode emits a rate-limited warning and increments an INFO counter. Unit and ignored crash-recovery tests cover both modes and kill-9 restart behavior.

Changes

WAL recycle protection

Layer / File(s) Summary
WAL recycle behavior and observability
src/shard/autovacuum.rs, src/command/info_reclamation.rs, CHANGELOG.md
Pass C gates recycling on disk-offload mode, blocks legacy recycling with a warning and counter, and documents the behavior.
Autovacuum mode propagation
src/shard/event_loop.rs
Both event-loop tick paths pass the disk-offload setting to run_tick.
Pass C mode tests
src/shard/autovacuum.rs
Unit tests verify legacy WAL preservation and continued recycling in disk-offload mode.
Crash-recovery test harness
tests/crash_recovery_wal_recycle_legacy.rs
Adds binary discovery, server lifecycle, restart synchronization, WAL scanning, and RESP helpers.
Legacy kill-9 regression coverage
tests/crash_recovery_wal_recycle_legacy.rs
Forces repeated recycle passes, checks WAL markers, performs a hard crash and restart, and verifies workspace recovery.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ShardEventLoop
  participant AutovacuumDaemon
  participant WAL
  participant ReclamationMetric
  ShardEventLoop->>AutovacuumDaemon: run_tick(..., disk_offload_enabled)
  AutovacuumDaemon->>WAL: stats()
  alt disk offload enabled
    AutovacuumDaemon->>WAL: recycle_aggressive(current_lsn)
  else legacy mode
    AutovacuumDaemon->>ReclamationMetric: increment blocked counter
    AutovacuumDaemon-->>ShardEventLoop: skip recycling and emit rate-limited warning
  end
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it omits the required Checklist, Performance Impact, and Notes sections from the template. Add the missing template sections with the required checkboxes, performance impact statement, and reviewer notes/trade-offs.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: gating autovacuum Pass C WAL recycling by checkpoint-backed mode.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/passc-wal-recycle-floor

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.

@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

🤖 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 `@tests/crash_recovery_wal_recycle_legacy.rs`:
- Around line 139-146: Add an accurate `// SAFETY:` comment immediately before
the `unsafe` block in `sigkill`, documenting why calling `libc::kill` with the
child process ID is safe.
🪄 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: 5b97e9e3-39c2-4c42-af09-37d16aca2fa1

📥 Commits

Reviewing files that changed from the base of the PR and between f89307a and 6b0dc5d.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • src/command/info_reclamation.rs
  • src/shard/autovacuum.rs
  • src/shard/event_loop.rs
  • tests/crash_recovery_wal_recycle_legacy.rs

Comment thread tests/crash_recovery_wal_recycle_legacy.rs
CodeRabbit review finding on PR #288: the libc::kill FFI call in the
crash_recovery_wal_recycle_legacy harness lacked the repo-mandated
// SAFETY: justification. Documents the invariant (owned live Child,
targeted pid, no memory-safety preconditions).

author: Tin Dang
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