Skip to content

perf(persistence): eliminate AOF+WAL KV double-write + fail-loud everysec backpressure - #211

Merged
pilotspacex-byte merged 7 commits into
mainfrom
perf/wal-aof-write-path
Jul 4, 2026
Merged

perf(persistence): eliminate AOF+WAL KV double-write + fail-loud everysec backpressure#211
pilotspacex-byte merged 7 commits into
mainfrom
perf/wal-aof-write-path

Conversation

@pilotspacex-byte

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

Copy link
Copy Markdown
Contributor

Summary

Two write-path durability fixes from the 2026-07-04 WAL/AOF investigation (tmp/WAL-AOF-REVIEW.md, built on the byte-level attribution in tmp/WRITE-DIAG.md), plus the GCE A/B harness that proves them.

1. AOF+WAL double-write eliminated — --wal-kv-log auto|on|off (default auto)

At --appendonly yes, every SPSC-executed KV write was logged to both the per-shard AOF and the per-shard WAL, yet Phase-B recovery (main.rs) wipes WAL-replayed state and replays the AOF — the WAL copy was pure write amplification. The gate lives at the single KV→WAL choke point (wal_append_and_fanout); auto skips KV records while the AOF is the recovery authority and no CDC subscriber is attached (re-engages dynamically on attach), on restores the old behavior (PITR / full CDC history), off never logs. FPI/checkpoint/feature records are untouched.

2. everysec/no appends no longer silently dropped under backpressure

A full AOF writer channel used to warn! + drop the record while the client still received +OK — client-acked write loss, and AOF/memory divergence that corrupts replay of dependent commands (e.g. INCR replayed without its SET). Now:

  • async handler paths (try_send_append_durable): await enqueue bounded by --aof-fsync-timeout-ms, surface ChannelFull/WriteFailed as an error frame;
  • sync paths (SPSC drain, monoio inline SET): 5 ms bounded blocking send (AOF_SPSC_BACKPRESSURE_BOUND) before a counted, error!-logged last-resort drop. Fast path is the same try_send — zero cost until the channel is full. Watch aof_backpressure_dropped in INFO.

GCE proof (c3-standard-4, pd-ssd, steal=0, same-instance A/B, 3 fresh-server reps)

SET 500K × 64B × c50, --appendonly yes everysec. Raw: tmp/wal-ab-c3-standard-4.txt.

shards=4 on (old) auto (fix) delta
WAL KV bytes 47,588,553 256 (headers) eliminated
device write bytes 107.3 MB 59.7 MB −44.4%
SET P64 median 621,158 684,975 +10.3% (all reps ordered)
SET P1 median 89,461 90,041 parity
kill -9 recovery 393,163/393,163 keys + marker lossless from AOF alone
backpressure drops 0 0 fail-loud path never engages on healthy disk

shards=1: byte-identical volumes and throughput parity in both modes (gate is structurally a no-op there).

Bonus measurement: WAL-only posture (--appendonly no, same instance type)

config WAL bytes P1 med P64 med kill-9 recovery
s1 walonly 64 (headers) 132,310 651,083 n/a — nothing logged at shards=1
s4 walonly 47.6 MB 83,195 1,152,148 79.2% (311,551/393,394)

WAL-only is fast (P64 +68% over AOF-only) but not durable: conn-local writes never reach the WAL, so recovery loses ~1/num_shards of writes at shards≥2 and everything at shards=1. Documented in docs/guides/persistence.md; confirms the AOF as the durability log and the WAL KV stream as CDC/PITR/offload infrastructure. (Cross-check: the s4 on run's WAL held 80% of AOF bytes — the same ~1/4 conn-local fraction.)

Tests

  • everysec_full_channel_errs_instead_of_silent_dropred-proven against the old fire-and-forget branch (reverted → fails, restored → passes) + drain-in-time success + sync-path bound/fast-path tests (pool.rs).
  • test_kv_append_skipped_when_wal_kv_log_false — WAL segment stays header-only while the AOF pool still receives the entry; control asserts on logs (spsc_handler.rs).
  • fmt + clippy clean (default & tokio feature sets); full lib suites green on both runtimes; OrbStack CI-parity matrix run before push.
  • Pre-existing, unrelated: memory_doctor_returns_documented_schema fails identically on unmodified main on macOS (DashTable accounting = 0).

Docs

docs/guides/configuration.md (+ "Defaults are tuned for the single-shard case" section), docs/configuration.md, docs/guides/persistence.md ("One KV log at a time"), CHANGELOG [Unreleased].

Follow-ups (tracked in .add/milestones/v3-5-write-path-durability/, not this PR)

  • Coordinator local-leg durable writes through group-commit/fsync_barrier (the 2000 ms always pipeline tail — [HIGH] carry-over from v3-4).
  • Hot-path Bytes::copy_from_slice elimination + single-pass encode in wal_append_and_fanout.
  • WAL-v3 always fsync off the shard event-loop thread.
  • BITOP/COPY/DEL/UNLINK coordinator local-leg durability.

🤖 Generated with Claude Code — investigation, fix, red/green tests, and GCE validation in tmp/WAL-AOF-REVIEW.md.

Two write-path fixes from the 2026-07-04 WAL/AOF investigation
(tmp/WAL-AOF-REVIEW.md, byte-level attribution in tmp/WRITE-DIAG.md):

1. --wal-kv-log auto|on|off (default auto). At --appendonly yes every
   SPSC-executed KV write was logged to BOTH the per-shard AOF and the
   per-shard WAL (2.7x file-byte / 4.1x device amplification at shards=4),
   yet Phase-B recovery wipes WAL-replayed state and replays the AOF.
   `auto` skips WAL KV records while the AOF is the recovery authority
   and no CDC subscriber is attached (re-engages dynamically on attach);
   `on` = pre-0.6 behavior (PITR / full CDC history); `off` = never.
   FPI/checkpoint/feature records unaffected.

2. everysec/no appends are no longer silently dropped on writer
   backpressure. Durable handler paths await enqueue bounded by
   --aof-fsync-timeout-ms and surface ChannelFull/WriteFailed as an
   error frame instead of a false +OK; the sync SPSC-drain and monoio
   inline-SET paths use a 5ms bounded blocking send before a counted,
   error!-logged last-resort drop (AOF_BACKPRESSURE_DROPPED in INFO).

Red/green: everysec_full_channel_errs_instead_of_silent_drop proven RED
against the old fire-and-forget branch; WAL gate pinned by
test_kv_append_skipped_when_wal_kv_log_false (header-only segment +
AOF still receives the entry). fmt + clippy (default & tokio) clean;
lib suites green on both runtimes. GCE A/B harness added
(scripts/gcloud-wal-aof-ab.sh): volume attribution, throughput reps,
kill -9 AOF-alone recovery, backpressure-counter watch.

author: Tin Dang
…ed probes

First GCE run hung after config 1: moon survives SIGTERM/SHUTDOWN
(graceful path lingers), SO_REUSEPORT lets the next rep's server bind
the SAME port alongside the half-dead one, and wait_port's un-bounded
redis-cli ping then blocks forever against the split-traffic pair.

stop_moon(): SIGKILL + pkill pattern backstop + wait-for-port-free;
every redis-cli probe now timeout-bounded; leftover state swept at
measure start. kill -9 is the documented house convention for moon
bench harnesses (CLAUDE.md).

author: Tin Dang
…ed remote driving

gcloud ssh cannot run under the local Monitor sandbox and a full
--measure exceeds foreground tool timeouts; --measure-one <shards>
<walkv> runs a single config (~2-3 min) so the outer driver can chunk
the sweep as short ssh calls. Used for the shipped GCE proof
(tmp/wal-ab-c3-standard-4.txt).

author: Tin Dang
…iterals

The new --wal-kv-log flag added a field to ServerConfig; 16 struct-literal
initializers across 10 tokio-gated integration tests (integration.rs,
txn_kv_wiring.rs, workspace_integration.rs, mq_integration.rs, ...) failed
to compile under the runtime-tokio feature set (E0063). Set "auto" (the
default) everywhere so test behavior is unchanged.

Caught by the OrbStack CI-parity matrix (tokio stage); the monoio suite
never compiles these cfg-gated files, so it stayed green.

author: Tin Dang
@coderabbitai

coderabbitai Bot commented Jul 4, 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: 18 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

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

Run ID: add920d7-1b24-4559-9cc3-089673efb9c8

📥 Commits

Reviewing files that changed from the base of the PR and between a2f5170 and 32167fd.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • docs/guides/persistence.md
  • scripts/gcloud-wal-aof-ab.sh
  • src/config.rs
  • src/persistence/aof/pool.rs
  • src/server/conn/blocking.rs
  • src/shard/spsc_handler.rs
📝 Walkthrough

Walkthrough

Adds a --wal-kv-log configuration mode and threads it through shard write paths to control when KV data is written to WAL versus AOF. Also changes AOF enqueue behavior under backpressure, updates docs and tests, and adds a GCE benchmark harness.

Changes

WAL-KV logging and AOF backpressure

Layer / File(s) Summary
wal_kv_log config field and mode resolution
src/config.rs
Adds wal_kv_log, WalKvLogMode, and mode resolution on ServerConfig.
AOF pool bounded backpressure and enqueue reporting
src/persistence/aof/mod.rs, src/persistence/aof/pool.rs
Adds a backpressure bound, changes append enqueue behavior, and updates EverySec/No durability handling and tests.
Inline SET path uses bounded blocking append
src/server/conn/blocking.rs
Switches the inline SET AOF append path to bounded blocking enqueue.
Shard event loop and spsc handler WAL-KV gating
src/shard/event_loop.rs, src/shard/spsc_handler.rs, src/shard/mod.rs
Resolves WAL-KV mode, threads wal_kv_log through drain and fanout paths, updates WAL/AOF routing, and adjusts tests.
Documentation and changelog updates
CHANGELOG.md, docs/configuration.md, docs/guides/configuration.md, docs/guides/persistence.md
Documents --wal-kv-log, --aof-fsync-timeout-ms, and the related persistence behavior.
gcloud WAL/AOF A/B benchmark script
scripts/gcloud-wal-aof-ab.sh
Adds a VM-based benchmark harness for WAL/AOF write-path measurements.
Test server configs set wal_kv_log=auto
tests/*.rs
Sets wal_kv_log: "auto" across integration and unit test server configurations.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ShardEventLoop
  participant SpscHandler
  participant WalWriter
  participant AofWriterPool

  Client->>ShardEventLoop: write command
  ShardEventLoop->>ShardEventLoop: resolve wal_kv_log_mode()
  ShardEventLoop->>SpscHandler: drain_spsc_shared(..., wal_kv_log)
  alt wal_kv_log enabled
    SpscHandler->>WalWriter: append KV record
  else wal_kv_log disabled
    SpscHandler->>SpscHandler: skip WAL KV append
  end
  SpscHandler->>AofWriterPool: send_append_bounded_blocking(bytes)
  alt enqueued within bound
    AofWriterPool-->>SpscHandler: ok
  else channel full past bound
    AofWriterPool-->>SpscHandler: dropped / timeout
  end
  SpscHandler-->>Client: +OK
Loading

Possibly related PRs

  • pilotspace/moon#100: Both PRs modify WAL append wiring in src/shard/spsc_handler.rs, including SWAPDB/MOVE/COPY handling paths.
  • pilotspace/moon#129: Both PRs touch the AofWriterPool append/enqueue plumbing in the AOF writer code.
  • pilotspace/moon#168: Both PRs modify the shard SPSC drain/write hot path in src/shard/spsc_handler.rs.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title clearly captures the two main persistence fixes: removing AOF+WAL KV double-writes and making everysec backpressure fail loud.
Description check ✅ Passed The description covers the summary, tests, docs, and performance evidence, but it does not explicitly follow the template's Checklist and Notes sections.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/wal-aof-write-path

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

perf(persistence): eliminate AOF+WAL KV double-write + fail-loud everysec backpressure

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Adds --wal-kv-log auto|on|off (default auto) to skip redundant WAL KV records when AOF is the
 crash-recovery authority, eliminating 2.7× file-byte / 4.1× device write amplification at `--shards
 >= 2`.
• Replaces silent fire-and-forget AOF drops under backpressure with bounded-wait paths: async
 handlers await enqueue under --aof-fsync-timeout-ms and return ChannelFull/WriteFailed errors;
 sync SPSC/monoio paths block up to 5 ms before a counted error!-logged last-resort drop.
• Adds WalKvLogMode enum and wal_kv_log_mode() resolver in config.rs; threads `wal_kv_log:
 bool through drain_spsc_shared, handle_shard_message_shared, wal_fanout_has_work`, and
 wal_append_and_fanout.
• Adds new AofWriterPool methods: send_append_bounded_blocking (sync) and
 send_append_backpressure (async); try_send_append now returns bool and counts drops in
 AOF_BACKPRESSURE_DROPPED.
• Adds GCE A/B benchmark harness (scripts/gcloud-wal-aof-ab.sh) proving −44.4% device writes and
 lossless AOF-only recovery at shards=4.
• Adds red/green tests: everysec_full_channel_errs_instead_of_silent_drop,
 everysec_backpressure_succeeds_when_writer_drains_in_time, spsc_bounded_blocking_*, and
 test_kv_append_skipped_when_wal_kv_log_false.
• Updates CHANGELOG.md, docs/configuration.md, docs/guides/configuration.md, and
 docs/guides/persistence.md with new flags and behavior.
Diagram

graph TD
    CLI["--wal-kv-log auto|on|off"] --> CFG["config.rs\nWalKvLogMode"]
    CFG --> EL["event_loop.rs\nper-drain resolution"]
    EL -->|wal_kv_log: bool| SPSC["spsc_handler.rs\ndrain_spsc_shared"]
    SPSC --> FAN["wal_append_and_fanout"]
    FAN -->|wal_kv_log=true| WAL[("WAL v2/v3")]
    FAN -->|always| AOF_POOL["AofWriterPool"]
    AOF_POOL --> BOUND["send_append_bounded_blocking\n5 ms sync bound"]
    AOF_POOL --> ASYNC_BP["send_append_backpressure\nasync, fsync_timeout bound"]
    BOUND --> AOF_FILE[("AOF file")]
    ASYNC_BP --> AOF_FILE
    BLOCK["blocking.rs\ntry_inline_dispatch"] --> BOUND
    ASYNC_BP -->|Err ChannelFull| CLIENT["Client error frame"]

    subgraph Legend
        direction LR
        _db[("Storage")] ~~~ _svc["Component"] ~~~ _ext["External / CLI"]
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. AtomicBool for wal_kv_log instead of per-cycle bool parameter
  • ➕ Avoids threading the bool through every call site
  • ➕ CDC attach/detach can flip the flag without waiting for the next drain cycle
  • ➖ Adds a shared atomic read on every fanout call (minor)
  • ➖ Harder to reason about — the current approach makes the decision explicit and testable at the call site
2. Dedicated overflow queue for AOF backpressure instead of bounded blocking
  • ➕ Zero stall on the shard event loop even under disk pressure
  • ➕ Decouples write acceptance from disk throughput
  • ➖ Adds memory pressure and a second queue to drain/monitor
  • ➖ Complicates ordering guarantees and the 'was this write durable?' answer
  • ➖ More code surface for a path that should never be reached on a healthy disk

Recommendation: The PR's approach — a single bool gate threaded through the existing fanout call chain — is the right choice for minimal diff surface and zero hot-path overhead when the channel is healthy. The main alternative worth considering is a dynamic runtime toggle (e.g., an AtomicBool read at the fanout site) rather than re-evaluating the CDC registry on every drain cycle, but the current approach is correct and the per-cycle cost is negligible. The bounded-blocking approach for sync paths is a deliberate, well-justified trade-off over alternatives like a dedicated overflow queue or dropping to a separate thread.

Files changed (22) +814 / -37

Enhancement (3) +76 / -0
config.rsAdd '--wal-kv-log' flag and 'WalKvLogMode' enum +37/-0

Add '--wal-kv-log' flag and 'WalKvLogMode' enum

• Adds the 'wal_kv_log: String' CLI field to 'ServerConfig' with full doc-comment. Introduces the 'WalKvLogMode { Auto, On, Off }' enum and the 'wal_kv_log_mode()' resolver method that parses the string flag, defaulting unknown values to 'Auto'.

src/config.rs

mod.rsExport 'AOF_SPSC_BACKPRESSURE_BOUND' constant (5 ms) +8/-0

Export 'AOF_SPSC_BACKPRESSURE_BOUND' constant (5 ms)

• Adds the 'AOF_SPSC_BACKPRESSURE_BOUND' public constant (5 ms 'Duration') used by sync callers of 'send_append_bounded_blocking' to cap the worst-case shard event-loop stall.

src/persistence/aof/mod.rs

event_loop.rsResolve 'wal_kv_log_mode' per drain cycle and pass to SPSC drain +31/-0

Resolve 'wal_kv_log_mode' per drain cycle and pass to SPSC drain

• Reads 'wal_kv_log_mode' from 'server_config' at event-loop startup and emits a warning when '--wal-kv-log off' is combined with '--appendonly no'. Passes the resolved 'wal_kv_log: bool' (computed from mode + 'appendonly_enabled' + CDC registry emptiness) to all three 'drain_spsc_shared' call sites.

src/shard/event_loop.rs

Bug fix (3) +464 / -36
pool.rsAdd bounded-backpressure AOF send paths; fix silent drop under everysec +294/-14

Add bounded-backpressure AOF send paths; fix silent drop under everysec

• Refactors 'try_send_append' to return 'bool' and count drops in 'AOF_BACKPRESSURE_DROPPED'. Adds 'send_append_bounded_blocking' (sync, 'send_timeout'-based) for SPSC/monoio callers and 'send_append_backpressure' (async, dual-runtime 'select!'/'timeout') for durable handler paths. Updates 'try_send_append_durable' to await 'send_append_backpressure' under 'EverySec'/'No' instead of fire-and-forget. Adds four new unit tests covering the full-channel error, drain-in-time success, sync bound/fast-path cases.

src/persistence/aof/pool.rs

spsc_handler.rsThread 'wal_kv_log' through SPSC drain and fanout; gate WAL KV writes +159/-21

Thread 'wal_kv_log' through SPSC drain and fanout; gate WAL KV writes

• Adds 'wal_kv_log: bool' parameter to 'drain_spsc_shared', 'handle_shard_message_shared', 'wal_fanout_has_work', and 'wal_append_and_fanout'. Updates 'wal_fanout_has_work' to exclude WAL writers from the 'has work' predicate when 'wal_kv_log' is false. Guards the WAL v2/v3 append in 'wal_append_and_fanout' behind 'if wal_kv_log'. Replaces 'try_send_append' with 'send_append_bounded_blocking' in the AOF fanout step. Updates all existing tests to pass 'true' for legacy behavior; adds 'test_kv_append_skipped_when_wal_kv_log_false'.

src/shard/spsc_handler.rs

blocking.rsReplace fire-and-forget AOF send with bounded-blocking in inline SET path +11/-1

Replace fire-and-forget AOF send with bounded-blocking in inline SET path

• Replaces the 'try_send_append' call in 'try_inline_dispatch' with 'send_append_bounded_blocking' using 'AOF_SPSC_BACKPRESSURE_BOUND', preventing silent client-acked write loss on the monoio inline SET fast path.

src/server/conn/blocking.rs

Tests (11) +19 / -0
mod.rsPass 'wal_kv_log: true' to shard test helpers +2/-0

Pass 'wal_kv_log: true' to shard test helpers

• Adds the new 'wal_kv_log' boolean argument to two in-module test 'drain_spsc_shared' calls, preserving pre-existing legacy behavior in unit tests.

src/shard/mod.rs

integration.rsAdd 'wal_kv_log: "auto"' to all integration test server configs +7/-0

Add 'wal_kv_log: "auto"' to all integration test server configs

• Adds the new required 'wal_kv_log' field (set to '"auto"') to all 'ServerConfig' struct literals in the integration test helpers to keep them compiling after the config struct change.

tests/integration.rs

kill_snapshot.rsAdd 'wal_kv_log: "auto"' to kill-snapshot test config +1/-0

Add 'wal_kv_log: "auto"' to kill-snapshot test config

• Adds the new 'wal_kv_log' field to 'base_config' in the kill-snapshot test suite.

tests/kill_snapshot.rs

replication_test.rsAdd 'wal_kv_log: "auto"' to replication test server config +1/-0

Add 'wal_kv_log: "auto"' to replication test server config

• Adds the new 'wal_kv_log' field to the replication integration test server config.

tests/replication_test.rs

mq_integration.rsAdd 'wal_kv_log: "auto"' to MQ integration test config +1/-0

Add 'wal_kv_log: "auto"' to MQ integration test config

• Adds the new 'wal_kv_log' field to the MQ server config struct literal.

tests/mq_integration.rs

txn_kv_wiring.rsAdd 'wal_kv_log: "auto"' to transaction KV wiring test config +1/-0

Add 'wal_kv_log: "auto"' to transaction KV wiring test config

• Adds the new 'wal_kv_log' field to the transaction server config struct literal.

tests/txn_kv_wiring.rs

vacuum_commands.rsAdd 'wal_kv_log: "auto"' to vacuum commands test config +1/-0

Add 'wal_kv_log: "auto"' to vacuum commands test config

• Adds the new 'wal_kv_log' field to the vacuum test base config.

tests/vacuum_commands.rs

workspace_integration.rsAdd 'wal_kv_log: "auto"' to workspace integration test configs +2/-0

Add 'wal_kv_log: "auto"' to workspace integration test configs

• Adds the new 'wal_kv_log' field to both workspace server config struct literals.

tests/workspace_integration.rs

ft_search_multi_shard_as_of.rsAdd 'wal_kv_log: "auto"' to FT search multi-shard test config +1/-0

Add 'wal_kv_log: "auto"' to FT search multi-shard test config

• Adds the new 'wal_kv_log' field to the FT search multi-shard test config.

tests/ft_search_multi_shard_as_of.rs

ft_search_temporal_parity.rsAdd 'wal_kv_log: "auto"' to FT search temporal parity test config +1/-0

Add 'wal_kv_log: "auto"' to FT search temporal parity test config

• Adds the new 'wal_kv_log' field to the FT search temporal parity test config.

tests/ft_search_temporal_parity.rs

txn_ft_search_snapshot.rsAdd 'wal_kv_log: "auto"' to transaction FT search snapshot test config +1/-0

Add 'wal_kv_log: "auto"' to transaction FT search snapshot test config

• Adds the new 'wal_kv_log' field to the transaction FT search snapshot test config.

tests/txn_ft_search_snapshot.rs

Documentation (4) +45 / -1
CHANGELOG.mdDocument AOF+WAL double-write fix and everysec backpressure fix in [Unreleased] +19/-0

Document AOF+WAL double-write fix and everysec backpressure fix in [Unreleased]

• Adds two entries under '[Unreleased] Fixed': the '--wal-kv-log auto' double-write elimination and the bounded-backpressure everysec/no drop fix with 'aof_backpressure_dropped' counter reference.

CHANGELOG.md

configuration.mdAdd '--wal-kv-log' row and update '--aof-fsync-timeout-ms' description +2/-1

Add '--wal-kv-log' row and update '--aof-fsync-timeout-ms' description

• Adds the '--wal-kv-log auto' row to the persistence configuration table. Expands the '--aof-fsync-timeout-ms' description to cover its new role as a writer-queue backpressure bound under 'everysec'.

docs/configuration.md

configuration.mdAdd 'Defaults are tuned for the single-shard case' guidance section +13/-0

Add 'Defaults are tuned for the single-shard case' guidance section

• Inserts a new prose section explaining single-shard default rationale and the 'one KV record to disk exactly once' guarantee under '--wal-kv-log auto'. Adds '--aof-fsync-timeout-ms' and '--wal-kv-log' rows to the persistence table.

docs/guides/configuration.md

persistence.mdAdd 'One KV log at a time' section explaining '--wal-kv-log' +11/-0

Add 'One KV log at a time' section explaining '--wal-kv-log'

• Adds a new subsection under the WAL documentation explaining why '--wal-kv-log auto' skips the WAL KV copy when AOF is the recovery authority, and when to set '--wal-kv-log on' for PITR/CDC.

docs/guides/persistence.md

Other (1) +210 / -0
gcloud-wal-aof-ab.shAdd GCE A/B benchmark harness for WAL/AOF write-path fixes +210/-0

Add GCE A/B benchmark harness for WAL/AOF write-path fixes

• New 210-line bash script that provisions a GCE instance, builds the local source snapshot, and runs a same-instance A/B comparison across '--wal-kv-log on' vs 'auto' at 'shards={1,4}'. Measures on-disk byte volumes (AOF vs WAL), '/proc/<pid>/io' device writes, SET throughput (P1/P64, 3 reps), kill-9 recovery correctness, and 'aof_backpressure_dropped' counter.

scripts/gcloud-wal-aof-ab.sh

New config `walonly` = --appendonly no with default disk-offload (enable),
making WAL-v3 the sole KV log (wal-kv-log auto keeps logging when there is
no AOF to defer to). Measures the WAL-only performance posture and extends
the shards=4 kill-9 recovery check to WAL-alone recovery — sizing the
conn-local-write gap (local-path writes never enter the SPSC drain and are
not WAL-logged, so WAL-only recovery is expected to be incomplete).

author: Tin Dang
@qodo-code-review

qodo-code-review Bot commented Jul 4, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Context used
✅ Compliance rules (platform): 40 rules

Grey Divider


Action required

1. SPSC path ignores AOF loss ✓ Resolved 🐞 Bug ≡ Correctness
Description
wal_append_and_fanout and the monoio inline write fast path (try_inline_dispatch) route write
commands to the per-shard AOF via send_append_bounded_blocking but discard its boolean result, so
a timed-out/disconnected enqueue can still return +OK/the original success frame to the caller.
This acknowledges in-memory mutations without a corresponding AOF entry, reintroducing acknowledged
write loss under backpressure and risking replay divergence.
Code

src/shard/spsc_handler.rs[R2544-2560]

+    // Bounded-blocking (`send_append_bounded_blocking`) because this function
+    // is sync and cannot await the fsync rendezvous: the fast path is the same
+    // try_send as before; only when the writer channel is FULL (writer >10k
+    // appends behind) does the shard thread block up to the bound instead of
+    // silently losing a record the client already got `+OK` for. The
+    // `appendfsync=always` ack is handled by the async connection handler
+    // (handler_sharded / handler_single). LSN=0 is safe here: per-shard order
+    // is preserved by write order; the LSN is only meaningful for cross-shard
+    // TXN merge (RFC step 5, not yet wired).
    if let Some(pool) = aof_pool {
-        pool.try_send_append(shard_id, 0, bytes::Bytes::copy_from_slice(data));
+        pool.send_append_bounded_blocking(
+            shard_id,
+            0,
+            bytes::Bytes::copy_from_slice(data),
+            crate::persistence::aof::AOF_SPSC_BACKPRESSURE_BOUND,
+        );
    }
Relevance

⭐⭐⭐ High

Team often fixes “silent success on failed send/recv” by propagating errors instead of defaults (PR
#17).

PR-#17

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In the synchronous SPSC write handling flow, wal_append_and_fanout is invoked before the response
is queued/constructed, yet it ignores the bool returned by
AofWriterPool::send_append_bounded_blocking, even though that API returns false precisely when
the append was not enqueued (timeout/disconnect). Similarly, try_inline_dispatch performs the same
bounded append for the inline SET AOF path but does not check the returned boolean and
unconditionally appends +OK, meaning both paths can acknowledge success despite the AOF writer
never receiving the record.

src/shard/spsc_handler.rs[723-747]
src/shard/spsc_handler.rs[2483-2561]
src/persistence/aof/pool.rs[458-497]
src/server/conn/blocking.rs[1419-1439]

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

## Issue description
Writes are acknowledged even when their AOF append fails: both the SPSC write path (`wal_append_and_fanout` as used by shard message handling) and the monoio inline SET fast path (`try_inline_dispatch`) call `AofWriterPool::send_append_bounded_blocking()` but ignore its `bool` return, so timeout/disconnect/backpressure can drop the AOF record while the client still receives a success frame (`+OK`/original success). We want to propagate bounded-enqueue failure back into the response so non-durable writes are not acknowledged.

## Issue Context
`send_append_bounded_blocking()` is explicitly designed to return `false` when the append was NOT enqueued (timeout/disconnect). In the SPSC handling, the response frame is constructed after `wal_append_and_fanout` is invoked, so the failure can be surfaced by overwriting the response frame to an error; for inline dispatch, the code currently unconditionally writes `+OK` and should instead emit an error (and ideally stop inlining further commands for that connection/batch) when the enqueue fails.

## Fix Focus Areas
- src/shard/spsc_handler.rs[2483-2561]
- src/shard/spsc_handler.rs[723-747]
- src/server/conn/blocking.rs[1425-1438]

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


2. Unannotated unwrap() in wal_append_tests 📘 Rule violation ✧ Quality
Description
New .unwrap() calls were added in tests in src/shard/spsc_handler.rs and
src/persistence/aof/pool.rs without the required adjacent #[allow(clippy::unwrap_used)] and
immediately preceding justification comment. This violates the unwrap-annotation/audit convention
and can undermine ratcheting expectations or fail lint/audit gates that enforce annotated unwrap
usage.
Code

src/shard/spsc_handler.rs[R2783-2789]

+        let tmp = tempfile::tempdir().unwrap();
+        let wal_dir = tmp.path().join("wal");
+        let mut w3 = Some(WalWriterV3::new(0, &wal_dir, 16 * 1024 * 1024).unwrap());
+        w3.as_mut().unwrap().flush_sync().unwrap();
+        let seg = wal_dir.join("000000000001.wal");
+        let base_len = std::fs::metadata(&seg).unwrap().len();
+
Relevance

⭐⭐⭐ High

Repo enforces unwrap-annotation even in tests; similar request was accepted (PR #71).

PR-#71

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In src/shard/spsc_handler.rs, the newly added test test_kv_append_skipped_when_wal_kv_log_false
includes multiple .unwrap() calls (e.g., tempfile::tempdir().unwrap(),
WalWriterV3::new(...).unwrap(), and metadata reads) without an adjacent
#[allow(clippy::unwrap_used)] on the containing scope and without a direct justification comment
immediately above such an allow, which the rule requires. Similarly, in the added tests in
src/persistence/aof/pool.rs, .unwrap() is used (including on operations like try_send(...))
but there is no nearby single-line // ... justification comment immediately above a
#[allow(clippy::unwrap_used)] attribute attached to the relevant function/block, demonstrating the
same policy violation.

Rule 302083: Annotate safe unwrap calls with allow and justification
src/shard/spsc_handler.rs[2783-2813]
src/persistence/aof/pool.rs[1925-1932]

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 PR introduces new `.unwrap()` calls in tests without meeting the project policy that each unwrap must be covered by an immediately attached `#[allow(clippy::unwrap_used)]` on the containing item/scope and an immediately preceding single-line justification comment explaining why the unwrap is safe.

## Issue Context
Policy requires each unwrap to be covered by:
- `// <why unwrap is safe>` directly above
- `#[allow(clippy::unwrap_used)]` directly attached to the containing item/scope (e.g., the test function or a narrower block)

Update the affected tests so every new unwrap is either removed (by handling errors explicitly) or is properly justified and locally allowed per the convention.

## Fix Focus Areas
- src/shard/spsc_handler.rs[2783-2813]
- src/persistence/aof/pool.rs[1925-2006]

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



Remediation recommended

3. wal-kv-log typo fallback ✓ Resolved 🐞 Bug ☼ Reliability
Description
ServerConfig::wal_kv_log_mode maps any unrecognized --wal-kv-log string to Auto, so typos
silently change whether KV commands are logged to the WAL. This can unexpectedly disable WAL KV
history needed for PITR/CDC without any validation or warning.
Code

src/config.rs[R763-771]

+    /// Resolve `--wal-kv-log` into its mode. Unknown values fall back to
+    /// `Auto` (matches the string-flag convention used by `--wal-fpi` etc.).
+    pub fn wal_kv_log_mode(&self) -> WalKvLogMode {
+        match self.wal_kv_log.as_str() {
+            "on" => WalKvLogMode::On,
+            "off" => WalKvLogMode::Off,
+            _ => WalKvLogMode::Auto,
+        }
+    }
Relevance

⭐⭐ Medium

Mixed: strict unknown-token validation accepted in commands (PR #66) but clap flag validators
rejected (PR #157).

PR-#66
PR-#157

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The config field is a String and the resolver explicitly maps all unknown strings to Auto,
meaning clap will accept arbitrary values and the server will proceed without alerting the operator.

src/config.rs[339-351]
src/config.rs[763-771]

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

## Issue description
`--wal-kv-log` is stored as a free-form `String` and `wal_kv_log_mode()` falls back to `Auto` for any unknown value. This silently accepts operator typos and can change durability/CDC behavior without notice.

## Issue Context
This flag controls whether KV commands are logged into the WAL, which affects PITR/CDC expectations. Silent fallback makes misconfiguration hard to detect.

## Fix Focus Areas
- src/config.rs[339-351]
- src/config.rs[763-771]

## Suggested fix
- Use a clap-enforced enum (e.g., `#[derive(clap::ValueEnum)] enum WalKvLogMode { Auto, On, Off }`) and store the enum directly in `ServerConfig`.
 - Or, add a `value_parser` restricting values to `{auto,on,off}`.
- If you intentionally want permissive parsing, at least emit a `warn!` once on unknown values and include the provided string.

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



Informational

4. Tests added outside mod.rs 📘 Rule violation ▣ Testability
Description
New unit tests were added in src/persistence/aof/pool.rs, but this module is part of a split
directory-module (src/persistence/aof/mod.rs). This violates the requirement that tests for split
modules live only in mod.rs, which helps keep module-level test organization consistent.
Code

src/persistence/aof/pool.rs[R1916-1923]

+    /// RED before the fix: `try_send_append_durable` under EverySec returned
+    /// `Ok(())` while the append was silently dropped. Now the caller awaits
+    /// enqueue under `fsync_timeout` and gets `Err(AofAck::ChannelFull)` when
+    /// the writer stays full past the bound — so it returns an error frame
+    /// instead of a false `+OK`.
+    #[cfg(feature = "runtime-tokio")]
+    #[tokio::test]
+    async fn everysec_full_channel_errs_instead_of_silent_drop() {
Relevance

⭐ Low

Same “tests must be in mod.rs for split modules” suggestion was rejected before (GEO split-module
tests, PR #68).

PR-#68

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
src/persistence/aof/mod.rs declares a split module structure including mod pool;, while the PR
adds new #[tokio::test] tests inside the leaf implementation file src/persistence/aof/pool.rs,
which violates the split-module test location requirement.

Rule 302093: Keep test code for split Rust modules in mod.rs
src/persistence/aof/mod.rs[477-497]
src/persistence/aof/pool.rs[1916-2050]

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/` is a split module (directory `mod.rs` + submodules), but new tests were added to the leaf file `pool.rs`. Policy requires unit tests for split modules to live in the directory `mod.rs` only.

## Issue Context
Keeping tests centralized in `mod.rs` avoids scattering unit tests across sibling implementation files and simplifies review/maintenance.

## Fix Focus Areas
- src/persistence/aof/pool.rs[1916-2050]
- src/persistence/aof/mod.rs[477-497]

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


Grey Divider

Qodo Logo

Comment thread src/shard/spsc_handler.rs
Comment on lines +2783 to +2789
let tmp = tempfile::tempdir().unwrap();
let wal_dir = tmp.path().join("wal");
let mut w3 = Some(WalWriterV3::new(0, &wal_dir, 16 * 1024 * 1024).unwrap());
w3.as_mut().unwrap().flush_sync().unwrap();
let seg = wal_dir.join("000000000001.wal");
let base_len = std::fs::metadata(&seg).unwrap().len();

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

2. Unannotated unwrap() in wal_append_tests 📘 Rule violation ✧ Quality

New .unwrap() calls were added in tests in src/shard/spsc_handler.rs and
src/persistence/aof/pool.rs without the required adjacent #[allow(clippy::unwrap_used)] and
immediately preceding justification comment. This violates the unwrap-annotation/audit convention
and can undermine ratcheting expectations or fail lint/audit gates that enforce annotated unwrap
usage.
Agent Prompt
## Issue description
The PR introduces new `.unwrap()` calls in tests without meeting the project policy that each unwrap must be covered by an immediately attached `#[allow(clippy::unwrap_used)]` on the containing item/scope and an immediately preceding single-line justification comment explaining why the unwrap is safe.

## Issue Context
Policy requires each unwrap to be covered by:
- `// <why unwrap is safe>` directly above
- `#[allow(clippy::unwrap_used)]` directly attached to the containing item/scope (e.g., the test function or a narrower block)

Update the affected tests so every new unwrap is either removed (by handling errors explicitly) or is properly justified and locally allowed per the convention.

## Fix Focus Areas
- src/shard/spsc_handler.rs[2783-2813]
- src/persistence/aof/pool.rs[1925-2006]

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

Comment thread src/shard/spsc_handler.rs
Comment thread src/config.rs Outdated

@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: 3

Caution

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

⚠️ Outside diff range comments (1)
src/persistence/aof/pool.rs (1)

237-248: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale doc contradicts the new EverySec backpressure behavior.

The surrounding doc still describes EverySec/No as fire-and-forget: line 235 says it "stays on the fire-and-forget path (zero new latency)" and lines 244-248 say "The non-Always branch resolves immediately (no actual suspension)". With the new send_append_backpressure path, a full writer channel now suspends up to fsync_timeout and can return Err(AofAck::ChannelFull). The ~5 ns claim only holds for the fast (non-full) path. Please reconcile lines 235 and 244-248 with the new bounded-await semantics you added at 237-242.

🤖 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/pool.rs` around lines 237 - 248, The doc comments around
the Aof append/ack path are now stale and contradict the new bounded-await
behavior. Update the comments on the `send_append_backpressure`-based flow and
the surrounding `AofAck` handling to say EverySec/No can block up to
`fsync_timeout` and may return `Err(AofAck::ChannelFull)` when the writer
channel is full, rather than describing it as fire-and-forget or zero-latency.
Also qualify the `~5 ns`/“no actual suspension” claim so it only applies to the
fast non-full path, and keep the Always-path failure semantics accurate in the
same doc block.
🧹 Nitpick comments (1)
src/shard/event_loop.rs (1)

1180-1186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the triplicated wal_kv_log_mode match into a helper.

The same 6-line match resolving the log-enable boolean is duplicated verbatim at three call sites (tokio notify wake, tokio periodic tick, monoio every-wake). A small free function keeps the three sites in sync and avoids drift if the Auto heuristic changes later.

♻️ Proposed refactor
+fn wal_kv_log_enabled(
+    mode: crate::config::WalKvLogMode,
+    appendonly_enabled: bool,
+    cdc_registry: &crate::cdc::CdcSubscriberRegistry,
+) -> bool {
+    match mode {
+        crate::config::WalKvLogMode::On => true,
+        crate::config::WalKvLogMode::Off => false,
+        crate::config::WalKvLogMode::Auto => !appendonly_enabled || !cdc_registry.is_empty(),
+    }
+}

And at each call site:

-                        match wal_kv_log_mode {
-                            crate::config::WalKvLogMode::On => true,
-                            crate::config::WalKvLogMode::Off => false,
-                            crate::config::WalKvLogMode::Auto => {
-                                !appendonly_enabled || !cdc_registry.is_empty()
-                            }
-                        },
+                        wal_kv_log_enabled(wal_kv_log_mode, appendonly_enabled, &cdc_registry),

Also applies to: 1282-1288, 1860-1866

🤖 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/event_loop.rs` around lines 1180 - 1186, Extract the repeated
wal_kv_log_mode boolean resolution into a shared helper function and call it
from all three sites (tokio notify wake, tokio periodic tick, monoio
every-wake). Move the existing WalKvLogMode::On/Off/Auto match logic into a
small free function or similarly named helper near event_loop, and replace each
duplicated match with that helper so the Auto heuristic lives in one place and
stays consistent.
🤖 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 `@scripts/gcloud-wal-aof-ab.sh`:
- Around line 205-210: `--measure-one` in the main subcommand dispatch can hit
an unbound-variable crash before `die` runs because `measure_single` is called
with `$2` and `$3` unconditionally. Update the argument handling around the
`case` in `gcloud-wal-aof-ab.sh` to validate that `--measure-one` has both
required parameters before referencing them, and route missing-arg cases to
`die` with a usage message; use the `measure_single` and `die` symbols to keep
the fix localized.

In `@src/persistence/aof/pool.rs`:
- Around line 528-546: The timeout handling around send_fut in the AOF pool
write path can misreport a successful append as ChannelFull because
flume::Sender::send_async is not cancel-safe. Update the outcome logic in the
AofAck write/backpressure branch to use a cancel-safe strategy instead of
directly timing out send_fut, and make sure the AOF_BACKPRESSURE_DROPPED/client
response only reflects writes that are truly not delivered.

In `@src/shard/spsc_handler.rs`:
- Around line 2553-2559: The batched AOF append path in the shard handler resets
backpressure for every write, which can block the event loop for N times the
bounded wait. Update the `MultiExecute` and `PipelineBatch(Slotted)` flow in
`spsc_handler` to track a single remaining deadline or budget across the whole
batch, and pass the reduced timeout into each `send_append_bounded_blocking`
call instead of always using the full `AOF_SPSC_BACKPRESSURE_BOUND`. Keep the
change localized to the batch-processing logic around `aof_pool` so unrelated
connections are not stalled by large batches.

---

Outside diff comments:
In `@src/persistence/aof/pool.rs`:
- Around line 237-248: The doc comments around the Aof append/ack path are now
stale and contradict the new bounded-await behavior. Update the comments on the
`send_append_backpressure`-based flow and the surrounding `AofAck` handling to
say EverySec/No can block up to `fsync_timeout` and may return
`Err(AofAck::ChannelFull)` when the writer channel is full, rather than
describing it as fire-and-forget or zero-latency. Also qualify the `~5 ns`/“no
actual suspension” claim so it only applies to the fast non-full path, and keep
the Always-path failure semantics accurate in the same doc block.

---

Nitpick comments:
In `@src/shard/event_loop.rs`:
- Around line 1180-1186: Extract the repeated wal_kv_log_mode boolean resolution
into a shared helper function and call it from all three sites (tokio notify
wake, tokio periodic tick, monoio every-wake). Move the existing
WalKvLogMode::On/Off/Auto match logic into a small free function or similarly
named helper near event_loop, and replace each duplicated match with that helper
so the Auto heuristic lives in one place and stays consistent.
🪄 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: 10d967fa-da60-4e32-8d3c-278cac006653

📥 Commits

Reviewing files that changed from the base of the PR and between 2bbc2dc and 80af5eb.

📒 Files selected for processing (22)
  • CHANGELOG.md
  • docs/configuration.md
  • docs/guides/configuration.md
  • docs/guides/persistence.md
  • scripts/gcloud-wal-aof-ab.sh
  • src/config.rs
  • src/persistence/aof/mod.rs
  • src/persistence/aof/pool.rs
  • src/server/conn/blocking.rs
  • src/shard/event_loop.rs
  • src/shard/mod.rs
  • src/shard/spsc_handler.rs
  • tests/ft_search_multi_shard_as_of.rs
  • tests/ft_search_temporal_parity.rs
  • tests/integration.rs
  • tests/kill_snapshot.rs
  • tests/mq_integration.rs
  • tests/replication_test.rs
  • tests/txn_ft_search_snapshot.rs
  • tests/txn_kv_wiring.rs
  • tests/vacuum_commands.rs
  • tests/workspace_integration.rs

Comment thread scripts/gcloud-wal-aof-ab.sh
Comment thread src/persistence/aof/pool.rs
Comment thread src/shard/spsc_handler.rs
GCE measurement (walonly posture, c3-standard-4): with --appendonly no only
SPSC-dispatched writes reach the WAL — conn-local writes are logged nowhere.
kill -9 recovery at 4 shards restored 311,551/393,394 keys (79.2% ≈
1 - 1/num_shards); at 1 shard nothing is logged at all. Document that
--appendonly yes remains the durability posture and the WAL KV stream
serves CDC/PITR/disk-offload only.

author: Tin Dang
…shared budget (PR #211 review)

Addresses the four valid PR #211 review findings (qodo + CodeRabbit):

1. AOF-loss no longer acked (qodo Bug): `wal_append_and_fanout` returns
   bool and `send_append_bounded_blocking` failures now replace the
   success frame with `MOONERR AOF backpressure ...` at every SPSC arm
   (Execute/Slotted, MultiExecute/Slotted, PipelineBatch/Slotted,
   MOVE, COPY) and the monoio inline-SET fast path (blocking.rs) —
   previously a timed-out enqueue still returned +OK. SwapDb discards
   the result deliberately (coordinator broadcast, no client frame).

2. Batch-shared backpressure budget (CodeRabbit Major):
   `send_append_bounded_blocking` now takes `&mut Duration` and
   decrements by time actually blocked; MultiExecute/PipelineBatch arms
   share ONE 5ms budget per batch, so sustained backpressure stalls the
   shard thread at most BOUND total, not BOUND x batch-len. Exhausted
   budget fails immediately (new test proves <10ms).

3. --wal-kv-log typo fallback (qodo): clap value_parser rejects unknown
   values at parse time instead of silently mapping to auto.

4. Harness --measure-one without args (CodeRabbit): ${2:?}/${3:?} usage
   message instead of set -u unbound-variable crash.

Skipped with justification:
- Test-code unwrap() annotations: tests are exempt per repo policy
  ("no unwrap in library code OUTSIDE tests"); the audit-unwrap ratchet
  passed in CI.
- flume send_async cancel-safety on the async timeout race: on timeout
  the record MAY still be delivered while the client sees an error —
  that is the safe failure direction (at-least-once on retry, never
  silent loss); documented in a comment instead of a heavy refactor.

Tests: spsc_bounded_blocking_* (3, incl. new exhausted-budget red/green),
wal_append fanout suite (6), everysec tokio suite (4), kv gate test —
all green; fmt + clippy clean on both feature sets.

author: Tin Dang
@pilotspacex-byte
pilotspacex-byte merged commit 450769b into main Jul 4, 2026
10 checks passed
TinDang97 pushed a commit that referenced this pull request Jul 4, 2026
Patch release bundling PR #211 (squash 450769b):
- AOF+WAL KV double-write eliminated (--wal-kv-log auto, -44% device
  writes / +10% P64 at shards=4, GCE-proven, lossless kill-9 recovery)
- everysec/no AOF appends fail-loud under backpressure (error frame
  instead of +OK; batch-shared 5ms bound on SPSC paths)
- Docker image build fix for the vendored-monoio chef stages

CHANGELOG [Unreleased] -> [0.5.1]; RELEASES.md ledger recorded via
add.py release v0.5.1 --force (no milestone bundled — hotfix-style
patch; v3-5 milestone remains open for the follow-up scope).

author: Tin Dang
pilotspacex-byte added a commit that referenced this pull request Jul 7, 2026
…ed, AOF-authority recovery (#236)

* refactor(persistence): unify XactCommit WAL record, retire v1 tag 0x51

Collapse the two XactCommit record variants into one. `XactCommitV2`
(tag 0x53, db-aware: txn_id + db_index + kv_op_count header) becomes
the sole `XactCommit`; the original non-db-aware v1 record (tag 0x51)
is deleted outright rather than reused, so any pre-freeze WAL segment
still carrying 0x51 fails loud on replay instead of being silently
misinterpreted as the new layout.

- src/persistence/wal_v3/record.rs: drop `WalRecordType::XactCommit = 0x51`
  and the v1 `encode_xact_commit_payload`; rename the v2 encoder to
  `encode_xact_commit_payload` (txn_id, db_index, undo_records, db).
  `from_u8` no longer maps 0x51 to anything (pinned by a new unit test).
- src/persistence/wal_v3/replay.rs: merge `replay_xact_commit` and
  `replay_xact_commit_v2` into one db-aware replay function; out-of-range
  db_index falls back to db 0 with a `tracing::warn!`.
- src/server/conn/handler_sharded/txn.rs,
  src/server/conn/handler_monoio/txn.rs: follow the rename
  (`encode_xact_commit_payload_v2` -> `encode_xact_commit_payload`,
  `WalRecordType::XactCommitV2` -> `WalRecordType::XactCommit`).

Tests (red/green): `test_record_type_discriminants` extended to assert
`WalRecordType::from_u8(0x51).is_none()`; renamed
`test_xact_commit_replays_into_selected_db` and
`test_xact_commit_out_of_range_db_falls_back_to_db0` cover the unified
replay path. Both failed to compile against the old dual-variant API
before the rename, pass now.

author: Tin Dang

* refactor(persistence)!: remove WAL v2; WAL v3 is now the only WAL

BREAKING (pre-1.0 format freeze): delete the flat-file WAL v2 writer,
replay path, and file layout entirely. WAL v3 (segment-based, LSN-
tracked `WalWriterV3`) becomes the sole write-ahead log everywhere —
event loop, shard restore, workspace/graph replay, and cross-shard
fanout. A WAL v2 file found on disk after upgrade is rejected loudly
(`WalError::UnsupportedVersion { version: 2 }`) instead of silently
replayed or ignored; operators must re-ingest via AOF or replay it on
a pre-freeze Moon build first.

- src/persistence/wal.rs: deleted (958 lines — `WalWriter`, `replay_wal`,
  `wal_path`, tests).
- src/persistence/mod.rs: drop `pub mod wal;`.
- src/persistence/wal_v3/replay.rs: `replay_wal_auto`'s v2 branch now
  logs a loud `tracing::error!` and returns `WalError::UnsupportedVersion`
  instead of delegating to the deleted v2 replay function.
- src/persistence/recovery.rs: Phase 4b simplified from "WAL v2 fallback
  then AOF fallback" to a single AOF-only fallback
  (`<dir>/appendonly.aof`) when the WAL replayed zero commands.
- src/shard/event_loop.rs: single writer-construction path builds
  `Option<WalWriterV3>` rooted at `<offload-or-persistence-dir>/shard-N/
  wal-v3/` (the old v2 file's niche); all downstream call sites
  (`drain_spsc_shared`, `finalize_snapshot_success`, flush/sync/shutdown)
  take one `wal_writer` argument instead of a v2+v3 pair.
- src/shard/persistence_tick.rs: `finalize_snapshot_success` no longer
  takes a WAL writer at all — v2's per-snapshot
  `truncate_after_snapshot(epoch)` had no v3 equivalent; v3 retention is
  LSN-driven (autovacuum Pass C `recycle_aggressive` /
  `recycle_segments_before`) and already runs independently of snapshot
  epochs. `flush_wal_if_needed` (v2) deleted; `flush_wal_v3_if_needed`
  kept.
- src/shard/timers.rs: `sync_wal` (v2) deleted; `sync_wal_v3` kept.
- src/shard/mod.rs: `restore_from_persistence_v2` (legacy recovery path)
  now does snapshot-load + AOF replay only, no WAL v2 fallback.
- src/shard/shared_databases.rs: `replay_graph_wal` ported from v2
  (`wal::wal_path` + `wal::replay_wal`) to v3 (`wal-v3/*.wal` segment
  scan + `replay_wal_v3_file`), mirroring `replay_workspace_wal`'s
  existing v3 pattern; stale "WAL v2/v3" doc comments on
  `wal_append_txs` and `wal_append` updated to "WAL v3".

Tests (red/green): all WAL v3 unit tests continue to pass
(`cargo test --lib wal_v3::` — 52/52); full `--lib` suite 3781 passed,
0 failed, 1 ignored (default features) and 3126 passed, 0 failed, 1
ignored (runtime-tokio,jemalloc — 1 unrelated pre-existing
environmental disk-free flake in `coordinator_local_leg_durability`,
confirmed passing 7/7 in isolation). `cargo test --no-run` (all
targets) compiles clean under both feature sets.

Deferred (pre-existing, out of scope for this refactor):
- `replay_temporal_wal` in shared_databases.rs scans
  `<dir>/shard-N/` directly instead of `<dir>/shard-N/wal-v3/` —
  directory-mismatch bug that predates this change, discovered while
  porting `replay_graph_wal`.
- `replay_wal_auto`'s v3 "Command" record replay passes the raw RESP
  payload as a parsed command name with empty args, which looks
  architecturally suspect; preserved bug-for-bug in the ported
  `replay_graph_wal` for consistency since it predates this task.

author: Tin Dang

* refactor(shard): simplify WAL fanout gate now that v2 fallback is gone

Apply the simplification the WAL v2 removal unlocks: the cross-shard
write fanout no longer needs to reason about two possible WAL writers.

- src/shard/spsc_handler.rs: `wal_fanout_has_work` collapses to
  `(wal_kv_log && wal_writer.is_some()) || !replica_txs.is_empty() ||
  aof_pool.is_some()` (was: OR of v2-has-work / v3-has-work).
  `wal_append_and_fanout` drops the v3-then-v2-fallback if/else and
  appends to the single `Option<WalWriterV3>` directly. Both functions
  lose the now-redundant second writer parameter, and every call site
  in `drain_spsc_shared` / `handle_shard_message_shared` follows.
- CHANGELOG.md: add the `[Unreleased]` BREAKING entry documenting the
  XactCommit unification and WAL v2 removal, including the two
  deferred follow-ups called out in the previous commit.

Gates (full local CI parity, from the worktree root):
- `cargo fmt --check` — clean.
- `cargo clippy -- -D warnings` — 0 errors (1 pre-existing `must_use`
  warning in vendored `vendor/monoio/src/builder.rs`, out of scope).
- `cargo clippy --no-default-features --features runtime-tokio,jemalloc
  -- -D warnings` — no issues found.
- `cargo test --lib` (default features) — 3781 passed, 0 failed, 1
  ignored.
- `cargo test --no-default-features --features runtime-tokio,jemalloc`
  — 3126 passed, 0 failed, 1 ignored (1 pre-existing environmental
  disk-free flake, confirmed passing 7/7 in isolation).
- `cargo test --no-run` (all targets, both feature sets) — compiles
  clean.
- `bash scripts/audit-unsafe.sh` — 258/258 unsafe blocks have SAFETY
  comments.
- `bash scripts/audit-unwrap.sh` — 0 un-annotated unwraps in hot-path
  modules (baseline 0).

author: Tin Dang

* fix(persistence): restore WAL-preferred recovery + loud legacy-v2 detection

The WAL-v2-removal refactor (branch refactor/wal-v3-only) silently dropped
the pre-refactor "prefer WAL, fall back to AOF" recovery contract in both
non-disk-offload restore (Shard::restore_from_persistence_v2, src/shard/mod.rs)
and the disk-offload fallback path (recover_shard_v3_with_fallback,
src/persistence/recovery.rs). Both paths went straight to AOF replay,
meaning any WAL v3 record written after the last AOF flush (or ahead of
what a lost/rebuilt appendonlydir/ manifest reflects) would be silently
lost on recovery -- a durability regression versus the retired WAL v2
behavior.

This change:

- Adds replay_wal_v3_dir_commands() (src/persistence/wal_v3/replay.rs), a
  thin wrapper over the existing segment-replay primitives that drives a
  CommandReplayEngine across every Command/XactCommit record in a WAL v3
  directory and returns the count replayed.
- restore_from_persistence_v2 and recover_shard_v3_with_fallback's Phase 4b
  now try WAL v3 first and only fall back to AOF replay when the WAL v3
  directory is absent or empty, mirroring the old WAL-v2-era preference and
  closing the "no durable write lost" gap.
- Both paths now tracing::error! (not silently skip) when a leftover legacy
  shard-{id}.wal (v2) file is found on disk, naming the file and stating
  that v2 support was removed and this build will not replay it.
- Corrects a misleading test comment (src/persistence/wal_v3/record.rs)
  that claimed unrecognized WAL v3 record tags are "warn + skip" -- the
  segment replay loop actually stops replaying the rest of that segment
  on any unrecognized/corrupt record. No behavior changed, comment only.

Testing: 8 new unit tests (3 in shard/mod.rs, 3 in persistence/recovery.rs,
plus the pre-existing record.rs assertion whose comment was corrected)
cover WAL-v3-preferred-over-AOF, AOF-fallback-when-WAL-v3-absent, and
stray-legacy-v2-file-does-not-break-recovery, for both the non-offload and
disk-offload-fallback code paths. Verified RED (temporarily hardcoding the
pre-fix "always AOF" behavior reproduced the exact failures: e.g.
`test_legacy_wal_v3_preferred_over_stale_aof` failed with left=2 right=5,
`test_restore_from_persistence_v2_prefers_wal_v3_over_aof` failed with
left=1 right=3) then GREEN after restoring the real fix.

An end-to-end SIGKILL crash-recovery integration test was attempted per
the original review ask but proven empirically unreliable within repo
constraints: --disk-offload defaults to "enable" (routes around the fixed
path entirely), ordinary local KV writes never touch WAL v3 (they go
through aof_pool.try_send_append_durable; WAL v3 only receives
cross-shard SPSC writes gated by --wal-kv-log, or WS/MQ/GRAPH/TXN
records), and forcing cross-shard dispatch via SO_REUSEPORT connection
routing is non-deterministic across separate redis-cli invocations. The
integration test and its helper were removed in favor of the deterministic
unit tests above, which exercise the same fixed functions directly; the
rationale is documented as a comment in
tests/crash_matrix_per_shard_aof.rs.

Gates run locally (this worktree, refactor/wal-v3-only):
- cargo fmt --check: clean
- cargo clippy -- -D warnings: 0 errors (1 pre-existing vendored monoio
  warning, unrelated to this change)
- CARGO_TARGET_DIR=target-tokio cargo clippy --no-default-features
  --features runtime-tokio,jemalloc -- -D warnings: no issues found
- cargo test --lib: 3787 passed, 1 ignored
- CARGO_TARGET_DIR=target-tokio cargo test --lib --no-default-features
  --features runtime-tokio,jemalloc: 3132 passed, 1 ignored
- cargo test --no-run (both feature sets): all test binaries compile

author: Tin Dang

* fix(persistence): AOF stays recovery authority; WAL v3 is last-resort only

Corrects the recovery-preference direction introduced by the previous
commit ("restore WAL-preferred recovery"). That fix resurrected the
retired WAL v2 "prefer the WAL, skip the AOF when the WAL replays >0"
contract for WAL v3 — but the contract's premise no longer holds:

Post-#211, WAL v3's KV coverage is intentionally PARTIAL. `--wal-kv-log`
is default-off, and connection-local KV writes bypass WAL v3 entirely
even when it is on (measured WAL-only recovery: 79.2% incomplete, see
tmp/WRITE-DIAG.md). Meanwhile `replay_wal_v3_dir_commands` counts EVERY
replayable record type (temporal/graph/txn included), so a WAL v3 dir
holding only non-KV records returns non-zero, shadows the AOF, and the
node recovers with ZERO KV data — total KV loss, strictly worse than
the gap the previous commit set out to close.

This change flips the preference in both legacy-dir recovery paths:

- `Shard::restore_from_persistence_v2` (src/shard/mod.rs) and Phase 4b
  of `recover_shard_v3_with_fallback` (src/persistence/recovery.rs)
  now replay `appendonly.aof` as the authority whenever it exists, and
  replay the legacy-mode `shard-N/wal-v3/` directory ONLY when no AOF
  exists — with a loud `tracing::warn!` stating the fallback's partial
  KV coverage. The loud legacy-v2 `shard-N.wal` detection from the
  previous commit is kept unchanged.
- `replay_wal_v3_dir_commands` doc comment rewritten: the helper is
  explicitly NOT an authority signal; callers must never skip the AOF
  on a non-zero return.
- Tests flipped/renamed to assert the corrected semantics, both paths:
  AOF-wins-over-populated-WAL (would have been the KV-loss scenario)
  and WAL-v3-last-resort-when-no-AOF (partial recovery beats none).
  AOF-when-WAL-absent and stray-legacy-v2 tests kept as-is.
- CHANGELOG BREAKING entry + crash_matrix_per_shard_aof.rs NOTE updated
  to describe the AOF-authority + last-resort-fallback semantics.

author: Tin Dang

---------

Co-authored-by: Tin Dang <tindang.ht97@gmail.com>
TinDang97 added a commit that referenced this pull request Aug 8, 2026
…luster election, silent-failure hardening (#453)

* fix(storage): full-width last_access + 24-bit entry version (deep-review A1/A3/A4/A2)

The packed 32-bit entry metadata stored last_access in 16 bits with three
mutually inconsistent wraparound treatments, and the WATCH/EXEC version in
8 bits:

- lfu_decay subtracted the u16-truncated last_access from the full u32
  epoch clock and truncated the quotient with `as u8`, so the decay value
  cycled 0->255 every ~4.5h of wall clock — LFU victim selection was
  effectively pseudo-random from day one.
- lru_is_older's i16 circular compare inverted verdicts once two keys'
  access times differed by more than ~9.1h, so allkeys-lru evicted hot
  keys and retained cold ones under diurnal idle patterns.
- OBJECT IDLETIME reported idle time modulo 65536s (18.2h wrap).
- The 8-bit version wrapped after 256 writes between WATCH and EXEC and
  reported 0 for both "absent" and "freshly created", so EXEC committed
  over concurrent modifications (ABA) and never detected key creation.

Fix, at zero size cost (CompactEntry stays 32 bytes): repurpose the unused
_pad u32 as a full-width last_access_secs field, and use the freed 16
metadata bits to widen version to 24 bits ([version:24|counter:8]).
Versions start at INITIAL_VERSION=1 and the wrap skips 0, so version 0 is
now a reliable "key absent" sentinel for WATCH creation detection, and the
ABA window moves from 256 to 16.7M intervening writes.

lfu_decay and IDLETIME use saturating_sub so a lagging cached clock yields
zero elapsed rather than a near-2^32 value; lru_is_older uses i32 circular
distance (valid to +/-68 years, correct across the year-2106 wrap).

Red/green: new regression tests for >9.1h LRU ordering, 2h/11h LFU decay,
>18.2h idle survival, 24-bit wrap-skip-0, and version-1-on-create; stale
8-bit/16-bit assertions updated to the new contract.
author: Tin Dang

* fix(persistence): fail-loud everysec fsync on tokio paths + status latch (deep-review F2/F3)

Both tokio everysec deadline-fsync sites dropped flush/sync_data errors
with `let _ =` and then unconditionally recorded the fsync as successful
(last_fsync reset, pending cleared) — zero log lines, and the PerShard
variant additionally lacked the `!write_error` torn-stream gate its three
siblings have. Because the kernel marks pages clean after a failed fsync
(fsyncgate), the next attempt succeeds trivially: the failed window's
records are permanently absent from the AOF while the server keeps
claiming the <=1s everysec loss bound.

- Tokio TopLevel + PerShard now mirror the monoio sites: log error!,
  record the fsync metric only on success, keep last_fsync unadvanced on
  failure, and gate on !write_error.
- New AOF_LAST_FSYNC_OK / AOF_FSYNC_FAILURES statics
  (record_everysec_fsync_result), wired into all four everysec sites
  across both runtimes and surfaced in INFO # Persistence as
  aof_last_fsync_status:ok|err + aof_fsync_failures — Moon's analogue of
  Redis's aof_last_write_status, closing the "no operator surface"
  inconsistency with the WalSyncAgent poison latch and PR #211 policy.

Red/green: everysec_fsync_failure_latches_err_status covers the latch
contract (err latches + counts; success restores status, never the count).
author: Tin Dang

* fix(storage): re-insert evicted key when background spill write fails (deep-review F1)

evict_one_async_spill removes the hot entry as soon as the spill request is
queued, relying on the completion to land the key in the cold index. When
the background pwrite failed (ENOSPC/EIO), the failure completion carried
no payload and the handler warned and continued — the key existed in
NEITHER plane, so GET returned nil for an acked write until an AOF-replay
restart (and permanently on the no-AOF sync paths' async sibling).

- SpillCompletion now carries the original SpillRequest back on failure
  (Box'd; Bytes fields are refcounted, so the clone is cheap).
- apply_completion_vec rehydrates the payload (rehydrate_spill_payload,
  the exact inverse of build_spill_payload) and re-inserts into the hot
  table via with_shard_db — fail-closed, mirroring the enqueue-failure
  path which retains the hot value. A newer write that recreated the key
  while the spill was in flight wins (version!=0 check); the stale payload
  is dropped.
- Loud operator surface: error! log + new spill_failed_reinserted counter
  in INFO # Persistence (nonzero means the spill volume is failing
  writes).

Red/green: spill_payload_round_trips_through_rehydrate (hash + string,
TTL fidelity) and failed_spill_write_carries_request_back (unwritable
shard_dir → failure completion carries key/payload/ttl back).
author: Tin Dang

* fix(tracking): enforce the max_keys bound on the tracking table (deep-review G1)

TrackingTable documented "bounded: max_keys (default 1_000_000), evict
oldest with fake invalidation" but the field was #[allow(dead_code)] — a
long-lived CLIENT TRACKING client reading many distinct never-written keys
grew key_clients without limit (writes and disconnects clean up, but a
read-mostly keyspace never triggers either).

track_key now enforces the cap: tracking a NEW key at capacity evicts an
arbitrary existing entry and returns its (key, senders) so track_read_keys
pushes the RESP3 invalidation for the evicted key — clients drop their
cached copy instead of holding it stale forever (Redis's tracking-table
"fake invalidation" semantics). Re-tracking an existing key never evicts.

Known follow-up (documented, out of scope here): expiry/eviction of the
underlying KEY still doesn't invalidate trackers — only writes do.

Red/green: test_track_key_enforces_max_keys_bound (cap holds, evicted
key's tracker notified, existing-key re-track exempt).
author: Tin Dang

* fix(cluster): disable the inline GET/SET fast path in cluster mode (deep-review R6)

The monoio inline dispatch loop executes plain GET/SET against the local
DashTable before the generic dispatch loop runs, and had no cluster gate:
can_inline_writes checked ACL/multi/tracking/replica/spill/fanout but not
cluster_enabled(), and GET inlining consulted no gate at all. In cluster
mode the two hottest commands therefore bypassed try_handle_cluster_routing
entirely — writes for slots owned elsewhere were silently misplaced
locally, reads returned stale/nil instead of MOVED, and non-ASKING writes
to migrating slots broke the ASK handoff (the known "three dispatch paths"
bug class).

try_inline_dispatch_loop now takes cluster_enabled (passed from
crate::cluster::cluster_enabled() at the call site; a param for
unit-testability) and inlines nothing when set — every command falls
through to the generic loop where MOVED/ASK routing lives. Non-cluster
deployments are unaffected (one relaxed atomic load per batch).

Red/green: test_inline_loop_disabled_in_cluster_mode.
author: Tin Dang

* fix(persistence): gate AOF rewrite prune on manifest-sync barrier + durable dirent (deep-review D1/D4)

Two crash windows around the rewrite's old-generation prune:

D1 — The task-#59 deferred ShardManifest commit is justified by "AOF
replay + orphan sweep reconstruct anything a lost manifest commit would
have recorded". The #433 auto-rewrite deletes exactly those AOF records
(the fold snapshots only db.data(), so cold-spilled keys exist ONLY in the
old incr), and the rewrite's own multi-GB write+fsync is what stalls the
manifest-sync thread, widening the deferred window across the rewrite. A
kill-9 after the prune but before the deferred root persists loses every
spilled key in the un-persisted batch — and the boot orphan sweep then
deletes the .mpf holding the only surviving copy.

D4 — write_manifest's rename fsyncs its parent dir best-effort (warn
only). Pruning after a FAILED dir fsync can crash into "old manifest
resolves, old files deleted": shard replay treats that as a fresh init and
cleanup_orphans deletes the NEW generation too — silent total loss
instead of a fail-stop.

Fix: both prune sites (TopLevel AofManifest::advance step 4; PerShard
RewriteCoordinator::shard_done) now delete the old generation only after
(a) manifest_sync::flush_all_agents() — a new ack-only barrier that blocks
until every live agent's pending deferred snapshot is durable (bounded
30s; registry of Weak agent handles, registered at spawn, deregistered at
shutdown) — and (b) an explicit propagating fsync_directory on the AOF
manifest's parent. On either failure the old generation is retained: costs
disk space, never data.

Red/green: flush_all_agents_forces_pending_deferred_commit_durable
(injected sync delay; barrier alone must make the snapshot durable with no
shutdown flush) + flush_all_agents_is_noop_when_idle.
author: Tin Dang

* fix(cluster): election ack path, vote integrity, epoch discipline, lock rules (deep-review C1-C4/R8/A5)

Five defects in the v0.9 W0/C-1 control plane, each small and independently
verifiable (the structural C-2/C-3 gaps — slot-map propagation, demotion,
plane wiring, node persistence — are tracked separately, not half-fixed
here):

- C1: election votes were structurally unreceivable. The replica sent
  FailoverAuthRequest on a fire-and-forget connection and dropped it before
  the master's reply, which was written back on that same dying stream; no
  master ever dials the replica's bus. Any election with votes_needed >= 2
  timed out unconditionally (masked in small test clusters where a FAILed
  master deflates quorum to 1 and the self-vote wins alone). The requester
  now holds each stream open and reads the length-prefixed ack back
  (4s bound, frame-length guard); the bus inbound ack arm stays as a
  secondary path.
- C2: acks carried no election binding and voters no identity check — a
  stale ack from a previous timed-out election, or repeated acks from one
  voter, counted toward quorum. Acks now ECHO the request epoch; the vote
  loop discards epoch mismatches and dedups voters via HashSet (self-id
  pre-seeded so a reflected self-ack can never double-count).
- C4: on win, check_and_initiate_failover bumped state.epoch a SECOND time,
  so the promoted master claimed an epoch nobody voted for — defeating
  last_vote_epoch's single-vote-per-epoch guarantee and enabling epoch ties
  gossip's strict-> merge cannot resolve. Election wins now promote via
  promote_self_to_master at the voted epoch; FORCE/TAKEOVER keep their
  explicit bumps.
- R8: graceful CLUSTER FAILOVER (no args) parked failover_state at
  WaitingDelay{0} forever — nothing consumed it and the ticker's election
  gate requires FailoverState::None, so one admin command permanently
  disabled automatic failover on the node. It now returns an explicit
  "not yet supported" error (state untouched) until the coordinated
  offset-sync protocol lands.
- C3: ClusterState was std::sync::RwLock with ~25 .read()/.write().unwrap()
  sites including the per-command route_slot path — violating the repo lock
  rules and turning any panic under a write guard into poisoning that
  panics every shard thread. Migrated to parking_lot::RwLock (no poisoning,
  no unwraps) across src/cluster, main.rs, shard wiring, and conn handlers.
- A5: a peer announcing a client port > 55535 got a silently wrapped bus
  port (our own port is already refused at startup); the wrap now warns
  loudly that gossip to that peer will fail.

Red/green: test_promote_self_keeps_voted_epoch,
test_failover_normal_errors_without_wedging_state; existing 55 cluster
tests green.
author: Tin Dang

* fix(storage): fail-loud vector compaction, idle CDC reaping, bounded temporal registry (deep-review F4/G3/G4)

F4 — vector background compaction failed silently at every layer: a dead
worker (panic/pool teardown) cleared inflight with no log, a compaction
Err (e.g. B2 disk persist on a full volume) was dropped with no log, and
the resubmit swallow made it an invisible infinite retry loop while the
mutable segment grew past COMPACT_THRESHOLD and FT.SEARCH degraded toward
brute force. The merge path learned this lesson in PR #353; the compact
path now matches: error! on worker death, warn! on compaction failure,
debug trail on deferred submits.

G3 — CDC subscribers whose consumer disconnected were only reaped via a
failed try_send driven by a NEW WAL record; write-idle shards accumulated
dead subscribers (each holding a tail reader) indefinitely. fanout_tick
now checks tx.is_disconnected() every tick.

G4 — TemporalRegistry grew one binding per TEMPORAL.SNAPSHOT_AT for the
life of the process. Bounded at 262,144 bindings/shard (~4 MB); overflow
evicts the oldest, bounding AS_OF history depth instead of memory.

Red/green: test_cdc_fanout_reaps_disconnected_subscriber_when_idle,
test_registry_bounded_evicts_oldest.
author: Tin Dang

* docs(changelog): record the 2026-08 deep-review fix wave

Consolidated [Unreleased] entry for the eight fix groups landed from the
six-dimension architecture review; structural follow-ups tracked in
issues #451 (cluster C-track) and #452 (durability).
author: Tin Dang

* style: rustfmt import ordering after deep-review wave

Formatting-only pass over files touched by the 2026-08 deep-review
fixes (cluster, tracking, AOF, conn tests). No behavior change.

author: Tin Dang

* test(cluster): migrate integration harness ClusterState lock to parking_lot

The C3 lock migration moved `Shard::run`'s `cluster_state` parameter to
`parking_lot::RwLock`, but the integration-test cluster harness still
wrapped ClusterState in `std::sync::RwLock`, so `cargo test` (all
targets) failed to compile on CI while the local `--lib`-only gate
stayed green. Lesson applied: verify with `cargo check --all-targets`
on both feature sets, both now clean.

author: Tin Dang

* fix(persistence): close 4 adversarial-review findings on the deep-review wave

Pre-merge adversarial review of this branch confirmed four defects in the
new code; all four are fixed here with red/green unit coverage.

1. P1 — flush_all_agents false-Ok after a failed persist: a failed
   deferred manifest commit consumed the root, latched nothing, and the
   next empty-slot barrier round acked Ok — letting the rewrite prune the
   old incr while the on-disk ShardManifest was missing spill placements
   (permanent cold-plane loss on a later crash). SyncShared now latches
   last_persist_failed; empty-slot rounds ack Err until a newer snapshot
   persists successfully.

2. P2 — AGENT_REGISTRY zombie: an agent handle dropped without shutdown()
   (panic unwind, early teardown) left the registry's wake-sender clone
   keeping the sync thread parked forever holding the manifest fd, and
   strong_count pruning could never collect it (the thread holds its own
   strong Arc). ManifestSyncAgent now deregisters in Drop, so the thread
   sees the disconnect, flushes, and exits.

3. P2 — AOF_LAST_FSYNC_OK cross-writer masking: one global bool meant a
   healthy shard's everysec success cleared a failing shard's err status
   within ~1s. Replaced with a per-writer bitmask (AOF_FSYNC_ERR_WRITERS);
   INFO reports err while ANY writer's latest fsync failed.

4. P2 — failed-spill re-insert stale shadow: a failed pwrite for key K
   could re-insert its stale payload into the hot table after K was
   re-created and re-evicted (newer spill in flight), shadowing the newer
   cold value and re-spilling the stale copy as authoritative. Databases
   now track the newest in-flight spill request id per key (requests
   already carry a unique monotonic file_id); a superseded request's
   failure arm skips the re-insert, and completions retire their own
   record.

Also removes two stale std-RwLock clippy annotations left over from the
parking_lot migration (route_slot call sites).

Refs the deep-review wave PR; found by pre-merge adversarial review.
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