Skip to content

perf(server): appendfsync-always local writes join per-batch group commit#239

Merged
pilotspacex-byte merged 1 commit into
mainfrom
perf/aof-always-local-group-commit
Jul 7, 2026
Merged

perf(server): appendfsync-always local writes join per-batch group commit#239
pilotspacex-byte merged 1 commit into
mainfrom
perf/aof-always-local-group-commit

Conversation

@pilotspacex-byte

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

Copy link
Copy Markdown
Contributor

Summary

Closes the biggest Moon-vs-Redis durability gap: under appendfsync always with pipelining, Moon was 8× slower than Redis (P16 SET: 5.2k vs 44k ops/s, GCE c3-standard-8 — full matrix in tmp/MOON-VS-REDIS-DURABILITY.md).

Root cause: all three connection handlers awaited one fsync ack per command (try_send_append_durable). A 16-deep pipeline paid 16 serialized fsync round-trips per connection; Redis fsyncs once per event-loop iteration. The writer-side group commit (one fsync per drained batch, src/persistence/aof/group_commit.rs) already existed but could only batch across connections — the per-command await defeated it within a connection.

Change

Extend the fire-and-forget + barrier contract (already used by cross-shard writes and coordinator local legs since PR #213) 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() issues ONE fsync_barrier per pipelined batch, converting joined responses 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) per flush; inline pre-SUBSCRIBE and single-shard GRAPH.* WAL-record paths 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 fsync-before-ack (H1) guarantee is unchanged.
  • everysec / no policies unaffected (send_append_group returns Ok(false), no barrier joined).

Test plan

  • crash_matrix_per_shard_aof --ignored: 3/3 green — SIGKILL under appendfsync always still recovers 100% of acked writes (the durability contract this change must not break).
  • flush_with_aof_ack H1 ordering test updated to the batch protocol (one Append + one AppendSync barrier; ack still gates the first response).
  • cargo fmt --check; clippy ×2 (default + tokio,jemalloc); cargo test --lib ×2 (monoio 3864 / tokio 3143); cargo test --no-run ×2.
  • GCE A/B: rerun the always-P16 scenario vs Redis on the same instance (expected 5.2k → ~40k+; results will be posted below before merge).

Summary by CodeRabbit

  • Changed
    • Local writes now use batched durability confirmation, reducing per-command waiting under always-on persistence.
    • If durability confirmation fails, affected commands now return an error instead of appearing successful.
    • Subscription-related transitions and graph write operations now follow the same batched confirmation behavior.
    • Response delivery now waits for durability confirmation before being sent, keeping results consistent with write safety.

…mmit

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
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

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

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

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR changes AOF durability behavior under appendfsync=always across three connection handlers (monoio, sharded, single). Local writes now enqueue via send_append_group fire-and-forget and confirm durability once per pipeline batch via a single fsync_barrier, replacing per-command try_send_append_durable awaits. Barrier failures rewrite affected responses to errors instead of allowing silent success. Tests and changelog updated to reflect the new protocol.

Changes

Group-commit AOF durability refactor

Layer / File(s) Summary
flush_with_aof_ack and GRAPH WAL group commit
src/server/conn/handler_single.rs
flush_with_aof_ack, the SUBSCRIBE/PSUBSCRIBE ordering fix, and the GRAPH WAL enqueue path all switch from per-entry try_send_append_durable awaits to send_append_group enqueue plus a single fsync_barrier, patching response slots on failure; test comments and mock writer expectations updated to match the new Append-then-AppendSync ordering.
handler_monoio: MOVE/COPY and generic write path
src/server/conn/handler_monoio/mod.rs
MOVE/COPY local write paths await send_append_group instead of try_send_append_durable; a new aof_barrier_pending flag gates whether a response index is added to local_leg_write_idxs, ensuring barrier failures never overwrite an existing error response.
handler_sharded: MOVE/COPY and batch post-processing
src/server/conn/handler_sharded/mod.rs
MOVE/COPY durability handling adopts the same send_append_group branching, and batch post-processing replaces the “durable path” logic with aof_barrier_pending/aof_failed, deferring barrier-join registration until after response conversion.
Changelog entry
CHANGELOG.md
Unreleased entry documents the per-batch group-commit change, barrier-failure behavior, and updated ordering test.

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

Possibly related PRs

  • pilotspace/moon#175: Introduces the AofWriterPool::fsync_barrier mechanism and AppendSync barrier/ack framing that this PR's handlers now depend on.
  • pilotspace/moon#178: Adds the AOF WAL group-commit batching implementation (group_commit.rs) that backs the send_append_group calls used here.
  • pilotspace/moon#213: Introduces send_append_group, resolve_local_leg_barrier, and local_leg_write_idxs plumbing that this PR builds upon in the handlers.
🚥 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 summarizes the main change: appendfsync-always local writes now use per-batch group commit.
Description check ✅ Passed The description covers summary, performance impact, and testing, but it omits the template’s Checklist and Notes sections.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/aof-always-local-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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/server/conn/handler_single.rs (1)

75-75: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid allocating barrier_idxs on the AOF flush path.

flush_with_aof_ack runs on the write response path, and Vec::new() adds a per-flush hot-path allocation. Use a stack-backed SmallVec or reusable scratch storage.

♻️ Proposed fix
-    let mut barrier_idxs: Vec<usize> = Vec::new();
+    let mut barrier_idxs: smallvec::SmallVec<[usize; 64]> = smallvec::SmallVec::new();

As per coding guidelines, src/**/*.rs hot paths should avoid Vec::new() and prefer preallocated buffers or SmallVec.

🤖 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/server/conn/handler_single.rs` at line 75, The hot path in
flush_with_aof_ack is allocating barrier_idxs with Vec::new(), which should be
avoided on the AOF flush path. Replace the Vec-based scratch storage with a
stack-backed SmallVec or another reusable preallocated buffer, and keep the
change localized around the barrier_idxs usage in
handler_single::flush_with_aof_ack so the write response path does not incur
per-flush heap allocations.

Source: Coding guidelines

src/server/conn/handler_sharded/mod.rs (1)

1224-1226: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Move aof_bytes instead of cloning it in MOVE/COPY.

These branches continue after handling AOF, so aof_bytes can be consumed and passed directly to send_append_group.

♻️ Proposed fix
-                                if let Some(ref bytes) = aof_bytes {
+                                if let Some(bytes) = aof_bytes {
                                     if let Some(ref pool) = ctx.aof_pool {
                                         let lsn = aof::AofWriterPool::issue_append_lsn(&ctx.repl_state, ctx.shard_id, bytes.len());
                                         match pool
-                                            .send_append_group(ctx.shard_id, lsn, bytes.clone())
+                                            .send_append_group(ctx.shard_id, lsn, bytes)
                                             .await

Apply the same pattern in the COPY branch.

As per coding guidelines, src/**/*.rs hot paths should avoid clone() and prefer borrowing or moving.

Also applies to: 1282-1284

🤖 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/server/conn/handler_sharded/mod.rs` around lines 1224 - 1226, The
MOVE/COPY AOF path in `handler_sharded::mod` is cloning `aof_bytes` before
`send_append_group`, even though those branches terminate with `continue` and
can move the buffer instead. Update the MOVE branch to pass `aof_bytes` directly
into `send_append_group`, and apply the same change in the COPY branch so the
buffer is consumed rather than cloned. Use the existing `send_append_group` call
sites in this match block as the target for the fix.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/server/conn/handler_sharded/mod.rs`:
- Around line 1224-1226: The MOVE/COPY AOF path in `handler_sharded::mod` is
cloning `aof_bytes` before `send_append_group`, even though those branches
terminate with `continue` and can move the buffer instead. Update the MOVE
branch to pass `aof_bytes` directly into `send_append_group`, and apply the same
change in the COPY branch so the buffer is consumed rather than cloned. Use the
existing `send_append_group` call sites in this match block as the target for
the fix.

In `@src/server/conn/handler_single.rs`:
- Line 75: The hot path in flush_with_aof_ack is allocating barrier_idxs with
Vec::new(), which should be avoided on the AOF flush path. Replace the Vec-based
scratch storage with a stack-backed SmallVec or another reusable preallocated
buffer, and keep the change localized around the barrier_idxs usage in
handler_single::flush_with_aof_ack so the write response path does not incur
per-flush heap allocations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 803afc9a-6bd3-450a-b1e5-bbde78d2768a

📥 Commits

Reviewing files that changed from the base of the PR and between 2c41609 and 27fbf35.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • src/server/conn/handler_monoio/mod.rs
  • src/server/conn/handler_sharded/mod.rs
  • src/server/conn/handler_single.rs

@pilotspacex-byte

Copy link
Copy Markdown
Contributor Author

GCE A/B results (c3-standard-8, pd-ssd, us-central1-a, Redis 7.0.15, 3 alternated reps, fresh server+dir per scenario)

redis-benchmark -t set -c 8 -n 300000 -r 100000 -d 64 [-P 16], Moon --shards 2 --appendonly yes --appendfsync always, Redis appendonly yes appendfsync always:

workload moon-base (pre-change, #238 tip) moon-gc (this PR) redis moon-gc vs base moon-gc vs redis
SET p1 3,383 3,399 3,438 1.00× 0.99×
SET p16 5,680 39,621 46,547 6.98× 0.85×
GET p1 134,789 134,830 132,067 1.00× 1.02×
GET p16 1,021,500 1,123,713 1,041,842 1.10× 1.08×

The 8× always-P16 deficit (0.12×) is closed to 0.85× of Redis. p1 is fsync-device-bound for all engines (~3.4k = disk fsync rate, parity). Remaining 15% gap at p16 is the next hardening target.

Durability re-verified post-change: crash_matrix_per_shard_aof 3/3 green (SIGKILL under appendfsync always recovers 100% of acked writes).

@pilotspacex-byte pilotspacex-byte merged commit 4788208 into main Jul 7, 2026
11 checks passed
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