Skip to content

fix(persistence): latch a degraded state when the data dir vanishes — no per-tick ENOENT error loop (#366)#369

Merged
pilotspacex-byte merged 2 commits into
mainfrom
fix/dir-deleted-degraded-latch
Jul 17, 2026
Merged

fix(persistence): latch a degraded state when the data dir vanishes — no per-tick ENOENT error loop (#366)#369
pilotspacex-byte merged 2 commits into
mainfrom
fix/dir-deleted-degraded-latch

Conversation

@pilotspacex-byte

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

Copy link
Copy Markdown
Contributor

Problem (#366)

Deleting --dir under a running server left the per-shard persistence tick retrying its file operations on ENOENT forever — observed in the wild as an idle 4-shard server at ~667% CPU (sys:user ~50:1) with a wedged PING. Even in the mildest reproduction, every shard warn-logged the WAL-overflow scan once per second, forever.

Fix — design-for-failure, mirroring the diskfull guard

  • Dir-lost latch in DiskMonitor: the existing 5s statvfs poll now distinguishes ENOENT/ENOTDIR on the monitored path (deterministic "the dir is gone") from transient errors (unchanged: retain previous state). Latch = ONE loud ERROR log.
  • Writes refused with a distinct MOONERR dirmissing message (both dispatch runtimes); reads and PING keep serving.
  • Persistence tick quiesces: WAL flush, checkpoint tick, WAL-overflow scan, and auto-save all skip via is_dir_lost() (one relaxed atomic load) — zero per-tick syscalls, zero per-tick log lines while latched. Manual BGSAVE still fails one attempt loudly through the existing finalize-error path (BGSAVE fan-in counter untouched).
  • Self-heal: next poll that sees the path clears the latch and resumes writes, with an explicit warning that data accepted since the deletion is not guaranteed durable until restart (WAL fds point at unlinked inodes).
  • Only arms when persistence is configured; works with --disk-free-min-pct 0 (poll_global now runs whenever either guard is active).

Measured (incident config: 4 shards, appendonly yes)

pre-fix post-fix
SET after rm -rf $dir +OK forever (silent data-loss risk) MOONERR dirmissing within one poll
stderr +4 warn lines/s, forever frozen after one ERROR line
CPU while latched error-looping ticks (667% in the wild) flat at idle baseline
dir restored n/a writes resume ≤ one poll

Tests

  • Red/green: tests/dir_deleted_degraded.rs pins refusal ≤ one poll, responsiveness while latched, quiesced log+CPU, and self-heal — failed at the refusal assertion pre-fix.
  • Unit: latch transitions + disarmed-guard default in disk_monitor.
  • Gates: fmt, clippy (default + tokio,jemalloc), 4369 lib tests, integration test green under monoio (macOS), tokio (macOS), and Linux VM.

Notes

  • The exact 667% steady state could not be re-created at HEAD (the incident binary predates this morning's rebuild); this PR eliminates the entire per-tick-retry class the incident belonged to.
  • Known pre-existing edge, not worsened here: after a heal, the feat(persistence): data-dir instance lock — refuse double-start on the same --dir #363 moon.lock flock is held on the unlinked inode, so double-start protection on the recreated dir is weakened until restart.

Closes #366

Summary by CodeRabbit

  • Bug Fixes
    • Prevented repeated persistence/WAL retry loops when the configured data directory is deleted during runtime.
    • Writes now fail fast with MOONERR dirmissing; reads and PING stay responsive.
    • Server enters a degraded persistence state (skipping autosave/WAL flush/checkpoint work) until the directory returns, then safely resumes with a durability warning.
    • Improved resilience and hardening: WAL rotation recreates missing parent WAL directories and flush failures use a brief backoff to avoid tight retry churn.
  • Tests
    • Added a Unix-only integration test covering directory deletion, latching behavior, and recovery.

… no per-tick ENOENT error loop (#366)

Deleting --dir under a running server (operator rm -rf, tmp-cleaner
sweep, mount loss) used to leave the per-shard persistence tick
retrying its file operations on ENOENT forever: WAL flush at the 1ms
cadence, the WAL-overflow directory scan warning once per second per
shard, checkpoints failing every tick. Observed in the wild as an idle
4-shard server at ~667% CPU (sys:user ~50:1) with a wedged PING.

Design-for-failure, mirroring the MOONERR diskfull guard:

- DiskMonitor's existing 5s statvfs poll now distinguishes
  ENOENT/ENOTDIR on the monitored path (deterministic "the dir is
  gone") from transient errors (which keep retaining previous state,
  as before), and latches a new `dir_lost` flag with ONE loud ERROR
  log. The latch feeds the existing write-stall gate: writes are
  refused with a distinct `MOONERR dirmissing` message at both
  dispatch sites; reads and PING keep serving.
- The persistence tick consults `is_dir_lost()` (one relaxed atomic
  load) and skips WAL flush, checkpoint tick, WAL-overflow scan, and
  auto-save entirely while latched — zero per-tick syscalls, zero
  per-tick log lines. Manual BGSAVE still fails one attempt loudly
  through the existing finalize-error path (fan-in counter intact).
- Self-heal: the next poll that sees the path again clears the latch
  and resumes writes, warning explicitly that data accepted since the
  deletion is not guaranteed durable until restart (WAL fds point at
  unlinked inodes).
- The latch only arms when persistence is configured (an in-memory
  server losing its CWD has nothing to protect) and works with
  --disk-free-min-pct 0: poll_global now runs whenever either the
  free-space guard or the dir guard is active.

Measured on the incident configuration (4 shards, appendonly yes):
SET → MOONERR dirmissing within one poll, GET/PING unaffected, CPU
flat at the idle baseline, stderr frozen after the single ERROR line
(pre-fix: +4 warn lines/s forever).

Red/green: tests/dir_deleted_degraded.rs pins the four contract points
(refusal ≤ one poll, responsiveness while latched, quiesced log+CPU,
self-heal) and failed at the refusal assertion pre-fix; two unit tests
pin the latch transitions and the disarmed-guard default. Gates: fmt,
clippy (default + tokio,jemalloc), 4369 lib tests, integration test
green under monoio (macOS), tokio (macOS), and Linux VM.

Closes #366

author: Tin Dang <tindang.ht97@gmail.com>
@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 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The server now latches a degraded state when its persistence directory disappears, refuses writes with MOONERR dirmissing, continues serving reads and PING, skips persistence filesystem work, and resumes after the directory is recreated.

Changes

Directory-loss degraded mode

Layer / File(s) Summary
Directory-loss detection and latch
src/shard/disk_monitor.rs
Missing paths map to PathMissing; guarded monitors latch dir_lost, pause writes, continue polling, and clear the latch when the directory returns.
Monitor wiring and write refusal
src/main.rs, src/server/conn/handler_monoio/dispatch.rs, src/server/conn/handler_sharded/dispatch.rs
Startup arms directory monitoring for persistence or disk offload, and both dispatch paths return MOONERR dirmissing for blocked writes.
Persistence tick quiescing
src/shard/persistence_tick.rs
Autosave, WAL flushing, warm transitions, overflow scanning, and checkpoint advancement skip work while the latch is active.
WAL hardening and validation
src/persistence/wal_v3/segment.rs, tests/dir_deleted_degraded.rs, CHANGELOG.md
WAL rotation recreates missing directories, flush failures use a retry backoff, and tests cover refusal, responsiveness, idle behavior, recovery, and WAL state transitions.

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

Sequence Diagram(s)

sequenceDiagram
  participant Filesystem
  participant DiskMonitor
  participant WriteHandler
  participant PersistenceTick
  Filesystem-->>DiskMonitor: PathMissing
  DiskMonitor->>DiskMonitor: Latch dir_lost
  WriteHandler->>DiskMonitor: Check write state
  DiskMonitor-->>WriteHandler: dir_lost=true
  WriteHandler-->>WriteHandler: Return MOONERR dirmissing
  PersistenceTick->>DiskMonitor: Check dir_lost
  DiskMonitor-->>PersistenceTick: Skip persistence work
  Filesystem-->>DiskMonitor: Directory restored
  DiskMonitor->>DiskMonitor: Clear dir_lost
Loading

Possibly related PRs

  • pilotspace/moon#218: Modifies the same write-stall enforcement paths with additional error-selection branches.

Suggested reviewers: tindang97

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: latching a degraded state to stop the per-tick ENOENT loop.
Description check ✅ Passed It includes the problem, fix, tests, measured impact, and notes, though it doesn't use the repo's exact section headings.
Linked Issues check ✅ Passed The changes meet #366 by latching dir loss, refusing writes, keeping reads/PING alive, quiescing persistence, and self-healing.
Out of Scope Changes check ✅ Passed The WAL repair, backoff, tests, and docs all support the dir-lost persistence fix and look scope-appropriate.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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/dir-deleted-degraded-latch

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: 4

🤖 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 `@src/shard/disk_monitor.rs`:
- Around line 155-166: In the successful `statvfs` branch of the disk-monitor
update logic, do not clear `dir_lost` or resume writes unless
`self.monitored_path` still exists as a directory; validate it with metadata
before the `dir_lost.swap(false, Ordering::Release)` transition. Preserve the
latch and paused state when the path is a regular file or otherwise invalid, and
retain the existing warning only for a genuinely restored directory.
- Around line 393-397: Update the non-Unix `query_free_bytes` stub to check the
supplied path with portable metadata and verify it is a directory; return
`QueryError::PathMissing` when metadata fails or the path is not a directory,
otherwise preserve the existing unlimited free-space result.

In `@src/shard/persistence_tick.rs`:
- Around line 117-129: Update the epoch-handling logic around
last_snapshot_epoch so the data-directory-missing early return applies only to
auto-save triggers. Preserve manual BGSAVE triggers for their existing
snapshot/error coordination path, or explicitly fail and complete BGSAVE before
returning; only advance last_snapshot_epoch without snapshot state for auto-save
epochs.

In `@tests/dir_deleted_degraded.rs`:
- Around line 74-84: Make the CPU-time probe fail explicitly when the ps command
fails or produces empty output. In the elapsed-time parsing block, validate
out.status, require non-empty trimmed stdout, and replace the unwrap_or(0.0)
fallback with strict field parsing that propagates or asserts on malformed
values, preventing invalid output from becoming 0ms.
🪄 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: b1607409-7abe-4049-ab50-81bcae566fb0

📥 Commits

Reviewing files that changed from the base of the PR and between ec55671 and 4a712b0.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • src/main.rs
  • src/server/conn/handler_monoio/dispatch.rs
  • src/server/conn/handler_sharded/dispatch.rs
  • src/shard/disk_monitor.rs
  • src/shard/persistence_tick.rs
  • tests/dir_deleted_degraded.rs

Comment thread src/shard/disk_monitor.rs
Comment on lines +155 to 166
Ok((free, total)) => {
if self.dir_guard && self.dir_lost.swap(false, Ordering::Release) {
tracing::warn!(
path = %self.monitored_path.display(),
"disk_monitor: data directory reappeared — writes resume; \
data accepted since the deletion is NOT guaranteed durable \
until the server is restarted (WAL fds point at unlinked \
inodes)",
);
}
self.free_bytes.store(free, Ordering::Relaxed);
self.update_paused(free, total);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file and inspect the relevant sections.
ast-grep outline src/shard/disk_monitor.rs --view expanded || true

echo
echo "---- relevant lines ----"
sed -n '1,260p' src/shard/disk_monitor.rs | cat -n

echo
echo "---- search for dir_lost / statvfs / metadata checks ----"
rg -n "dir_lost|statvfs|metadata\\(|is_dir\\(|mounted|mountpoint|fstatfs|statfs|realpath" src/shard/disk_monitor.rs

Repository: pilotspace/moon

Length of output: 17354


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the disk monitor implementation around the latch update and any fallback.
wc -l src/shard/disk_monitor.rs
sed -n '1,220p' src/shard/disk_monitor.rs | cat -n

Repository: pilotspace/moon

Length of output: 11233


Keep dir_lost latched until the path is still the monitored directory. statvfs can still succeed for a regular file or after a mountpoint loses its backing mount, so this branch can clear the latch and resume writes even though the persistence target is no longer valid. Check metadata().is_dir() and, if mount loss matters, the original filesystem identity before clearing.

🤖 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 `@src/shard/disk_monitor.rs` around lines 155 - 166, In the successful
`statvfs` branch of the disk-monitor update logic, do not clear `dir_lost` or
resume writes unless `self.monitored_path` still exists as a directory; validate
it with metadata before the `dir_lost.swap(false, Ordering::Release)`
transition. Preserve the latch and paused state when the path is a regular file
or otherwise invalid, and retain the existing warning only for a genuinely
restored directory.

Comment thread src/shard/disk_monitor.rs
Comment on lines +393 to +397
/// Non-Unix stub: monitoring is a no-op, writes are never paused by disk and
/// the dir-lost latch never engages.
#[cfg(not(unix))]
fn query_free_bytes(_path: &Path) -> Option<(u64, u64)> {
Some((u64::MAX, u64::MAX))
fn query_free_bytes(_path: &Path) -> Result<(u64, u64), QueryError> {
Ok((u64::MAX, u64::MAX))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and inspect the non-Unix implementation plus nearby logic.
ast-grep outline src/shard/disk_monitor.rs --view expanded || true
wc -l src/shard/disk_monitor.rs
sed -n '1,260p' src/shard/disk_monitor.rs
printf '\n----\n'
sed -n '260,460p' src/shard/disk_monitor.rs
printf '\n----\n'
sed -n '460,620p' src/shard/disk_monitor.rs

# Find all uses of the directory-loss / PathMissing path.
rg -n "PathMissing|dir-lost|directory|query_free_bytes|monitoring is a no-op|degraded mode|pause" src/shard -S

Repository: pilotspace/moon

Length of output: 45832


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether Windows / non-Unix is treated as a supported runtime target.
rg -n "#\[cfg\(not\(unix\)\)\]|windows|win32|microsoft|non-unix|target_os = \"windows\"" src Cargo.toml README.md -S

# Inspect the persistence tick / hot-path gating that depends on dir-lost.
sed -n '100,170p' src/shard/persistence_tick.rs
printf '\n----\n'
sed -n '220,260p' src/shard/persistence_tick.rs
printf '\n----\n'
sed -n '1110,1275p' src/shard/persistence_tick.rs
printf '\n----\n'
sed -n '1,120p' src/shard/segment_stall.rs

Repository: pilotspace/moon

Length of output: 32760


Implement non-Unix directory-loss detection.
The #[cfg(not(unix))] stub always reports a healthy path, so query_free_bytes can never surface PathMissing and is_dir_lost() stays false on supported non-Unix targets. Add a portable metadata/is_dir() check and return PathMissing when --dir is missing or no longer a directory, or the persistence tick will keep retrying doomed filesystem work after the directory disappears.

🤖 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 `@src/shard/disk_monitor.rs` around lines 393 - 397, Update the non-Unix
`query_free_bytes` stub to check the supplied path with portable metadata and
verify it is a directory; return `QueryError::PathMissing` when metadata fails
or the path is not a directory, otherwise preserve the existing unlimited
free-space result.

Comment on lines 117 to +129
if new_epoch > *last_snapshot_epoch && snapshot_state.is_none() {
*last_snapshot_epoch = new_epoch;
// #366: consume the epoch but skip the save while the data directory
// is missing — the snapshot temp file can't be created, and retrying
// per save-point trigger just spams one doomed attempt per epoch.
if crate::shard::disk_monitor::is_dir_lost() {
tracing::warn!(
"Shard {}: auto-save epoch {} skipped — data directory missing",
shard_id,
new_epoch
);
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not consume manual BGSAVE triggers through the auto-save skip path.

This watch channel drives both auto-save and BGSAVE. The code advances last_snapshot_epoch, then returns without creating snapshot state or completing/failing BGSAVE coordination, potentially leaving BGSAVE permanently in progress and bypassing its existing error path.

Distinguish trigger origin, or explicitly fail and complete manual BGSAVE while only consuming auto-save epochs.

🤖 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 `@src/shard/persistence_tick.rs` around lines 117 - 129, Update the
epoch-handling logic around last_snapshot_epoch so the data-directory-missing
early return applies only to auto-save triggers. Preserve manual BGSAVE triggers
for their existing snapshot/error coordination path, or explicitly fail and
complete BGSAVE before returning; only advance last_snapshot_epoch without
snapshot state for auto-save epochs.

Comment on lines +74 to +84
let out = std::process::Command::new("ps")
.args(["-o", "cputime=", "-p", &pid.to_string()])
.output()
.expect("ps");
let s = String::from_utf8_lossy(&out.stdout);
let s = s.trim().replace('-', ":"); // DD-HH:MM:SS → DD:HH:MM:SS
let mut secs = 0.0f64;
for field in s.split(':') {
secs = secs * 60.0 + field.parse::<f64>().unwrap_or(0.0);
}
(secs * 1000.0) as u64

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fail the test when the CPU probe cannot be parsed.

A failed ps invocation or changed output format currently becomes 0ms, making the spin regression assertion pass falsely. Check out.status, require non-empty output, and reject malformed fields instead of using unwrap_or(0.0).

🤖 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 `@tests/dir_deleted_degraded.rs` around lines 74 - 84, Make the CPU-time probe
fail explicitly when the ps command fails or produces empty output. In the
elapsed-time parsing block, validate out.status, require non-empty trimmed
stdout, and replace the unwrap_or(0.0) fallback with strict field parsing that
propagates or asserts on malformed values, preventing invalid output from
becoming 0ms.

…review round (B1, S1-class, S3, N1, N2)

Adversarial review of the dir-lost latch found one blocker and three
gaps; all fixed here, two skipped with reasons (PR thread).

B1 (blocker) — shallow heal re-armed the original bug UNLATCHED: the
heal check is statvfs on the top-level --dir only, but WalWriterV3
creates <dir>/shard-N/wal-v3/ exactly once at startup and
open_new_segment() only `create(true)`s the LEAF file. An operator
answering "MOONERR dirmissing" with `mkdir -p <dir>` cleared the latch
while the nested WAL dir stayed missing — the first post-heal segment
rotation then failed ENOENT on every 1ms tick, reproducing issue #366
with the latch already released. Two independent layers:

- open_new_segment() now runs fs::create_dir_all(&wal_dir) first —
  rotation self-repairs a vanished parent (rotations are rare; one
  mkdir syscall is free). Unit-pinned by
  test_rotation_self_repairs_deleted_wal_dir (tiny 256-byte segment,
  delete the wal dir under an open writer, force rotation, assert it
  recreates the dir and lands the new segment).
- flush_wal_v3_if_needed() arms a 1s backoff on ANY flush failure
  (flush_backing_off/note_flush_failure/clear_flush_backoff on
  WalWriterV3) — no future failure class (EACCES, quota, …) can loop
  at tick cadence. Healthy-path cost: one Option discriminant check.
  Unit-pinned by test_flush_backoff_arms_and_clears.

S3 — disk-offload without AOF/save never armed the guard: spill and
the vector warm tier write to the data dir even with appendonly=no.
init_global now arms on persistence OR disk_offload_enabled(), and
check_warm_transitions (10s cadence segment writer) gates on
is_dir_lost() like the other tick functions.

N1 — module doc no longer claims clean-unmount coverage (mountpoint
dir survives an unmount; catching it needs an st_dev comparison —
follow-up material).

N2 — QueryError variants are only constructed under cfg(unix); added
cfg_attr(not(unix), allow(dead_code)) so the main-push-only Windows
Check job can't fail on the dead-variant lint.

Skipped with reason:
- S2 (explicit BGSAVE not fast-failed while latched): the existing
  finalize-error path already fails the one attempt cleanly and keeps
  the BGSAVE fan-in counter correct; a fast-fail reply would need its
  own counter accounting and risks the stuck-in-progress bug class.
- S1 residual (expiry/eviction WAL appends growing the buffer during
  an outage): append-time flush errors predate this PR (same class as
  diskfull) and are now bounded by the 1s backoff; full write-path
  gating of internal ops is follow-up scope.

author: Tin Dang <tindang.ht97@gmail.com>
@pilotspacex-byte

Copy link
Copy Markdown
Contributor Author

Adversarial review round applied in 5b01285 (verdict was BLOCK on B1; all findings triaged):

  • B1 (blocker) — FIXED, two layers: open_new_segment() now create_dir_alls the WAL dir (rotation self-repairs a shallow mkdir -p heal; unit-pinned by test_rotation_self_repairs_deleted_wal_dir), and flush_wal_v3_if_needed arms a 1s backoff on any flush failure so no error class can ever loop at the 1ms tick cadence again (test_flush_backoff_arms_and_clears).
  • S3 — FIXED: guard now arms for disk_offload_enabled() configs too, and check_warm_transitions gates on is_dir_lost(). Custom --disk-offload-dir on a different volume than --dir is still only monitored via --dir — noted as follow-up.
  • N1 — FIXED: doc no longer claims clean-unmount coverage (needs st_dev comparison — follow-up).
  • N2 — FIXED: cfg_attr(not(unix), allow(dead_code)) on QueryError so the main-push-only Windows Check can't trip the dead-variant lint.
  • S2 — SKIPPED: explicit BGSAVE while latched already fails its one attempt cleanly with correct fan-in counter accounting via the finalize-error path; a fast-fail reply would need its own counter handling and risks the stuck-rdb_bgsave_in_progress bug class (see bgsave-startup-race history).
  • S1 residual — SKIPPED: append-time flush errors from internal writers (expiry/eviction) predate this PR (same class as diskfull) and are now bounded by the 1s backoff; gating internal write paths is follow-up scope.
  • N3 — acknowledged: RECL_* global-metric coupling in DiskMonitor::poll predates this PR; no test asserts those atomics today.

Local gates on the amended head: fmt, clippy (default + tokio,jemalloc), 4371 lib tests, integration test green.

@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.

🧹 Nitpick comments (1)
src/persistence/wal_v3/segment.rs (1)

580-587: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify clear_flush_backoff by unconditionally assigning None.

Checking is_some() introduces an unnecessary branch. Directly assigning None is cleaner and slightly more efficient.

♻️ Proposed refactor
     /// Clear the backoff after a successful flush.
     #[inline]
     pub fn clear_flush_backoff(&mut self) {
-        if self.flush_backoff_until.is_some() {
-            self.flush_backoff_until = None;
-        }
+        self.flush_backoff_until = None;
     }
🤖 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 `@src/persistence/wal_v3/segment.rs` around lines 580 - 587, simplify
clear_flush_backoff by directly assigning None to flush_backoff_until, removing
the redundant is_some conditional while preserving its existing behavior.
🤖 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.

Nitpick comments:
In `@src/persistence/wal_v3/segment.rs`:
- Around line 580-587: simplify clear_flush_backoff by directly assigning None
to flush_backoff_until, removing the redundant is_some conditional while
preserving its existing behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a9098232-815c-41f1-9e25-f54e85b11bf8

📥 Commits

Reviewing files that changed from the base of the PR and between 4a712b0 and 5b01285.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • src/main.rs
  • src/persistence/wal_v3/segment.rs
  • src/shard/disk_monitor.rs
  • src/shard/persistence_tick.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main.rs
  • src/shard/persistence_tick.rs
  • CHANGELOG.md
  • src/shard/disk_monitor.rs

@pilotspacex-byte

Copy link
Copy Markdown
Contributor Author

B1 scenario proven end-to-end against the amended binary (2 shards, --appendonly yes --wal-kv-log on --wal-segment-size 64kb):

  1. rm -rf $dirMOONERR dirmissing latched in ~4s.
  2. Shallow heal (mkdir -p $dir only — nested shard-N/wal-v3/ NOT recreated) → writes resumed in ~6s.
  3. 100 × 4KB SETs forced WAL v3 segment rotation post-heal → rotation self-repaired the nested dirs and landed shard-{0,1}/wal-v3/000000000002.wal; grep -c "flush failed" = 0; CPU 2.6% over a 10s idle window; final SET OK.

Pre-B1-fix this step 3 would have ENOENT-looped the 1ms tick with the latch already cleared. Linux VM re-run on the amended head: 28 segment unit tests + integration test green.

@pilotspacex-byte
pilotspacex-byte merged commit 1e86f3a into main Jul 17, 2026
9 checks passed
@TinDang97
TinDang97 deleted the fix/dir-deleted-degraded-latch branch July 17, 2026 06:43
pilotspacex-byte added a commit that referenced this pull request Jul 17, 2026
…ows Check red on main (#370)

Windows Check (main-push-only, PR-invisible) failed 3/3 tries on
test_dir_lost_latches_pauses_and_self_heals after #369 merged: the
non-unix query_free_bytes stub always returns Ok, so the dir-lost
latch never engages on Windows BY DESIGN — the test's dir_lost()
assertion can't hold there. Gate the latch-behavior test with
cfg(unix); the disarmed-guard sibling stays cross-platform (its
expectations hold under the stub). Same Windows-gate treatment the
integration suite already had (#![cfg(unix)]).

author: Tin Dang <tindang.ht97@gmail.com>

Co-authored-by: Tin Dang <tindang.ht97@gmail.com>
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.

Data-dir deleted under a running server → per-shard tick error loop burns ~100% CPU per thread, no backoff, server wedges

2 participants