Skip to content

feat(persistence): automatic AOF rewrite + un-gated multi-shard BGREWRITEAOF (#433) - #443

Merged
TinDang97 merged 2 commits into
mainfrom
fix/433-aof-auto-rewrite
Aug 7, 2026
Merged

feat(persistence): automatic AOF rewrite + un-gated multi-shard BGREWRITEAOF (#433)#443
TinDang97 merged 2 commits into
mainfrom
fix/433-aof-auto-rewrite

Conversation

@TinDang97

@TinDang97 TinDang97 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #433. Fixes #432.

Problem

The AOF was append-only with no compaction path: it grew with write volume, not dataset size (observed live: 4.8 GB appendonlydir over a 2.43 GB dataset, ~1 GB/day) until the diskfull guard paused writes. On the default multi-shard config even the manual escape hatch was gated off (BGREWRITEAOF refused unless --experimental-per-shard-rewrite). And INFO persistence reported aof_enabled:0 unconditionally, so operators couldn't even see it happening.

Changes

1. Redis-parity automatic rewrite. New --auto-aof-rewrite-percentage (default 100, 0 disables) and --auto-aof-rewrite-min-size (default 64mb; size strings). A monitor thread (src/persistence/aof/auto_rewrite.rs) samples the on-disk AOF size once a second and dispatches bgrewriteaof_start_sharded — the exact entry the command uses — when current >= min_size and (current−base)·100/max(base,1) >= percentage. Base re-records after boot recovery and each completed rewrite. Design-for-failure: failed dispatch arms a 60 s cooldown (no hot-retry livelock); skipped during BGSAVE/rewrite; nothing on the hot path. Both knobs in CONFIG GET; conf-file keys work.

2. Multi-shard BGREWRITEAOF un-gated. The per-shard fan-out (C4 cooperative snapshot + synchronized manifest commit) is the default. The gate dated from the pre-C4 ~38%-key-loss era; the current path holds exact INCR recovery across a rewrite straddling a live write stream + SIGKILL. --experimental-per-shard-rewrite deprecated (warn, no-op).

3. INFO persistence real AOF state (#432). aof_enabled / aof_rewrite_in_progress real; new aof_base_size / aof_current_size expose the trigger's inputs.

4. Crash-matrix harness fix. crash_matrix_per_shard_bgrewriteaof lacked --disk-free-min-pct 0 and parsed MOONERR diskfull INCR rejections as -1 — on this host (root volume ~4% free) the straddle test "lost" 261/500 INCRs that were never acked; a reply-capture hunt proved the rewrite itself exact. Harness now disables the guard and panics on non-numeric INCR replies; green 5/5 consecutive runs.

Layout note (found by the dual-runtime leg): tokio's TopLevel (shards=1) writer appends to a legacy flat appendonly.aof, not the appendonlydir manifest monoio uses. The sampler measures both; its in-place rewrite is detected in tests by shrink-below-high-water sampled during the write stream.

Tests (red/green TDD)

tests/aof_auto_rewrite.rs (all #[ignore], crash-suite convention, run explicitly against both runtime binaries — monoio and tokio 5/5 each):

  • un-gated manual rewrite (shards=2, no flag) + SIGKILL-exact recovery
  • auto trigger fires on growth at shards=2 AND shards=1, INCR-exact recovery
  • percentage 0 disables
  • INFO fields real, current > base after writes

Unit tests pin the trigger predicate (boundaries, zero-base→1 Redis rule, min-size floor, overflow).

Gates

  • Crash matrix green 5/5 repeat runs; shardslice_live fold suite green
  • Full monoio release suite green — sole exceptions: the two known client_tracking_invalidation push-delivery flakes, A/B-verified same failure band on pristine main under identical host conditions
  • Tokio lib tests 3653/3653; clippy -D warnings both feature sets; fmt
  • No hot-path code touched (monitor = 1 stat-walk/s off-thread) — bench waived

Summary by CodeRabbit

  • New Features

    • Added automatic AOF rewrites based on configurable growth-percentage and minimum-size thresholds.
    • Enabled multi-shard BGREWRITEAOF by default.
    • Added visibility into AOF status and current/base sizes through monitoring commands.
    • Added configuration visibility for automatic rewrite settings.
  • Documentation

    • Documented automatic and multi-shard AOF rewrite behavior, including threshold controls.
  • Bug Fixes

    • Improved rewrite retry handling, recovery validation, and crash-test accuracy.
    • Deprecated the experimental per-shard rewrite option; it is now ignored with a warning.

…RITEAOF (#433)

The AOF was append-only with no compaction path: it grew with write
volume, not dataset size (observed live: 4.8 GB appendonlydir over a
2.43 GB dataset, ~1 GB/day) until the diskfull guard paused writes. And
on the default multi-shard config even the manual escape hatch was
gated off (BGREWRITEAOF refused unless --experimental-per-shard-rewrite).

Three changes:

1. Redis-parity automatic rewrite. New flags
   --auto-aof-rewrite-percentage (default 100, 0 disables) and
   --auto-aof-rewrite-min-size (default "64mb", size strings accepted;
   both in CONFIG GET, conf-file keys work via the generic key->flag
   synthesis). A monitor thread (src/persistence/aof/auto_rewrite.rs)
   samples the on-disk appendonlydir size once a second and dispatches
   bgrewriteaof_start_sharded — the exact entry the command uses (CAS
   in-progress flag, PerShard fan-out vs TopLevel routing) — when
   current >= min_size and (current-base)*100/max(base,1) >= percentage.
   Base re-records after boot recovery and each completed rewrite
   (deterministic post-dispatch wait + flag-transition + shrunk-below-
   base detection for manual rewrites that finish between ticks).
   Design-for-failure: failed dispatch arms a 60 s cooldown (no
   hot-retry livelock); skipped while BGSAVE or a rewrite runs; one
   directory walk per second, nothing on the hot path.

2. Multi-shard BGREWRITEAOF un-gated. The per-shard fan-out (C4
   cooperative snapshot + synchronized manifest commit) is the default;
   MULTI_SHARD_AOF_REWRITE_UNSAFE is no longer set at boot.
   --experimental-per-shard-rewrite is deprecated (warn, no-op). The
   historical ~38%-key-loss gate predated the C4 redesign; the current
   path holds exact INCR recovery across a straddling rewrite + SIGKILL.

3. INFO persistence reports real AOF state (#432). aof_enabled and
   aof_rewrite_in_progress were hardcoded 0; now real, plus new
   aof_base_size / aof_current_size fields exposing the trigger's
   inputs (current refreshed on read — INFO is cold path).

Also fixes the crash-matrix harness that made the per-shard rewrite
look broken on this host: crash_matrix_per_shard_bgrewriteaof lacked
--disk-free-min-pct 0 and parsed MOONERR-diskfull INCR rejections as -1
— on a root volume hovering at ~4% free the straddle test "lost"
261/500 INCRs that were never acked (reply-capture hunt proved the
server exact). The harness now disables the guard and panics on any
non-numeric INCR reply; suite green 5/5 consecutive runs.

Layout note (found by the dual-runtime run): the tokio TopLevel
(shards=1) writer appends to a legacy flat appendonly.aof, not the
appendonlydir manifest the monoio writers use — the size sampler
measures both (manifest walk + flat-file stat), and its in-place
rewrite is detected in tests via shrink-below-high-water sampled
DURING the write stream (a post-hoc poll misses a mid-stream
compaction that immediately regrows).

Tests (red/green): tests/aof_auto_rewrite.rs — un-gated manual rewrite
(shards=2, no flag) with SIGKILL-exact recovery; auto trigger fires on
growth at shards=2 AND shards=1 with INCR-exact recovery;
percentage=0 never triggers; INFO fields real and current>base after
writes. All #[ignore]d (spawn real binaries + SIGKILL, crash-suite
convention), run explicitly against BOTH runtime binaries — monoio and
tokio 5/5 each. Unit tests pin the trigger predicate (boundaries,
zero-base=1 Redis rule, min-size floor, overflow).

Gates: crash matrix green (repeat runs), shardslice_live fold suite
green, full monoio release suite green, clippy -D warnings both
feature sets, fmt. No hot-path code touched — bench waived (monitor
is 1 stat-walk/s off-thread).

Fixes #433
Fixes #432
author: Tin Dang
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@TinDang97, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

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.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 43fcdd2a-6e4b-4454-9b2d-1a61b79f537e

📥 Commits

Reviewing files that changed from the base of the PR and between e8f53aa and 5cf27f5.

📒 Files selected for processing (3)
  • tests/mq_integration.rs
  • tests/txn_kv_wiring.rs
  • tests/workspace_integration.rs
📝 Walkthrough

Walkthrough

The PR adds automatic AOF rewrite monitoring with configurable size and growth thresholds. Multi-shard BGREWRITEAOF runs by default. INFO persistence and CONFIG GET expose live AOF data. Integration and crash-recovery tests cover rewrite behavior.

Changes

AOF rewrite lifecycle

Layer / File(s) Summary
Configuration and startup wiring
src/config.rs, src/main.rs, src/persistence/aof/mod.rs, CHANGELOG.md
Adds automatic rewrite settings, deprecates the experimental gate, and starts monitoring after AOF recovery.
Automatic rewrite monitor
src/persistence/aof/auto_rewrite.rs
Tracks AOF sizes, evaluates thresholds, skips conflicting operations, applies failure backoff, and dispatches sharded rewrites.
Multi-shard rewrite validation
docs/runbooks/multi-shard-aof-rewrite.md, tests/aof_auto_rewrite.rs, tests/crash_matrix_per_shard_bgrewriteaof.rs
Documents and tests manual and automatic rewrites, legacy layouts, disabled automation, and crash recovery.
Persistence configuration and metrics
src/command/config.rs, src/command/connection.rs, tests/aof_auto_rewrite.rs, CHANGELOG.md
Exposes rewrite thresholds through CONFIG GET and reports live AOF state and sizes through INFO persistence.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AOFMonitor
  participant AOFStorage
  participant BGREWRITEAOF
  AOFMonitor->>AOFStorage: Refresh current AOF size
  AOFMonitor->>AOFMonitor: Evaluate thresholds and rewrite guards
  AOFMonitor->>BGREWRITEAOF: Dispatch sharded rewrite
  BGREWRITEAOF->>AOFStorage: Compact AOF generations
  AOFMonitor->>AOFStorage: Record new baseline
Loading

Possibly related PRs

Suggested reviewers: pilotspacex-byte

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary changes: automatic AOF rewriting and enabling multi-shard BGREWRITEAOF.
Description check ✅ Passed The description provides a detailed summary, test results, design notes, and performance impact, despite omitting some template headings.
Linked Issues check ✅ Passed The changes satisfy the objectives in [#433] and [#432] by adding automatic rewrites, enabling multi-shard rewrites, and reporting real AOF state.
Out of Scope Changes check ✅ Passed The documentation, tests, configuration, monitoring, and crash-matrix changes directly support the linked issue objectives.
Docstring Coverage ✅ Passed Docstring coverage is 97.22% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/433-aof-auto-rewrite

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.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

feat(persistence): auto AOF rewrite + default multi-shard BGREWRITEAOF

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add Redis-parity automatic AOF rewrite trigger based on on-disk growth thresholds.
• Make multi-shard BGREWRITEAOF the default and deprecate the experimental gate.
• Fix INFO persistence AOF fields and harden crash-matrix harness against diskfull artifacts.
Diagram

graph TD
  OP["Operator / Config"] --> CFG["ServerConfig + CONFIG GET"] --> MON["Auto-rewrite monitor"] --> START["bgrewriteaof_start_sharded"] --> POOL["AOF writer pool"] --> DISK[("AOF files on disk")]
  MON --> STAT["AOF size statics"] --> INFO["INFO persistence"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Integrate rewrite checks into the main runtime tick (async)
  • ➕ Avoids a dedicated OS thread
  • ➕ Could reuse existing scheduling/telemetry hooks
  • ➖ More coupling with hot-path runtime/event-loop concerns
  • ➖ Harder to keep strictly off the write path across both runtimes
2. Track AOF size via writer-side counters instead of filesystem walks
  • ➕ No directory walking / metadata IO
  • ➕ Immediate size updates and simpler INFO freshness
  • ➖ More invasive plumbing across writer implementations and rewrite codepaths
  • ➖ Risk of counter drift vs actual on-disk bytes (especially across crashes/partial writes)

Recommendation: Current approach is a good tradeoff for correctness and low risk: it reuses the existing BGREWRITEAOF entry point, keeps all work off the hot path, and bases triggering on the authoritative on-disk size (including both manifest and legacy layouts). The filesystem walk cost (1Hz) is acceptable for a persistence control-plane thread, and the cooldown/backoff reduces operational risk under deterministic failures.

Files changed (10) +834 / -66

Enhancement (3) +315 / -34
main.rsRemove multi-shard BGREWRITEAOF gate; start auto-rewrite monitor after recovery +37/-34

Remove multi-shard BGREWRITEAOF gate; start auto-rewrite monitor after recovery

• Retires the boot-time refusal/gating logic for per-shard BGREWRITEAOF and emits only a deprecation warning if the old flag is used. Initializes and spawns the auto-rewrite monitor after AOF recovery so the post-replay size becomes the new baseline.

src/main.rs

auto_rewrite.rsImplement 1Hz auto AOF rewrite monitor with cooldown and INFO statics +275/-0

Implement 1Hz auto AOF rewrite monitor with cooldown and INFO statics

• Adds a new module that measures total AOF size (manifest directory or legacy file), maintains base/current size statics, and triggers BGREWRITEAOF automatically when growth thresholds are met. Includes safety behavior (skip during rewrite/BGSAVE, 60s cooldown after failed dispatch) and unit tests for the trigger predicate math.

src/persistence/aof/auto_rewrite.rs

mod.rsExport auto_rewrite submodule +3/-0

Export auto_rewrite submodule

• Registers the new auto_rewrite module within the persistence AOF module tree so it can be initialized from main and queried from INFO.

src/persistence/aof/mod.rs

Bug fix (1) +23 / -2
connection.rsFix INFO persistence to report real AOF state and sizes +23/-2

Fix INFO persistence to report real AOF state and sizes

• Stops hardcoding aof_enabled/aof_rewrite_in_progress to 0 and adds aof_base_size/aof_current_size fields. Ensures aof_current_size is refreshed on INFO reads for freshness between monitor ticks.

src/command/connection.rs

Tests (2) +417 / -8
aof_auto_rewrite.rsAdd crash-style integration tests for auto rewrite and un-gated BGREWRITEAOF +397/-0

Add crash-style integration tests for auto rewrite and un-gated BGREWRITEAOF

• Introduces ignored tests that spawn real binaries, drive INCR workloads, and validate (1) multi-shard BGREWRITEAOF works by default, (2) auto rewrite triggers at configured thresholds, (3) percentage=0 disables the trigger, (4) SIGKILL recovery exactness, and (5) INFO persistence fields are real. Handles both manifest-based and legacy in-place rewrite detection across runtimes.

tests/aof_auto_rewrite.rs

crash_matrix_per_shard_bgrewriteaof.rsFix crash-matrix harness: disable diskfull guard and fail loudly on INCR errors +20/-8

Fix crash-matrix harness: disable diskfull guard and fail loudly on INCR errors

• Updates the harness to always pass --disk-free-min-pct 0 and to panic on non-numeric INCR replies, preventing MOONERR diskfull rejections from being miscounted as lost writes. Also updates test docs to reflect both runtimes running the same per-shard fold path.

tests/crash_matrix_per_shard_bgrewriteaof.rs

Documentation (2) +49 / -9
CHANGELOG.mdDocument auto AOF rewrite, BGREWRITEAOF default, and INFO persistence fixes +30/-0

Document auto AOF rewrite, BGREWRITEAOF default, and INFO persistence fixes

• Adds Unreleased notes describing the new automatic AOF compaction knobs and behavior, the un-gating of multi-shard BGREWRITEAOF, the corrected INFO persistence fields, and the crash harness fix for diskfull-induced false positives.

CHANGELOG.md

multi-shard-aof-rewrite.mdUpdate runbook: per-shard BGREWRITEAOF supported + auto rewrite guidance +19/-9

Update runbook: per-shard BGREWRITEAOF supported + auto rewrite guidance

• Replaces the prior 'not yet supported' guidance with the new default per-shard fan-out rewrite behavior and documents the automatic rewrite trigger knobs and how to monitor them via INFO persistence.

docs/runbooks/multi-shard-aof-rewrite.md

Other (2) +30 / -13
config.rsExpose auto AOF rewrite knobs via CONFIG GET +11/-0

Expose auto AOF rewrite knobs via CONFIG GET

• Adds CONFIG GET keys for auto-aof-rewrite-percentage and auto-aof-rewrite-min-size, normalizing the min-size value to bytes (Redis-style) even when configured via size strings.

src/command/config.rs

config.rsAdd auto AOF rewrite flags; deprecate experimental per-shard rewrite flag +19/-13

Add auto AOF rewrite flags; deprecate experimental per-shard rewrite flag

• Introduces --auto-aof-rewrite-percentage and --auto-aof-rewrite-min-size CLI/config options with Redis-parity semantics. Marks --experimental-per-shard-rewrite as deprecated/no-op now that per-shard rewrite is the default.

src/config.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: 5

🧹 Nitpick comments (2)
src/main.rs (1)

1754-1761: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The auto-aof-rewrite-min-size fallback is duplicated. ServerConfig stores the raw string and exposes no resolved accessor, so two call sites each run ServerConfig::parse_size and each hardcode 64 * 1024 * 1024 on failure. The two values agree today only by coincidence. If one default changes, CONFIG GET reports a threshold the monitor does not use.

Add one resolver on ServerConfig, for example pub fn auto_aof_rewrite_min_size_bytes(&self) -> u64, that owns both the parse and the default, then call it from both sites.

  • src/main.rs#L1754-L1761: replace the inline parse_size(...).unwrap_or_else(...) with the new accessor. Keep the startup tracing::warn! for the unparseable case inside the accessor or at this site only.
  • src/command/config.rs#L48-L54: replace ServerConfig::parse_size(&server_config.auto_aof_rewrite_min_size).unwrap_or(64 * 1024 * 1024) with the same accessor.
🤖 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/main.rs` around lines 1754 - 1761, Centralize auto-AOF rewrite
minimum-size resolution in a new ServerConfig accessor such as
auto_aof_rewrite_min_size_bytes, owning parsing and the 64 MiB fallback. Update
src/main.rs lines 1754-1761 to use it while preserving the startup warning, and
update src/command/config.rs lines 48-54 to use the same accessor instead of
parsing and hardcoding the fallback independently.
tests/aof_auto_rewrite.rs (1)

200-202: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove or document the unused compacting helper.

wait_for_compaction is not called by any test in tests/aof_auto_rewrite.rs, so it will produce an unused-private-function warning. It also cannot observe the in-place shrink path: a fresh CompactionTracker has high_water == 0, and the shrink check requires high_water > 1024. A new helper should document seq-only semantics if kept, otherwise remove it.

🤖 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/aof_auto_rewrite.rs` around lines 200 - 202, Remove the unused
wait_for_compaction helper from tests/aof_auto_rewrite.rs; if retaining it,
document its seq-only semantics and ensure callers use a CompactionTracker with
the required high_water state so it can observe in-place shrinking.

Source: Coding guidelines

🤖 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/command/connection.rs`:
- Around line 281-291: Update the AOF size assignment in the INFO-building flow
to read the existing auto-rewrite::AOF_CURRENT_SIZE atomic with the appropriate
relaxed ordering instead of calling refresh_current_size(). Remove the
synchronous filesystem-walk path from this INFO request while preserving the
disabled-AOF value of zero.

In `@src/persistence/aof/auto_rewrite.rs`:
- Around line 137-152: Make the auto-rewrite monitor cancellable: in
src/persistence/aof/auto_rewrite.rs lines 137-152, add a CancellationToken
parameter to spawn_monitor, pass it to monitor_loop, and exit after each TICK
sleep or during the 300-second completion wait when cancellation is set; in
src/main.rs lines 1749-1768, pass cancel_token.child_token() to spawn_monitor.
- Around line 216-229: Update the completion-wait branch around
AOF_REWRITE_IN_PROGRESS so record_base_size() and saw_in_progress reset occur
only after the flag has actually cleared before the deadline. If the wait times
out while the rewrite remains in progress, preserve the existing baseline and
saw_in_progress state so a later tick can detect completion and rebase
correctly.

In `@tests/aof_auto_rewrite.rs`:
- Around line 350-354: Update the max_base_seq assertion in the
auto-aof-rewrite-percentage 0 test to accept any value less than or equal to 1,
preserving the intended no-rewrite behavior while allowing fresh multi-shard
boots that have not created a seq-1 base file.
- Around line 99-107: Add an accurate // SAFETY: comment immediately before the
unsafe block in sigkill, documenting why calling libc::kill with the child
process ID is safe. Leave the surrounding platform-specific termination and wait
behavior unchanged.

---

Nitpick comments:
In `@src/main.rs`:
- Around line 1754-1761: Centralize auto-AOF rewrite minimum-size resolution in
a new ServerConfig accessor such as auto_aof_rewrite_min_size_bytes, owning
parsing and the 64 MiB fallback. Update src/main.rs lines 1754-1761 to use it
while preserving the startup warning, and update src/command/config.rs lines
48-54 to use the same accessor instead of parsing and hardcoding the fallback
independently.

In `@tests/aof_auto_rewrite.rs`:
- Around line 200-202: Remove the unused wait_for_compaction helper from
tests/aof_auto_rewrite.rs; if retaining it, document its seq-only semantics and
ensure callers use a CompactionTracker with the required high_water state so it
can observe in-place shrinking.
🪄 Autofix

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: 60fed3ad-bc0e-4556-b57a-3697cafdf405

📥 Commits

Reviewing files that changed from the base of the PR and between cfb4bd6 and e8f53aa.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • docs/runbooks/multi-shard-aof-rewrite.md
  • src/command/config.rs
  • src/command/connection.rs
  • src/config.rs
  • src/main.rs
  • src/persistence/aof/auto_rewrite.rs
  • src/persistence/aof/mod.rs
  • tests/aof_auto_rewrite.rs
  • tests/crash_matrix_per_shard_bgrewriteaof.rs

Comment thread src/command/connection.rs
Comment on lines +281 to +291
// #432: aof_enabled / aof_rewrite_in_progress / sizes are real state, not
// hardcoded zeros. Sizes come from the auto-rewrite monitor's statics
// (#433); refresh_current_size keeps `aof_current_size` honest when INFO
// is read between monitor ticks (one directory walk — INFO is cold path).
let aof_enabled = crate::persistence::aof::auto_rewrite::AOF_ENABLED
.load(std::sync::atomic::Ordering::Relaxed);
let aof_current_size = if aof_enabled {
crate::persistence::aof::auto_rewrite::refresh_current_size()
} else {
0
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Do not run a filesystem walk on every INFO call.

Line 288 calls refresh_current_size(), which calls measure_total_size() in src/persistence/aof/auto_rewrite.rs. That function walks the whole appendonlydir tree recursively and issues one metadata syscall per file, plus one for the legacy flat file.

The comment says INFO is a cold path. Two facts contradict that:

  1. info at Line 172 ignores _args and always builds every section. A bare INFO therefore triggers the walk, not only INFO persistence.
  2. Monitoring agents poll INFO continuously, often from several collectors at once.

The walk is synchronous blocking IO on the shard event loop thread that serves the connection, so it stalls command processing for that shard. The cost grows with the number of retained generations and shards.

The auto-rewrite monitor already stores a fresh value in AOF_CURRENT_SIZE every second. Read that atomic instead and accept at most one second of staleness. init() seeds the atomic through record_base_size(), so it is populated before the first INFO.

⚡ Proposed fix
     let aof_enabled = crate::persistence::aof::auto_rewrite::AOF_ENABLED
         .load(std::sync::atomic::Ordering::Relaxed);
     let aof_current_size = if aof_enabled {
-        crate::persistence::aof::auto_rewrite::refresh_current_size()
+        // Read the monitor's cached sample (refreshed every TICK). A directory
+        // walk here would run on the shard event loop on every INFO call.
+        crate::persistence::aof::auto_rewrite::AOF_CURRENT_SIZE
+            .load(std::sync::atomic::Ordering::Relaxed)
     } else {
         0
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// #432: aof_enabled / aof_rewrite_in_progress / sizes are real state, not
// hardcoded zeros. Sizes come from the auto-rewrite monitor's statics
// (#433); refresh_current_size keeps `aof_current_size` honest when INFO
// is read between monitor ticks (one directory walk — INFO is cold path).
let aof_enabled = crate::persistence::aof::auto_rewrite::AOF_ENABLED
.load(std::sync::atomic::Ordering::Relaxed);
let aof_current_size = if aof_enabled {
crate::persistence::aof::auto_rewrite::refresh_current_size()
} else {
0
};
// `#432`: aof_enabled / aof_rewrite_in_progress / sizes are real state, not
// hardcoded zeros. Sizes come from the auto-rewrite monitor's statics
// (`#433`); refresh_current_size keeps `aof_current_size` honest when INFO
// is read between monitor ticks (one directory walk — INFO is cold path).
let aof_enabled = crate::persistence::aof::auto_rewrite::AOF_ENABLED
.load(std::sync::atomic::Ordering::Relaxed);
let aof_current_size = if aof_enabled {
// Read the monitor's cached sample (refreshed every TICK). A directory
// walk here would run on the shard event loop on every INFO call.
crate::persistence::aof::auto_rewrite::AOF_CURRENT_SIZE
.load(std::sync::atomic::Ordering::Relaxed)
} else {
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 `@src/command/connection.rs` around lines 281 - 291, Update the AOF size
assignment in the INFO-building flow to read the existing
auto-rewrite::AOF_CURRENT_SIZE atomic with the appropriate relaxed ordering
instead of calling refresh_current_size(). Remove the synchronous
filesystem-walk path from this INFO request while preserving the disabled-AOF
value of zero.

Comment on lines +137 to +152
pub fn spawn_monitor(
pool: Arc<super::AofWriterPool>,
shard_databases: Arc<crate::shard::shared_databases::ShardDatabases>,
percentage: u64,
min_size: u64,
) {
let spawned = std::thread::Builder::new()
.name("aof-auto-rewrite".to_string())
.spawn(move || {
monitor_loop(&pool, &shard_databases, percentage, min_size);
});
if let Err(e) = spawned {
// Non-fatal: manual BGREWRITEAOF still works; sizes go stale.
warn!("aof-auto-rewrite monitor failed to spawn: {e}");
}
}

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

The auto-rewrite monitor thread cannot be stopped. spawn_monitor takes no CancellationToken, so monitor_loop runs an unbounded loop for the life of the process. Every other auxiliary thread started in src/main.rs takes cancel_token.child_token() (per-shard AOF writers at Line 880, the TopLevel writer at Line 915, the auto-save thread at Line 2016). The monitor can therefore dispatch a rewrite after pool.broadcast_shutdown() runs at src/main.rs Line 2069.

  • src/persistence/aof/auto_rewrite.rs#L137-L152: add a cancel: CancellationToken parameter to spawn_monitor and forward it to monitor_loop. Check cancel.is_cancelled() after each TICK sleep and inside the 300 s completion wait, and return from the loop when it is set.
  • src/main.rs#L1749-L1768: pass cancel_token.child_token() as the new argument to spawn_monitor.
📍 Affects 2 files
  • src/persistence/aof/auto_rewrite.rs#L137-L152 (this comment)
  • src/main.rs#L1749-L1768
🤖 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/aof/auto_rewrite.rs` around lines 137 - 152, Make the
auto-rewrite monitor cancellable: in src/persistence/aof/auto_rewrite.rs lines
137-152, add a CancellationToken parameter to spawn_monitor, pass it to
monitor_loop, and exit after each TICK sleep or during the 300-second completion
wait when cancellation is set; in src/main.rs lines 1749-1768, pass
cancel_token.child_token() to spawn_monitor.

Comment on lines +216 to +229
_ => {
// Started. Wait for completion here (bounded) so the rebase
// is deterministic even when the whole rewrite fits inside
// one tick; the transition/shrink detection above is the
// fallback for manual rewrites.
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(300);
while AOF_REWRITE_IN_PROGRESS.load(Ordering::SeqCst)
&& std::time::Instant::now() < deadline
{
std::thread::sleep(std::time::Duration::from_millis(100));
}
record_base_size();
saw_in_progress = false;
}

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

Do not rebase the baseline when the completion wait times out.

If the 300 s deadline expires while AOF_REWRITE_IN_PROGRESS is still true, Line 227 still calls record_base_size(). At that moment both the old and the new AOF generation are on disk, so the measured total is inflated. aof_base_size in INFO persistence then reports a wrong value, and the next growth evaluation is measured against it.

Line 228 also clears saw_in_progress while the flag is still set, so the real completion transition is not detected on a later tick. The shrunk_below_base fallback recovers the correct baseline after the prune, so the state self-corrects, but only after at least one extra tick.

Rebase only when the flag actually cleared.

🐛 Proposed fix
                 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(300);
                 while AOF_REWRITE_IN_PROGRESS.load(Ordering::SeqCst)
                     && std::time::Instant::now() < deadline
                 {
                     std::thread::sleep(std::time::Duration::from_millis(100));
                 }
-                record_base_size();
-                saw_in_progress = false;
+                if AOF_REWRITE_IN_PROGRESS.load(Ordering::SeqCst) {
+                    // Deadline expired with the rewrite still running. Leave the
+                    // baseline alone and let the next tick's transition/shrink
+                    // detection rebase it.
+                    warn!(
+                        "aof-auto-rewrite: rewrite still in progress after 300s; \
+                         deferring baseline rebase"
+                    );
+                    saw_in_progress = true;
+                } else {
+                    record_base_size();
+                    saw_in_progress = false;
+                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_ => {
// Started. Wait for completion here (bounded) so the rebase
// is deterministic even when the whole rewrite fits inside
// one tick; the transition/shrink detection above is the
// fallback for manual rewrites.
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(300);
while AOF_REWRITE_IN_PROGRESS.load(Ordering::SeqCst)
&& std::time::Instant::now() < deadline
{
std::thread::sleep(std::time::Duration::from_millis(100));
}
record_base_size();
saw_in_progress = false;
}
_ => {
// Started. Wait for completion here (bounded) so the rebase
// is deterministic even when the whole rewrite fits inside
// one tick; the transition/shrink detection above is the
// fallback for manual rewrites.
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(300);
while AOF_REWRITE_IN_PROGRESS.load(Ordering::SeqCst)
&& std::time::Instant::now() < deadline
{
std::thread::sleep(std::time::Duration::from_millis(100));
}
if AOF_REWRITE_IN_PROGRESS.load(Ordering::SeqCst) {
// Deadline expired with the rewrite still running. Leave the
// baseline alone and let the next tick's transition/shrink
// detection rebase it.
warn!(
"aof-auto-rewrite: rewrite still in progress after 300s; \
deferring baseline rebase"
);
saw_in_progress = true;
} else {
record_base_size();
saw_in_progress = false;
}
}
🤖 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/aof/auto_rewrite.rs` around lines 216 - 229, Update the
completion-wait branch around AOF_REWRITE_IN_PROGRESS so record_base_size() and
saw_in_progress reset occur only after the flag has actually cleared before the
deadline. If the wait times out while the rewrite remains in progress, preserve
the existing baseline and saw_in_progress state so a later tick can detect
completion and rebase correctly.

Comment thread tests/aof_auto_rewrite.rs
Comment on lines +99 to +107
fn sigkill(child: &mut Child) {
#[cfg(unix)]
unsafe {
libc::kill(child.id() as i32, libc::SIGKILL);
}
#[cfg(not(unix))]
let _ = child.kill();
let _ = child.wait();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add a // SAFETY: comment to the unsafe block.

The coding guidelines require an accurate // SAFETY: comment on every unsafe block. Line 101 opens one without it.

🛡️ Proposed fix
 fn sigkill(child: &mut Child) {
     #[cfg(unix)]
+    // SAFETY: libc::kill with a pid owned by this process and a valid signal
+    // number. The call has no memory-safety preconditions; a reaped pid only
+    // returns ESRCH, which is ignored.
     unsafe {
         libc::kill(child.id() as i32, libc::SIGKILL);
     }

As per coding guidelines: "every unsafe block must include an accurate // SAFETY: comment".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn sigkill(child: &mut Child) {
#[cfg(unix)]
unsafe {
libc::kill(child.id() as i32, libc::SIGKILL);
}
#[cfg(not(unix))]
let _ = child.kill();
let _ = child.wait();
}
fn sigkill(child: &mut Child) {
#[cfg(unix)]
// SAFETY: `libc::kill` has no memory-safety preconditions; `SIGKILL` is valid.
unsafe {
libc::kill(child.id() as i32, libc::SIGKILL);
}
#[cfg(not(unix))]
let _ = child.kill();
let _ = child.wait();
}
🤖 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/aof_auto_rewrite.rs` around lines 99 - 107, Add an accurate // SAFETY:
comment immediately before the unsafe block in sigkill, documenting why calling
libc::kill with the child process ID is safe. Leave the surrounding
platform-specific termination and wait behavior unchanged.

Source: Coding guidelines

Comment thread tests/aof_auto_rewrite.rs
Comment on lines +350 to +354
assert_eq!(
max_base_seq(&dir),
1,
"auto-aof-rewrite-percentage 0 must disable automatic rewrites"
);

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

Assert <= 1 instead of == 1.

The test intent is "no rewrite happened". max_base_seq returns 0 when it finds no moon.aof.<seq>.base.rdb file at all. If a fresh multi-shard boot has not yet materialized a seq-1 base file, this assertion fails even though automatic rewrites were correctly disabled.

💚 Proposed fix
     assert_eq!(
         max_base_seq(&dir),
         1,
         "auto-aof-rewrite-percentage 0 must disable automatic rewrites"
     );
+    assert!(
+        max_base_seq(&dir) <= 1,
+        "auto-aof-rewrite-percentage 0 must disable automatic rewrites; \
+         found a compacted base with seq {}",
+        max_base_seq(&dir)
+    );
🤖 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/aof_auto_rewrite.rs` around lines 350 - 354, Update the max_base_seq
assertion in the auto-aof-rewrite-percentage 0 test to accept any value less
than or equal to 1, preserving the intended no-rewrite behavior while allowing
fresh multi-shard boots that have not created a seq-1 base file.

…iterals

Three integration tests (workspace_integration, mq_integration,
txn_kv_wiring) construct ServerConfig as exhaustive struct literals
without a ..default spread; the two new #433 fields broke their
compile. CI's tokio `cargo test --no-run` caught it (the local gate
ran lib tests only for tokio). All test targets now compile under both
default and runtime-tokio,jemalloc feature sets.

Refs #433
author: Tin Dang
@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (3) 📜 Skill insights (0)

Grey Divider


Action required

1. sigkill() unsafe missing SAFETY 📘 Rule violation ≡ Correctness
Description
tests/aof_auto_rewrite.rs introduces an unsafe block calling libc::kill without the required
adjacent // SAFETY: comment. This violates the repository unsafe policy and increases audit risk
around unsafe usage.
Code

tests/aof_auto_rewrite.rs[R99-102]

+fn sigkill(child: &mut Child) {
+    #[cfg(unix)]
+    unsafe {
+        libc::kill(child.id() as i32, libc::SIGKILL);
Relevance

●●● Strong

Repo consistently accepts adding required // SAFETY annotations for unsafe blocks in tests.

PR-#424

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
UNSAFE_POLICY.md mandates that every unsafe block must include a nearby // SAFETY: comment.
The new unsafe { libc::kill(...) } block in tests/aof_auto_rewrite.rs has no such comment, so it
violates the unsafe policy requirements.

Rule 297369: Enforce unsafe code usage against UNSAFE_POLICY.md
tests/aof_auto_rewrite.rs[99-103]
UNSAFE_POLICY.md[14-26]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new `unsafe` block was added without an adjacent `// SAFETY:` comment, violating `UNSAFE_POLICY.md`.

## Issue Context
`UNSAFE_POLICY.md` requires every `unsafe` block to have a `// SAFETY:` comment describing the upheld preconditions and why UB is avoided. In this case, the unsafe can likely be removed entirely by using the existing safe `Child::kill()` approach (or reusing the safe helper in `tests/common`).

## Fix Focus Areas
- tests/aof_auto_rewrite.rs[99-107]
- UNSAFE_POLICY.md[14-26]
- tests/common/mod.rs[168-175]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Premature base size rebase 🐞 Bug ≡ Correctness
Description
auto_rewrite::monitor_loop calls record_base_size() and clears saw_in_progress after a bounded
300s wait even if AOF_REWRITE_IN_PROGRESS is still true. This can prevent later rewrite completion
detection and leave aof_base_size/auto-trigger math incorrect, suppressing or misfiring subsequent
automatic rewrites.
Code

src/persistence/aof/auto_rewrite.rs[R225-228]

+                    std::thread::sleep(std::time::Duration::from_millis(100));
+                }
+                record_base_size();
+                saw_in_progress = false;
Relevance

●●● Strong

Clear logic bug: rebasing while rewrite still in progress can break trigger/completion detection;
likely fixed.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The monitor’s normal completion detection relies on saw_in_progress && !in_progress, but the
bounded-wait success path unconditionally rebases and resets saw_in_progress after waiting,
regardless of whether the rewrite finished. If the rewrite exceeds the deadline, this loses the edge
needed to detect completion later and records a baseline from an in-progress filesystem state.

src/persistence/aof/auto_rewrite.rs[175-186]
src/persistence/aof/auto_rewrite.rs[221-229]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The auto-rewrite monitor rebases `AOF_BASE_SIZE` even when a rewrite hasn’t actually finished (rewrite still in progress after the 300s deadline), and it also clears `saw_in_progress`. This breaks the monitor’s state machine: it can miss the eventual `in_progress: true -> false` transition and permanently keep an incorrect baseline.

### Issue Context
- The monitor’s completion detection depends on `saw_in_progress && !in_progress`.
- The timeout path should not be treated as completion.

### Fix Focus Areas
- src/persistence/aof/auto_rewrite.rs[175-229]

### Suggested fix
- After the bounded wait, **check** `AOF_REWRITE_IN_PROGRESS` again:
 - If it is still `true`, **do not** call `record_base_size()` and **do not** set `saw_in_progress = false`; optionally log a warning that the rewrite exceeded the wait bound.
 - Only call `record_base_size()` (and reset monitor state) once `AOF_REWRITE_IN_PROGRESS` is observed `false` (either via the normal tick-based `completed_since_last_tick` path, or immediately after the wait loop if it actually completed within the deadline).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Double-counts AOF layouts 🐞 Bug ≡ Correctness
Description
measure_total_size() sums both appendonlydir/ contents and the legacy appendonly.aof file
whenever they exist, instead of measuring only the active persistence layout. In the supported tokio
--shards 1 path where a multi-part manifest may exist but legacy recovery is used, this inflates
aof_base_size/aof_current_size and can delay or prevent auto-rewrite for the active legacy AOF.
Code

src/persistence/aof/auto_rewrite.rs[R69-72]

+    let _ = AOF_DIR.set(persistence_dir.join("appendonlydir"));
+    let _ = AOF_LEGACY_FILE.set(persistence_dir.join(appendfilename));
+    AOF_ENABLED.store(true, Ordering::Relaxed);
+    record_base_size();
Relevance

●● Moderate

No close precedent; change contradicts current comment “they never coexist”, so acceptance
uncertain.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
auto_rewrite::init registers both appendonlydir/ and the legacy file and measure_total_size()
sums both. Meanwhile main.rs explicitly supports running tokio --shards 1 with an existing
multi-part manifest (it warns that multi-part isn’t loaded and legacy appendonly.aof is active),
which means both locations can coexist and only one is active. In that case, the monitor’s total
size becomes inflated by stale multi-part data.

src/persistence/aof/auto_rewrite.rs[58-72]
src/persistence/aof/auto_rewrite.rs[75-101]
src/main.rs[1472-1485]
PR-#63

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The auto-rewrite monitor’s size sampler counts both:
1) the manifest directory (`<dir>/appendonlydir/**`), and
2) the legacy single file (`<dir>/<appendfilename>`),
whenever they exist.

But the codebase explicitly supports scenarios where a multi-part manifest exists on disk while the active runtime/layout uses legacy `appendonly.aof` (tokio + `--shards 1`). In that case, the sampler’s total size (and thus `aof_base_size` and trigger inputs) includes stale/unreferenced multi-part files, making the trigger math wrong for the actively-written file.

### Issue Context
- `main.rs` warns that tokio `--shards 1` will not replay multi-part even if the manifest exists.
- `auto_rewrite` currently assumes the two formats “never coexist” and adds both sizes.

### Fix Focus Areas
- src/persistence/aof/auto_rewrite.rs[58-101]
- src/main.rs[1745-1767]

### Suggested fix
Implement layout-aware measurement:
- Decide *once at init* which storage is active (e.g., `ActiveAofLayout::{LegacyFile, ManifestDir}`), and store it in a static.
 - For tokio `--shards 1`, choose `LegacyFile`.
 - For PerShard pools (and monoio TopLevel multi-part), choose `ManifestDir`.
- Update `measure_total_size()` to measure **only** the chosen layout.

Optional hardening (if you want exactness):
- When measuring `ManifestDir`, load the manifest and sum only files referenced by the current committed seq/layout (exclude old generations / temp files), so base/current align with what rewrite/recovery actually uses.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. Unit tests in auto_rewrite.rs 📘 Rule violation ▣ Testability
Description
src/persistence/aof/auto_rewrite.rs defines a #[cfg(test)] mod tests, but the compliance
checklist requires unit tests for split modules to live in the module’s mod.rs. Keeping tests in
leaf submodules makes split-module test organization inconsistent and harder to audit.
Code

src/persistence/aof/auto_rewrite.rs[R234-237]

+#[cfg(test)]
+mod tests {
+    use super::should_trigger;
+
Relevance

● Weak

Prior similar request to move leaf-module tests into mod.rs was explicitly rejected by team.

PR-#211

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist rule requires split-module unit tests to be placed in mod.rs. The AOF module is a
split directory module (src/persistence/aof/mod.rs declares pub mod auto_rewrite;), but
auto_rewrite.rs introduces a #[cfg(test)] mod tests, violating the rule.

Rule 302093: Keep test code for split Rust modules in mod.rs
src/persistence/aof/mod.rs[487-493]
src/persistence/aof/auto_rewrite.rs[234-242]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`src/persistence/aof/auto_rewrite.rs` contains a `#[cfg(test)] mod tests` block, but for split directory modules, unit tests must be centralized in the module’s `mod.rs`.

## Issue Context
`src/persistence/aof/` is a directory module with a `mod.rs` that declares submodules (including `auto_rewrite`). The policy requires that unit tests for such split modules live in `mod.rs`, not in leaf submodule files.

## Fix Focus Areas
- src/persistence/aof/auto_rewrite.rs[234-275]
- src/persistence/aof/mod.rs[487-506]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. find_moon_binary() used in test 📘 Rule violation ▣ Testability
Description
The new integration test spawns the server using common::find_moon_binary() rather than requiring
MOON_BIN to be set. This violates the compliance requirement that integration tests must
explicitly set MOON_BIN and must not rely on fallback binary resolution.
Code

tests/aof_auto_rewrite.rs[R65-68]

+    args.extend_from_slice(extra);
+    let mut cmd = Command::new(common::find_moon_binary());
+    cmd.args(&args).arg("--dir").arg(dir);
+    cmd.stdout(std::fs::File::create(dir.join("moon.stdout.log")).expect("stdout log"))
Relevance

● Weak

Multiple prior reviews rejected enforcing MOON_BIN-only; team keeps find_moon_binary fallback in
tests.

PR-#427
PR-#421

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist explicitly disallows using find_moon_binary()-style helpers that can fall back when
MOON_BIN is unset. The new test calls common::find_moon_binary(), and that helper clearly
documents and implements fallback behavior beyond MOON_BIN.

Rule 992389: Integration tests must set MOON_BIN explicitly for server binaries
tests/aof_auto_rewrite.rs[65-71]
tests/common/mod.rs[133-165]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`tests/aof_auto_rewrite.rs` uses `common::find_moon_binary()` to locate the server binary instead of failing fast unless `MOON_BIN` is explicitly set.

## Issue Context
The compliance rule requires integration tests that spawn a moon server to require `MOON_BIN` and avoid helpers that fall back to `target/{release,debug}/moon` or other implicit paths.

## Fix Focus Areas
- tests/aof_auto_rewrite.rs[50-72]
- tests/common/mod.rs[133-166]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 55 rules

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread tests/aof_auto_rewrite.rs
Comment on lines +99 to +102
fn sigkill(child: &mut Child) {
#[cfg(unix)]
unsafe {
libc::kill(child.id() as i32, libc::SIGKILL);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. sigkill() unsafe missing safety 📘 Rule violation ≡ Correctness

tests/aof_auto_rewrite.rs introduces an unsafe block calling libc::kill without the required
adjacent // SAFETY: comment. This violates the repository unsafe policy and increases audit risk
around unsafe usage.
Agent Prompt
## Issue description
A new `unsafe` block was added without an adjacent `// SAFETY:` comment, violating `UNSAFE_POLICY.md`.

## Issue Context
`UNSAFE_POLICY.md` requires every `unsafe` block to have a `// SAFETY:` comment describing the upheld preconditions and why UB is avoided. In this case, the unsafe can likely be removed entirely by using the existing safe `Child::kill()` approach (or reusing the safe helper in `tests/common`).

## Fix Focus Areas
- tests/aof_auto_rewrite.rs[99-107]
- UNSAFE_POLICY.md[14-26]
- tests/common/mod.rs[168-175]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +225 to +228
std::thread::sleep(std::time::Duration::from_millis(100));
}
record_base_size();
saw_in_progress = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Premature base size rebase 🐞 Bug ≡ Correctness

auto_rewrite::monitor_loop calls record_base_size() and clears saw_in_progress after a bounded
300s wait even if AOF_REWRITE_IN_PROGRESS is still true. This can prevent later rewrite completion
detection and leave aof_base_size/auto-trigger math incorrect, suppressing or misfiring subsequent
automatic rewrites.
Agent Prompt
### Issue description
The auto-rewrite monitor rebases `AOF_BASE_SIZE` even when a rewrite hasn’t actually finished (rewrite still in progress after the 300s deadline), and it also clears `saw_in_progress`. This breaks the monitor’s state machine: it can miss the eventual `in_progress: true -> false` transition and permanently keep an incorrect baseline.

### Issue Context
- The monitor’s completion detection depends on `saw_in_progress && !in_progress`.
- The timeout path should not be treated as completion.

### Fix Focus Areas
- src/persistence/aof/auto_rewrite.rs[175-229]

### Suggested fix
- After the bounded wait, **check** `AOF_REWRITE_IN_PROGRESS` again:
  - If it is still `true`, **do not** call `record_base_size()` and **do not** set `saw_in_progress = false`; optionally log a warning that the rewrite exceeded the wait bound.
  - Only call `record_base_size()` (and reset monitor state) once `AOF_REWRITE_IN_PROGRESS` is observed `false` (either via the normal tick-based `completed_since_last_tick` path, or immediately after the wait loop if it actually completed within the deadline).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +69 to +72
let _ = AOF_DIR.set(persistence_dir.join("appendonlydir"));
let _ = AOF_LEGACY_FILE.set(persistence_dir.join(appendfilename));
AOF_ENABLED.store(true, Ordering::Relaxed);
record_base_size();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

5. Double-counts aof layouts 🐞 Bug ≡ Correctness

measure_total_size() sums both appendonlydir/ contents and the legacy appendonly.aof file
whenever they exist, instead of measuring only the active persistence layout. In the supported tokio
--shards 1 path where a multi-part manifest may exist but legacy recovery is used, this inflates
aof_base_size/aof_current_size and can delay or prevent auto-rewrite for the active legacy AOF.
Agent Prompt
### Issue description
The auto-rewrite monitor’s size sampler counts both:
1) the manifest directory (`<dir>/appendonlydir/**`), and
2) the legacy single file (`<dir>/<appendfilename>`),
whenever they exist.

But the codebase explicitly supports scenarios where a multi-part manifest exists on disk while the active runtime/layout uses legacy `appendonly.aof` (tokio + `--shards 1`). In that case, the sampler’s total size (and thus `aof_base_size` and trigger inputs) includes stale/unreferenced multi-part files, making the trigger math wrong for the actively-written file.

### Issue Context
- `main.rs` warns that tokio `--shards 1` will not replay multi-part even if the manifest exists.
- `auto_rewrite` currently assumes the two formats “never coexist” and adds both sizes.

### Fix Focus Areas
- src/persistence/aof/auto_rewrite.rs[58-101]
- src/main.rs[1745-1767]

### Suggested fix
Implement layout-aware measurement:
- Decide *once at init* which storage is active (e.g., `ActiveAofLayout::{LegacyFile, ManifestDir}`), and store it in a static.
  - For tokio `--shards 1`, choose `LegacyFile`.
  - For PerShard pools (and monoio TopLevel multi-part), choose `ManifestDir`.
- Update `measure_total_size()` to measure **only** the chosen layout.

Optional hardening (if you want exactness):
- When measuring `ManifestDir`, load the manifest and sum only files referenced by the current committed seq/layout (exclude old generations / temp files), so base/current align with what rewrite/recovery actually uses.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@TinDang97
TinDang97 merged commit c0ba6fb into main Aug 7, 2026
12 checks passed
@TinDang97
TinDang97 deleted the fix/433-aof-auto-rewrite branch August 7, 2026 06:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant