Skip to content

perf(persistence): local-leg group commit + BITOP/COPY/DEL/UNLINK coordinator leg durability#213

Merged
pilotspacex-byte merged 5 commits into
mainfrom
perf/local-leg-group-commit
Jul 6, 2026
Merged

perf(persistence): local-leg group commit + BITOP/COPY/DEL/UNLINK coordinator leg durability#213
pilotspacex-byte merged 5 commits into
mainfrom
perf/local-leg-group-commit

Conversation

@pilotspacex-byte

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

Copy link
Copy Markdown
Contributor

Summary

Two v3-5 write-path-durability items, both red/green TDD-proven, plus a test-harness de-flake.

1. Coordinator local legs ride group commit under appendfsync=always (666d32c)

persist_local_leg (co-located MSET/MSETNX, scattered-MSET local slices) awaited one fsync ack per command, each bounded by --aof-fsync-timeout-ms (default 2000ms) — a pipeline of coordinated writes stacked these serially into the measured 2000–3000ms always far-tail (tmp/V3-4-GCLOUD-BENCH.md; the carried [HIGH] follow-up).

Local legs now use the same fire-and-forget-then-barrier contract as the remote SPSC legs:

  • New AofWriterPool::send_append_group — bounded-backpressure enqueue, never awaits the per-write ack; Ok(true) = caller owes a barrier (Always only).
  • Both connection handlers collect barrier-pending response indexes and issue one fsync_barrier(local shard) per pipeline batch, before response serialization — +OK still implies confirmed durability; batch of N coordinated writes = 1 awaited fsync instead of N.
  • On barrier failure every affected response becomes MOONERR AOF fsync — never a false +OK. everysec/no unchanged.

2. BITOP/COPY/DEL/UNLINK local legs now persist (d513e8d)

The coordinator's in-process legs for BITOP (dest write), COPY (dst write + PEXPIRE), and multi-key DEL/UNLINK (co-located fast path AND scattered local slice) executed in memory but never reached the AOF (carried v3-4 follow-up). Failure modes on kill-9 + restart: deleted keys resurrected from their seed writes; BITOP/COPY results on the connection's own shard vanished.

  • New run_on_owner_persist mirrors run_on_owner but appends the executed command to the local shard's AOF on success — replay-safe (each persisted command covers only locally-owned keys).
  • DEL/UNLINK persist skipped when nothing was removed (n=0 replays identically). EXISTS/TOUCH stay read-only.
  • All legs ride the batch-end barrier from item 1.

3. Cluster test harness de-flake (1fd4ac7, test-only)

start_cluster_server slept a fixed 100ms then connected with a no-retry unwrap — "Connection refused" flakes under full-suite parallelism (observed repeatedly in CI-parity runs, pass in isolation). Replaced with a bounded connect-poll; integration suite now 3/3 green at full parallelism.

Testing

  • 3 new red-proven pool unit tests (send_append_group must not await the per-write ack / everysec no-barrier / dead-writer errors)
  • 4 new red-proven crash-recovery tests in coordinator_local_leg_durability.rs (DEL scatter resurrection, UNLINK fast-path resurrection, BITOP dest both shapes, COPY dst both shapes) — deterministic via per-shard {hash-tags}; suite 7/7 green post-fix
  • Full OrbStack CI-parity matrix green: fmt, clippy -D warnings both feature sets, monoio --release full suite, tokio full suite (zero failures, honest exit code)

Follow-up (not in this PR)

  • Absolute always-tail magnitude requires a GCE bench (OrbStack fsync is near-free) — happy to run the A/B on request
  • Remaining v3-5 scope: WAL-only conn-local logging, shardslice waiver retirement

Summary by CodeRabbit

  • Performance

    • Improved coordinator-local AOF durability by batching local fsync work for grouped multi-key and delete operations.
  • Bug Fixes

    • Fixed coordinator-local legs for multi-key writes and deletes so they reliably persist to the owning shard’s log, preventing post-restart key resurrection or missing results.
    • Improved handling of local durability barrier failures to avoid incorrect success responses.
  • Tests

    • Added restart durability regression coverage for DEL/UNLINK/BITOP/COPY local-leg scenarios.
    • Improved cluster test server startup reliability using TCP readiness probing.

…commit under appendfsync=always

The cross-shard coordinator's LOCAL-leg persist (co-located MSET/MSETNX
and scattered-MSET local slices, shipped in v3-4 Finding 1) called
try_send_append_durable, which under appendfsync=always awaits one fsync
ack PER COMMAND bounded by --aof-fsync-timeout-ms (default 2000ms). A
pipeline of coordinated writes stacked these serially — the measured
2000-3000ms always far-tail (tmp/V3-4-GCLOUD-BENCH.md), carried as the
[HIGH] follow-up into v3-5.

Fix: local legs now use the same fire-and-forget-then-barrier contract
the remote SPSC legs have used since the H1-BARRIER fix:

- New AofWriterPool::send_append_group enqueues under bounded
  backpressure and returns immediately; Ok(true) means Always policy —
  the caller owes a barrier. EverySec/No are unchanged (writer loop owns
  the fsync cadence).
- persist_local_leg switches to it and reports needs-barrier up through
  coordinate_mset/coordinate_msetnx/coordinate_multi_key.
- Both connection handlers (monoio + sharded) collect the response
  indexes of barrier-pending local-leg writes and issue ONE
  fsync_barrier(local shard) per pipeline batch, BEFORE response
  serialization — +OK still implies confirmed durability, but a batch of
  N coordinated writes costs 1 awaited fsync instead of N.
- On barrier failure every affected response is overwritten with
  AOF_FSYNC_ERR — never a false +OK (design-for-failure preserved).

Tests: 3 new red-proven pool unit tests (send_append_group must not
await the per-write ack; everysec needs no barrier; dead writer errors);
v3-4 coordinator_local_leg_durability crash-recovery suite green on the
new path (local legs still persist and replay); full lib suite 3656
pass; clippy clean on default + tokio,jemalloc.

Absolute tail magnitude needs a GCE run (OrbStack fsync is near-free);
tracked in v3-5.

author: Tin Dang
The cross-shard coordinator's in-process legs for BITOP (dest write),
COPY (dst write + PEXPIRE TTL restore), and multi-key DEL/UNLINK (both
the co-located fast path and the scattered local slice) executed in
memory but never appended to the owning shard's AOF — the carried v3-4
follow-up. Failure modes on kill-9 + restart:

- DEL/UNLINK: deleted keys RESURRECTED from their seed writes (the seed
  MSET is in the AOF, the local-leg delete never was).
- BITOP/COPY: results whose dest/dst owner == the connection's own shard
  silently vanished. Remote legs were always durable (MultiExecute ->
  wal_append_and_fanout).

Fix: all four now persist through the same persist_local_leg
group-commit path as MSET/MSETNX (previous commit):

- New run_on_owner_persist mirrors run_on_owner but appends the command
  to my_shard's AOF when the owner is local and execution succeeded —
  used by BITOP's whole-command forward + synthesized DEL/SET dest legs
  and COPY's whole-command forward + SET/PEXPIRE dst legs. Replay-safe:
  each persisted command covers only keys owned by my_shard.
- coordinate_multi_del_or_exists persists DEL/UNLINK on the co-located
  fast path (whole command) and the scattered local slice (synthesized
  over only local keys), skipped when n=0 (nothing removed replays
  identically without a record). EXISTS/TOUCH stay read-only.
- All legs ride the batch-end fsync barrier under appendfsync=always
  via local_barrier_pending (previous commit's plumbing).

Tests: 4 new red-proven crash-recovery tests in
coordinator_local_leg_durability.rs (DEL scatter resurrection, UNLINK
co-located fast-path resurrection, BITOP dest legs both shapes, COPY dst
legs both shapes) — deterministic via per-shard {hash-tags} regardless
of which shard the connection lands on; suite 7/7 green post-fix. Full
lib suite 3656 pass; clippy clean both feature sets.

author: Tin Dang
…s sleep

The cluster_* tests spawned an in-process cluster server, slept a fixed
100ms, then connected with a no-retry unwrap (integration.rs:242). Under
full-suite parallelism the listener thread can take longer to bind ->
"Connection refused" flakes (observed repeatedly in CI-parity runs; pass
in isolation). Replace the sleep with a bounded connect-poll (10s), the
same pattern the binary-spawning suites use.

author: Tin Dang
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7d1a6064-693f-4d5e-9877-d3bb9b749fc8

📥 Commits

Reviewing files that changed from the base of the PR and between 7e7304a and aa3e300.

📒 Files selected for processing (1)
  • CHANGELOG.md
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md

📝 Walkthrough

Walkthrough

This PR adds group-commit durability for coordinator-local write legs, threads barrier tracking through multi-key coordination and connection handling, and adds restart regression coverage plus changelog and startup readiness updates.

Changes

Coordinator local-leg durability

Layer / File(s) Summary
AOF pool group-commit enqueue API
src/persistence/aof/pool.rs
Adds send_append_group returning whether a later fsync barrier is required, with unit tests for Always, EverySec, and dead-writer cases.
Coordinator persistence and local-leg routing
src/shard/coordinator.rs
Reworks persist_local_leg to use group-commit enqueue, adds run_on_owner_persist, and threads local_barrier_pending through coordinate_multi_key and its BITOP/COPY/MSET/MSETNX/DEL-UNLINK-EXISTS sub-coordinators, plus test updates.
Connection handler barrier wiring
src/server/conn/handler_monoio/dispatch.rs, src/server/conn/handler_monoio/mod.rs, src/server/conn/handler_monoio/pubsub.rs, src/server/conn/handler_sharded/mod.rs, src/server/conn/handler_sharded/pubsub.rs, src/server/conn/shared.rs
Tracks pending local-leg write response indices per batch and resolves them with local fsync barriers before flush or serialization, overwriting tracked responses with AOF_FSYNC_ERR on failure.
Restart durability tests and changelog
tests/coordinator_local_leg_durability.rs, tests/integration.rs, CHANGELOG.md
Adds restart regression tests for DEL/UNLINK/BITOP/COPY local-leg persistence, replaces a fixed startup sleep with active TCP readiness polling, and documents the performance and durability fix.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Coordinator
  participant AofWriterPool
  participant Shared
  Client->>Coordinator: multi-key write
  Coordinator->>AofWriterPool: enqueue local leg append
  Coordinator->>Shared: resolve_local_leg_barrier
  Shared->>AofWriterPool: fsync_barrier(shard_id)
  Shared-->>Coordinator: update responses or clear pending indexes
  Coordinator-->>Client: send batch responses
Loading

Possibly related PRs

  • pilotspace/moon#129: Both PRs extend the AOF writer-pool and barrier plumbing with group-append and fsync coordination.
  • pilotspace/moon#173: Both PRs touch src/shard/coordinator.rs BITOP/COPY coordination paths.
  • pilotspace/moon#178: Both PRs rely on AOF batching and fsync-barrier behavior for durability.

Suggested labels: enhancement

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title concisely captures the main persistence and group-commit changes.
Description check ✅ Passed The description is detailed and mostly complete, though it doesn’t use the template’s exact Checklist and Performance Impact sections.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/local-leg-group-commit

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

Coordinator local-leg group commit + BITOP/COPY/DEL/UNLINK AOF durability

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

Grey Divider

AI Description

• Batch coordinator local-leg AOF appends and confirm with one fsync barrier per pipeline.
• Persist coordinator local legs for BITOP/COPY/DEL/UNLINK to prevent crash resurrection.
• De-flake cluster integration harness by polling until the listener accepts connections.
Diagram

graph TD
  Client((Client)) --> Handler["Conn handler"] --> Coord["Shard coordinator"] --> LocalDB[("Local shard DB")] --> Pool["AofWriterPool"] --> Barrier{{"fsync_barrier"}} --> Reply["Serialize + send"]

  subgraph Legend
    direction LR
    _cli((Client)) ~~~ _svc["Service"] ~~~ _db[(DB)] ~~~ _dec{{Barrier}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Fsync inside coordinator per local-leg write
  • ➕ Keeps durability logic localized to coordinator code paths
  • ➖ Reintroduces per-command waits; loses pipeline-batch amortization
  • ➖ Harder to preserve the invariant that +OK implies confirmed durability for the whole batch
2. Keep try_send_append_durable for local legs and tune timeouts
  • ➕ Minimal behavioral change; no new API surface
  • ➖ Still serializes fsync waits per command under appendfsync=always
  • ➖ Timeout tuning can mask, not solve, far-tail behavior under pipelining
3. Switch local legs to a WAL-only commit protocol (defer AOF durability)
  • ➕ Could reduce fsync frequency further and simplify aof-barrier semantics
  • ➖ Larger architectural change; impacts replay/replication contracts
  • ➖ Higher regression risk than aligning local legs with existing remote-leg barrier pattern

Recommendation: The PR’s approach (fire-and-forget enqueue + single batch-end fsync_barrier) is the best fit because it reuses the already-established remote-leg durability contract, preserves the client-visible guarantee that +OK implies confirmed durability under appendfsync=always, and removes per-command fsync serialization without widening failure modes (barrier failure deterministically flips affected replies to AOF_FSYNC_ERR).

Files changed (8) +703 / -41

Enhancement (1) +110 / -0
pool.rsAdd send_append_group API and unit tests for non-blocking enqueue +110/-0

Add send_append_group API and unit tests for non-blocking enqueue

• Introduces AofWriterPool::send_append_group, which enqueues appends under bounded backpressure without awaiting per-write fsync acks, returning whether an Always-policy barrier is required. Adds unit tests verifying Always does not block on fsync, EverySec requires no barrier, and dead-writer errors surface as Err.

src/persistence/aof/pool.rs

Bug fix (4) +268 / -39
dispatch.rsTrack coordinator local-leg writes requiring a batch-end fsync barrier +10/-0

Track coordinator local-leg writes requiring a batch-end fsync barrier

• Extends cross-shard dispatch to accept a vector of response indexes for successful local-leg writes that were enqueued under appendfsync=always. Ensures only non-error responses are eligible to be overwritten on a later barrier failure.

src/server/conn/handler_monoio/dispatch.rs

mod.rsIssue one local-shard fsync_barrier per batch before serializing responses +30/-2

Issue one local-shard fsync_barrier per batch before serializing responses

• Adds per-batch tracking for coordinator local-leg writes and calls fsync_barrier(ctx.shard_id) once per batch when needed. On barrier failure, rewrites all affected responses to AOF_FSYNC_ERR before response serialization to avoid false +OK durability signals.

src/server/conn/handler_monoio/mod.rs

mod.rsMirror monoio: batch-end fsync barrier for coordinator local legs +27/-1

Mirror monoio: batch-end fsync barrier for coordinator local legs

• Adds the same response-index tracking and single fsync_barrier(ctx.shard_id) invocation in the tokio sharded handler. Preserves the invariant that barrier failures only overwrite successful write responses.

src/server/conn/handler_sharded/mod.rs

coordinator.rsPersist BITOP/COPY/DEL/UNLINK local legs and propagate group-commit barrier requirement +201/-36

Persist BITOP/COPY/DEL/UNLINK local legs and propagate group-commit barrier requirement

• Threads a local_barrier_pending flag through coordinator entrypoints and changes persist_local_leg to enqueue via send_append_group, returning whether a handler-side barrier is required. Adds run_on_owner_persist and applies it to BITOP/COPY, and persists local slices/fast paths for DEL/UNLINK when they actually remove keys, preventing restart resurrection and lost local results.

src/shard/coordinator.rs

Tests (2) +296 / -2
coordinator_local_leg_durability.rsAdd crash-recovery tests for DEL/UNLINK resurrection and BITOP/COPY durability +283/-0

Add crash-recovery tests for DEL/UNLINK resurrection and BITOP/COPY durability

• Adds helpers and multiple restart-based integration tests proving coordinator local legs for DEL scatter, UNLINK co-located fast path, BITOP dest writes (scatter + co-located), and COPY dst writes (cross-shard + same-owner) survive kill -9 + restart. Validates both presence and absence conditions to catch resurrection regressions.

tests/coordinator_local_leg_durability.rs

integration.rsDe-flake cluster startup by polling until the port accepts connections +13/-2

De-flake cluster startup by polling until the port accepts connections

• Replaces a fixed sleep with a bounded connect-poll loop to wait for the listener to actually bind before tests attempt to connect. Eliminates intermittent connection refused panics under parallel integration runs.

tests/integration.rs

Documentation (1) +29 / -0
CHANGELOG.mdDocument local-leg group commit and new durability coverage +29/-0

Document local-leg group commit and new durability coverage

• Adds unreleased changelog entries describing coordinator local-leg group commit under appendfsync=always and the durability fix for BITOP/COPY/DEL/UNLINK local legs, including failure-mode notes and error semantics.

CHANGELOG.md

@qodo-code-review

qodo-code-review Bot commented Jul 4, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Context used
✅ Compliance rules (platform): 40 rules

Grey Divider


Remediation recommended

1. Benchmark numbers lack Linux context ✓ Resolved 📘 Rule violation ➹ Performance
Description
New benchmark-like latency numbers (measured 2000–3000ms always-tail) are added without stating
they were collected on a Linux environment (OrbStack VM/GCloud) or explicitly marked as
non-production. This violates the requirement that production performance benchmarking be reported
only with Linux environment context to avoid misleading conclusions.
Code

CHANGELOG.md[R9-15]

+### Performance — coordinator local legs ride group commit under appendfsync=always (PR #TBD)
+
+- The cross-shard coordinator's LOCAL-leg persist (co-located MSET/MSETNX and
+  scattered-MSET local slices) awaited one fsync ack **per command**, each
+  bounded by `--aof-fsync-timeout-ms` (default 2000ms) — a pipeline of
+  coordinated writes stacked these serially into the measured 2000–3000ms
+  `always` far-tail. Local legs now enqueue fire-and-forget (bounded
Relevance

⭐⭐⭐ High

Team stresses benchmark environment validity (OrbStack vs GCloud) in changelog/PRs; likely require
explicit Linux/non-prod context.

PR-#203
PR-#204

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 351129 requires benchmark results to be reported with explicit Linux environment context. The
PR adds a measured latency range in CHANGELOG and repeats it in a hot-path persistence comment, but
neither location states the environment (Linux OrbStack VM/GCloud) nor marks it as non-production.

Rule 351129: Benchmark production performance only on Linux environments
CHANGELOG.md[9-16]
src/persistence/aof/pool.rs[285-293]

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

## Issue description
Newly added performance numbers are reported without stating the benchmark environment is Linux-based (or explicitly marking the numbers as non-production).

## Issue Context
Rule 351129 requires that production performance benchmarks be reported only with Linux environment context (OrbStack VM or GCloud). The PR adds the numeric latency tail `2000–3000ms` in user-facing docs/comments without such context.

## Fix Focus Areas
- CHANGELOG.md[9-22]
- src/persistence/aof/pool.rs[285-293]

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


2. Unjustified too_many_arguments allow ✓ Resolved 📘 Rule violation ✧ Quality
Description
A new #[allow(clippy::too_many_arguments)] suppression was added without an explicit justification
comment tied to the lint suppression. This weakens clippy enforcement under supported feature
profiles and violates the requirement that new clippy suppressions be justified and narrowly scoped.
Code

src/shard/coordinator.rs[252]

+#[allow(clippy::too_many_arguments)]
Relevance

⭐⭐⭐ High

Repo enforces justified lint suppressions; prior reviews requested/accepted justification for new
#[allow(...)] usages.

PR-#63
PR-#71

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 209935 requires that any newly introduced clippy suppressions be narrowly scoped and justified.
The PR adds #[allow(clippy::too_many_arguments)] on run_on_owner_persist without a justification
comment explicitly explaining why suppressing this lint is acceptable here.

Rule 209935: Rust code must be clippy-clean under all supported feature profiles
src/shard/coordinator.rs[238-266]

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 `#[allow(clippy::too_many_arguments)]` was introduced without an explicit justification comment explaining why the suppression is necessary.

## Issue Context
Rule 209935 requires clippy cleanliness under supported feature profiles and instructs reviewers to validate that any new `#[allow(clippy::...)]` is narrowly scoped and justified in a comment.

## Fix Focus Areas
- src/shard/coordinator.rs[238-266]

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


3. No-op writes persisted ✓ Resolved 🐞 Bug ≡ Correctness
Description
run_on_owner_persist persists any non-error local execution, including responses that indicate no
mutation (e.g., SET ... NX refusal in COPY), so no-op commands can unnecessarily require a batch
fsync barrier and can fail with AOF_FSYNC_ERR if the AOF writer/barrier path is unavailable. This
is inconsistent with the new DEL/UNLINK logic that explicitly skips persistence when nothing was
removed (n=0).
Code

src/shard/coordinator.rs[R285-296]

+    let resp = run_local(shard_databases, db_index, cached_clock, &cmd, args);
+    if !matches!(resp, Frame::Error(_)) {
+        let serialized = crate::persistence::aof::serialize_command(&Frame::Array(
+            command_parts.to_vec().into(),
+        ));
+        match persist_local_leg(aof_pool, repl_state, my_shard, serialized).await {
+            Ok(needs_barrier) => *local_barrier_pending |= needs_barrier,
+            Err(()) => {
+                return Frame::Error(Bytes::from_static(crate::persistence::aof::AOF_FSYNC_ERR));
+            }
+        }
+    }
Relevance

⭐⭐ Medium

No close historical precedent found on skipping AOF persistence for no-op writes (NX refusal);
correctness tradeoff unclear.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The helper persists on any non-error response, so Frame::Null from SET ... NX (which COPY
interprets as NX refusal and returns 0) will still be serialized and appended, making a no-op
depend on AOF/barrier success. By contrast, the DEL/UNLINK coordinator explicitly checks n > 0
before persisting, demonstrating that persistence is intended to be skipped on no-op results.

src/shard/coordinator.rs[253-297]
src/shard/coordinator.rs[686-717]
src/shard/coordinator.rs[1159-1189]
src/shard/coordinator.rs[520-540]

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

## Issue description
`run_on_owner_persist()` currently appends to the local shard AOF for any non-error response. This includes known no-op outcomes (e.g., `SET ... NX` returning Null, which the COPY coordinator maps to `COPY -> 0`). Persisting these no-ops (a) adds unnecessary AOF traffic + fsync barriers under `appendfsync=always`, and (b) can incorrectly surface `AOF_FSYNC_ERR` for commands that did not change state (because the AOF writer/barrier path can fail even though there was nothing to persist).

## Issue Context
The DEL/UNLINK coordinator paths explicitly avoid persisting when `n==0` (no keys removed), indicating the intended rule is “persist only when a local leg actually mutates state”. The new helper should apply the same principle for other write-like commands whose responses can indicate “no change”.

## Fix Focus Areas
- src/shard/coordinator.rs[253-297]
- src/shard/coordinator.rs[686-717]
- src/shard/coordinator.rs[520-540]

## Suggested fix approach
1. Change `run_on_owner_persist` to persist only when the local execution *actually mutated state*, not merely when it wasn’t an error.
  - Easiest: accept an additional parameter (enum or closure) that decides `should_persist(&resp)` so callers can express their mutation semantics.
  - Alternative (less flexible): pattern-match on `(cmd, args, resp)` inside `run_on_owner_persist` for the specific commands it’s used with (currently BITOP/COPY synthesized writes), e.g.:
    - `SET ... NX`: persist only on `Frame::SimpleString(_)`.
    - `DEL`/`UNLINK`: persist only when `Frame::Integer(n) if n > 0`.
2. Update COPY’s `SET ... NX` local-leg call site to skip persistence when NX refused (since COPY returns 0 and no state changed).
3. Update BITOP’s “delete dest” branch to persist the DEL only when it actually deleted something (optional but consistent with DEL/UNLINK logic).
4. Add/adjust a unit/integration test that exercises the NX-refusal path and asserts it does not enqueue a local-leg append / does not require a barrier.

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


Grey Divider

Qodo Logo

Comment thread CHANGELOG.md Outdated
Comment thread src/shard/coordinator.rs
Comment thread src/shard/coordinator.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: 2

Caution

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

⚠️ Outside diff range comments (1)
src/shard/coordinator.rs (1)

1192-1266: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Local-leg persist failure aborts before remaining shard groups are dispatched.

In the scatter branch, persist_local_leg(...).await (and its early return on Err(())) runs inside the for (shard_id, key_args) in &groups loop, before all groups have been dispatched and before pending_shards is awaited. If my_shard's slice happens before a higher-shard-id group in the BTreeMap ordering and the local AOF enqueue fails, the function returns immediately — the remaining (not-yet-iterated) shard groups never get their spsc_send DEL/UNLINK dispatched at all, while my_shard's own keys were already deleted in memory and any earlier (lower-shard-id) groups were already fire-and-forget dispatched. The client gets a single AOF_FSYNC_ERR, but the actual keyspace ends up partially deleted across shards with no way to tell which keys were affected.

coordinate_mset avoids this by deferring its local-leg persist call until after the full group loop and the pending_shards await loop. coordinate_multi_del_or_exists should follow the same pattern: capture the synthesized local DEL/UNLINK parts during the loop, dispatch every group unconditionally, await all pending_shards, and only then attempt persist_local_leg (returning the error afterward if it fails).

🔧 Proposed restructuring
     let mut total_count: i64 = 0;
     let mut pending_shards: Vec<channel::OneshotReceiver<Vec<Frame>>> = Vec::new();
+    let mut local_delete_parts: Option<Vec<Frame>> = None;

     for (shard_id, key_args) in &groups {
         if *shard_id == my_shard {
             let mut selected = db_index;
             let result = crate::shard::slice::with_shard_db(db_index, |db| {
                 db.refresh_now_from_cache(cached_clock);
                 cmd_dispatch(db, cmd, key_args, &mut selected, db_count)
             });
             if let DispatchResult::Response(Frame::Integer(n)) = result {
                 total_count += n;
                 if is_delete && n > 0 {
                     let mut parts: Vec<Frame> = Vec::with_capacity(key_args.len() + 1);
                     parts.push(Frame::BulkString(Bytes::from(cmd_upper.clone())));
                     parts.extend_from_slice(key_args);
-                    let serialized =
-                        crate::persistence::aof::serialize_command(&Frame::Array(parts.into()));
-                    match persist_local_leg(aof_pool, repl_state, my_shard, serialized).await {
-                        Ok(needs_barrier) => *local_barrier_pending |= needs_barrier,
-                        Err(()) => {
-                            return Frame::Error(Bytes::from_static(
-                                crate::persistence::aof::AOF_FSYNC_ERR,
-                            ));
-                        }
-                    }
+                    local_delete_parts = Some(parts);
                 }
             }
         } else {
             ...
             spsc_send(dispatch_tx, my_shard, *shard_id, msg, spsc_notifiers).await;
             pending_shards.push(reply_rx);
         }
     }

     for reply_rx in pending_shards {
         match reply_rx.recv().await { ... }
     }

+    if let Some(parts) = local_delete_parts {
+        let serialized = crate::persistence::aof::serialize_command(&Frame::Array(parts.into()));
+        match persist_local_leg(aof_pool, repl_state, my_shard, serialized).await {
+            Ok(needs_barrier) => *local_barrier_pending |= needs_barrier,
+            Err(()) => {
+                return Frame::Error(Bytes::from_static(crate::persistence::aof::AOF_FSYNC_ERR));
+            }
+        }
+    }
+
     Frame::Integer(total_count)
🤖 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/coordinator.rs` around lines 1192 - 1266, The scatter path in
coordinate_multi_del_or_exists is returning from persist_local_leg too early
inside the shard loop, which can prevent later shard groups from being
dispatched. Refactor the loop to always finish collecting/disptaching all groups
and awaiting pending_shards first, while capturing the local DEL/UNLINK payload
for my_shard during the loop. Then perform the persist_local_leg call after the
loop (as coordinate_mset does), and only return AOF_FSYNC_ERR afterward if that
persist fails.
🤖 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/server/conn/handler_monoio/mod.rs`:
- Around line 1816-1831: Move the local-leg fsync barrier in
handler_monoio::mod::handle so it runs before any early-return PSYNC handling
and before the blocking-command flush/break path, not only at end-of-batch.
Ensure the same barrier logic that currently drains local_leg_write_idxs and
uses ctx.aof_pool.fsync_barrier(ctx.shard_id) is executed for MSET/MSETNX local
legs before those exits can skip it, so acknowledgements are only serialized
after durability is confirmed.

In `@src/server/conn/handler_sharded/mod.rs`:
- Around line 1719-1735: Batch-end durability barrier is using stale
local_leg_write_idxs after the blocking-command early flush path replaces
responses. In handler_sharded::mod.rs, make sure any pending AOF fsync barrier
for local write legs is resolved before the blocking-commands branch flushes
responses and resets the vector, then clear local_leg_write_idxs so later code
cannot index into the new buffer. Use the existing fsync_barrier logic around
the responses/local_leg_write_idxs handling in the per-frame loop, especially
the blocking-command branch and the final barrier block near the end of batch
processing.

---

Outside diff comments:
In `@src/shard/coordinator.rs`:
- Around line 1192-1266: The scatter path in coordinate_multi_del_or_exists is
returning from persist_local_leg too early inside the shard loop, which can
prevent later shard groups from being dispatched. Refactor the loop to always
finish collecting/disptaching all groups and awaiting pending_shards first,
while capturing the local DEL/UNLINK payload for my_shard during the loop. Then
perform the persist_local_leg call after the loop (as coordinate_mset does), and
only return AOF_FSYNC_ERR afterward if that persist fails.
🪄 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: 4af9ed81-0668-4713-a812-f3d9cb558f92

📥 Commits

Reviewing files that changed from the base of the PR and between eadc8b8 and 1fd4ac7.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • src/persistence/aof/pool.rs
  • src/server/conn/handler_monoio/dispatch.rs
  • src/server/conn/handler_monoio/mod.rs
  • src/server/conn/handler_sharded/mod.rs
  • src/shard/coordinator.rs
  • tests/coordinator_local_leg_durability.rs
  • tests/integration.rs

Comment thread src/server/conn/handler_monoio/mod.rs Outdated
Comment thread src/server/conn/handler_sharded/mod.rs Outdated
…shes (PR #213 review)

Review findings from PR #213 (CodeRabbit + Qodo), all addressed:

1. [CRITICAL, CodeRabbit] The batch-end local-leg barrier could be
   skipped or corrupted by mid-batch early flushes:
   - blocking commands (BLPOP...) flush accumulated responses then
     REPLACE the vec — pending barrier indexes became stale (panic or
     misattributed AOF_FSYNC_ERR onto the blocking response) and the
     flushed +OK escaped without confirmed durability;
   - PSYNC hijack (monoio) and SUBSCRIBE-entry (both runtimes) flush
     early with the same durability leak.
   Fix: new shared::resolve_local_leg_barrier(pool, shard, idxs,
   responses) — always drains, patches AOF_FSYNC_ERR on barrier failure
   — called at the batch end AND before every early flush (blocking,
   SUBSCRIBE entry, PSYNC) in both handlers.

2. [Qodo bug] run_on_owner_persist logged no-op writes (COPY SET..NX
   refusal, DEL of absent dest, PEXPIRE on vanished key) — costing a
   needless barrier fsync and risking AOF_FSYNC_ERR on a command that
   wrote nothing. Fix: persist_if predicate per call site (BITOP forward
   always mutates; DEL only n>0; COPY only :1; SET dst only +OK;
   PEXPIRE only :1).

3. [Qodo rule] #[allow(clippy::too_many_arguments)] now carries its
   justification; CHANGELOG latency numbers now state their Linux/GCE
   measurement context.

Tests: coordinator_local_leg_durability 7/7, full lib suite 3656 pass,
clippy clean both feature sets. The barrier-failure arm of the early
flush paths is not black-box testable (requires an AOF writer dying
mid-pipeline); covered by the shared helper's single code path + the
existing fsync_barrier unit tests.

author: Tin Dang
@pilotspacex-byte pilotspacex-byte merged commit ab85684 into main Jul 6, 2026
10 checks passed
pilotspacex-byte added a commit that referenced this pull request Jul 7, 2026
…mmit (#239)

Under `appendfsync always`, all three connection handlers awaited one
fsync ack PER COMMAND for plain local writes (try_send_append_durable).
A 16-deep pipeline therefore paid 16 serialized fsync round-trips per
connection, while Redis fsyncs once per event-loop iteration — measured
as an 8x SET deficit at P16 (Moon 5.2k vs Redis 44k ops/s, GCE
c3-standard-8; tmp/MOON-VS-REDIS-DURABILITY.md).

The writer-side group commit (one fsync per drained batch) already
existed, and cross-shard writes + coordinator local legs already used
the fire-and-forget + barrier contract from PR #213. This change
extends that contract to plain local writes:

- handler_monoio / handler_sharded: local writes, MOVE, and COPY
  enqueue via send_append_group(); successful responses join
  local_leg_write_idxs and the existing end-of-batch
  resolve_local_leg_barrier() (one fsync_barrier per pipelined batch)
  converts them to AOF_FSYNC_ERR on failure — never a silent +OK.
- handler_single: flush_with_aof_ack collects barrier indexes and
  issues ONE fsync_barrier(0) for the whole flush; the inline
  pre-SUBSCRIBE path and single-shard GRAPH.* WAL-record loop use the
  same pattern.
- The AOF writer processes its channel in order, so an acked
  zero-length AppendSync barrier proves every prior Append durable:
  the H1 fsync-before-ack guarantee is unchanged.
- everysec/no policies unaffected (send_append_group returns Ok(false),
  no barrier joined).

Validation:
- crash_matrix_per_shard_aof --ignored: 3/3 green (SIGKILL under
  appendfsync always still recovers 100% of acked writes).
- flush_with_aof_ack H1 ordering test updated to the batch protocol
  (Append then AppendSync barrier; ack still gates the response).
- fmt, clippy x2 (default + tokio,jemalloc), cargo test --lib x2
  (monoio 3864 / tokio 3143), cargo test --no-run x2: all green.

author: Tin Dang

Co-authored-by: Tin Dang <tindang.ht97@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants