Skip to content

perf(replication): per-write fanout gate — parking_lot migration + FANOUT_HINT probe fix + dead PSYNC path deletion (tasks #70, #72)#332

Merged
pilotspacex-byte merged 2 commits into
mainfrom
worktree-agent-ad25fc066a7440219
Jul 14, 2026
Merged

perf(replication): per-write fanout gate — parking_lot migration + FANOUT_HINT probe fix + dead PSYNC path deletion (tasks #70, #72)#332
pilotspacex-byte merged 2 commits into
mainfrom
worktree-agent-ad25fc066a7440219

Conversation

@pilotspacex-byte

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

Copy link
Copy Markdown
Contributor

Summary

Deep-review Findings 2 & 4 (pre-soak). Two commits:

Commit 1 (tasks #70 + #72):

Commit 2 (orchestrator-review-caught regression fix): splitting allocation from hint activation opened a REPLCONF→PSYNC window where handler-inline writes advanced shard offsets via bare issue_append_lsn with no matching backlog append → permanent byte/offset skew → FULLRESYNC catch-up / partial resync could serve wrong bytes or silently skip (acked-write loss on replica attach under load — the exact 24h-soak shape). Fix: ReplicationBacklog::realign_to + ReplicationState::realign_backlog, wired into try_handle_psync and both spsc registration arms (owning-shard-thread, before cut/push_offset capture). Skewed buffers reset-to-empty at the real offset; stale partial-resync offsets fail safe to full resync.

Verification

  • RED/GREEN: test_ensure_backlogs_allocated_does_not_activate_fanout_hint (RED via bug reintroduction), test_realign_backlog_fixes_skew_after_hint_false_window + test_realign_backlog_then_append_reads_back_exact_record (RED via neutered realign), + 3 more.
  • cargo test --lib: 4307/4307 monoio, 3490/3490 tokio. fmt + both clippy matrices clean.
  • Replication gates (fresh MOON_BIN, --ignored --test-threads=1): replication_hardening 6/6, replication_multishard 9/9, replication_planes 11/11.
  • VM A/B (moon-dev, interleaved n=8, pinned): probe-leg SET +5.0% (p1) / +16.9% (p16) — the targeted leg; virgin/attached legs within the VM noise floor (GET control swung ±8-20% on identical code). Lock-across-await audit: none in diff.

Pre-soak: this fix must be in the soaked binary (REPL-SOAK-01).

Summary by CodeRabbit

  • Bug Fixes
    • Improved replication fanout performance and reduced contention during write-heavy workloads.
    • Fixed replication resynchronization issues during the REPLCONF-to-PSYNC transition.
    • Improved backlog alignment to prevent stale or skipped data during catch-up.
    • Removed unreachable replication paths and strengthened error handling during replica synchronization.
  • Compatibility
    • Replication handshake behavior and wire protocol remain unchanged, aside from corrected fanout hint gating.
  • Tests
    • Added coverage for fanout activation, backlog realignment, and replication scenarios.

…x; chore(replication): delete dead master PSYNC path (tasks #70, #72)

Two pre-soak defects blocking the v0.7.0 tag, fixed together since both
touch ReplicationState locking on the per-write hot path.

Task #70 — per-write replication fanout gate perf debt (4 sub-fixes):

1. Migrated ReplicationState's outer lock from std::sync::RwLock to
   parking_lot::RwLock across 27 call sites (admin/metrics_setup.rs,
   cluster/gossip.rs, command/server_admin.rs, main.rs,
   persistence/aof/pool.rs, replication/{reason_del,replica,master,
   state}.rs, scripting/bridge.rs, server/conn/*, shard/*). The inner
   per_shard_backlogs Mutex was already parking_lot; the outer lock was
   the odd one out, forcing .read().unwrap() / if-let-Ok poisoning
   dances on the write path. parking_lot doesn't poison: read()/write()
   return the guard directly, try_read()/try_write() return Option not
   Result (fixed 3 sites that assumed Result).
2. ensure_backlogs_allocated() — called from try_handle_replconf on ANY
   bare REPLCONF, including probes and failed handshakes — used to call
   mark_fanout_active() unconditionally. FANOUT_HINT is a sticky,
   never-cleared process-global AtomicBool, so one stray REPLCONF
   permanently taxed every write with the fanout-active gate check even
   on a server that never completes PSYNC. Split allocation (still
   load-bearing, kept in ensure_backlogs_allocated) from hint activation
   (moved to try_handle_psync, on an actual PSYNC/replica registration).
3. record_local_write / record_local_write_db collapsed from up to 3-4
   separate repl_state.read() acquisitions per write down to ONE guard,
   taken at the top of record_local_write_db and threaded through.
   replication_fanout_active stays a separate short-lived gate lock by
   design: some external call sites (handler_monoio/mod.rs MOVE/COPY)
   reach an .await between the gate check and the next repl-state touch,
   and Rust drops guards at end of lexical scope (not last-use), so
   folding it into one long-lived guard would hold a lock across .await.
   Net: at most 2 acquisitions per write (1 gate + 1 write), down from
   4-5.
4. Added #[inline] to record_local_write, record_local_write_db, and
   replication_fanout_active, matching the try_handle_* convention.

Task #72 — delete dead master PSYNC path:

handle_psync_on_master (both tokio and monoio variants) and
register_replica_with_shards in src/replication/master.rs were
unreachable — every live PSYNC handler goes through
handle_psync_inline_single_shard / handle_psync_inline_multi_shard from
shard/conn_accept.rs instead. The dead pair also had a latent broken
WAIT implementation: it initialized ack_offsets but never spawned
ack_read_loop to drain replica ACKs into them, so wait_for_replicas
against that path would block forever. Deleted both variants plus
evaluate_psync_shared (only reachable from the deleted code); kept
backlog_bytes_from, still used by the live send_backlog_range.

Testing:

- Two new unit tests in replication::state::tests cover the FANOUT_HINT
  fix with delta-based assertions (robust against shared-binary
  test-order contamination):
  test_ensure_backlogs_allocated_does_not_activate_fanout_hint (RED on
  the pre-fix code, verified by temporarily reintroducing the bare
  mark_fanout_active() call) and test_mark_fanout_active_sets_hint.
- Lock-across-await audit: every touched call site was checked by
  reading the actual code, not just grepping. The one risk found
  (handler_monoio/mod.rs MOVE/COPY awaiting pool.send_append_group()
  after the fanout gate check) is why replication_fanout_active was
  kept as its own short-lived guard instead of being folded into the
  record_local_write_db guard. No parking_lot guard is held across an
  .await point anywhere in the touched code.
- fmt clean; clippy -D warnings clean on both default (monoio) and
  --no-default-features --features runtime-tokio,jemalloc.
- Full cargo test --lib green on both feature sets (4303 passed / 1
  ignored monoio; 3484 passed / 1 ignored tokio).
- replication_hardening (6/6), replication_multishard (9/9),
  replication_planes (11/11) integration suites green, MOON_BIN pinned
  to a fresh release-fast monoio binary, --ignored --test-threads=1.
  replication_test has 0 tests (empty suite, pre-existing).
  replication:: unit-test module green under both runtimes (67/67
  monoio-filtered, full set included in the --lib totals above) —
  master-side PSYNC (and thus the suites above) is monoio-only by
  pre-existing, unmodified design (try_handle_psync_unsupported under
  tokio); the "both runtimes" gate is satisfied at the unit-test level.
- Rebased onto origin/main (108a01e) to pick up PR #329's
  seed_master_offset shard-0 seeding fix, which touched the same two
  files (master.rs, state.rs); auto-merged cleanly with no conflict
  markers, re-verified by re-running every gate above post-rebase.

No wire-protocol or observable behavior change beyond the hint gating:
REPLCONF -> PSYNC handshake sequence is unchanged.

author: Tin Dang
…t regression on task #70)

Orchestrator review of eb4d47b3 found a correctness regression in the
FANOUT_HINT fix (task #70.2). Splitting backlog allocation from hint
activation opened a REPLCONF -> PSYNC window during which a bare
REPLCONF could allocate + seed a shard's ReplicationBacklog at its
offset at that instant, then -- with FANOUT_HINT still false -- every
local write in the window advanced the shard's real offset counter via
the bare-issue_lsn branch with NO matching backlog append (the KV
wal_append_and_fanout path is unaffected; only the handler-inline
issue_append_lsn branch skews). The backlog's end_offset silently
drifted stale relative to the real counter for as long as the window
stayed open -- an AOF-enabled master under continuous write load with
periodic replica kill-9/reconnect, exactly the v0.7.0 24h soak's shape
(shards=4, appendfsync always, reconnects every 12min). Any
snapshot/cut/push-offset captured from that backlog after activation
would be range-inconsistent with it: FULLRESYNC catch-up and partial
resync could then read wrong bytes, or silently skip catch-up
(bytes_from returning None for an offset the backlog didn't yet know
about) -- acked-write loss / replica corruption.

Fix: restore the invariant "backlog byte positions are trustworthy
from the last activation realign onward" by realigning
already-allocated backlogs at every activation site, before any
snapshot/cut/push offset is captured from them.

1. ReplicationBacklog::realign_to(offset) (src/replication/backlog.rs):
   resets the buffer to empty, reseeded at offset -- the same state as
   new_at(capacity, offset). No-op if already aligned. A skewed
   buffer's contents are already useless (they cover positions the
   real counter never confirmed), so dropping them is correct: any
   partial-resync request for an offset below the new start_offset
   falls through bytes_from returning None, and the caller's existing
   fail-safe -- full resync -- kicks in. Never serves wrong bytes.
2. ReplicationState::realign_backlog(shard_id) (src/replication/state.rs):
   reads the shard's current real offset and calls realign_to on that
   shard's backlog if allocated. Race-free only when called from the
   shard thread that owns shard_id's own offset advances -- true for
   every call site below.
3. Wired into try_handle_psync (src/server/conn/handler_monoio/dispatch.rs),
   right after mark_fanout_active(): realigns ctx.shard_id's backlog.
   Race-free at shards=1, where the connection task IS the owning
   shard's own event-loop thread (documented in the code -- this call
   does NOT cover other shards on a multi-shard master; that's handled
   by point 4 below, which runs per-shard on each shard's own thread).
4. Wired into the RegisterReplica and PrepareReplicaSync arms
   (src/shard/spsc_handler.rs): both arms already had an
   `if guard.is_none() { allocate }` pattern; changed to
   `match guard.as_mut() { None => allocate, Some(backlog) => realign }`
   so an already-allocated backlog is realigned instead of silently
   left skewed, before cut/push_offset/shard_offset are captured
   further down in the same arm.

Testing (RED/GREEN):

- backlog.rs: test_realign_to_resets_skewed_backlog,
  test_realign_to_noop_when_already_aligned -- low-level buffer-reset
  behavior of realign_to itself.
- state.rs: test_realign_backlog_fixes_skew_after_hint_false_window --
  reproduces the exact bug: ensure_backlogs_allocated (REPLCONF), then
  issue_lsn(shard, 100/150/50) with no append (the hint-false window),
  asserts the skew is real (backlog end_offset stuck at 0 while the
  real offset is 300, and bytes_from(300) returns None on the
  unrealigned backlog), then calls realign_backlog and asserts
  end_offset now matches the real offset and bytes_from(300) returns
  Some(empty). test_realign_backlog_then_append_reads_back_exact_record
  is the integration-shaped companion: allocate, issue_lsn, activate,
  append one record via the normal backlog.append +
  increment_shard_offset pairing, assert bytes_from(pre-append offset)
  returns exactly that record's bytes.
- RED verified by temporarily neutering realign_backlog's body
  (#[cfg(any())] stub) and confirming both new state.rs tests fail
  (end_offset stuck at 0 instead of 300; the post-append record
  unreadable at its own pre-append offset), then reverted.
- GREEN: all 4 new tests pass, plus the full replication:: unit module
  (71/71) and full cargo test --lib (4307/4307 monoio,
  3490/3490 tokio, both 0 failed).

Gates:

- fmt clean.
- clippy -D warnings clean, both default (monoio) and
  --no-default-features --features runtime-tokio,jemalloc.
- cargo test --lib green both feature sets (4307 passed / 1 ignored
  monoio; 3490 passed / 1 ignored tokio; 0 failed either).
- replication_hardening (6/6), replication_multishard (9/9),
  replication_planes (11/11) integration suites green against a fresh
  MOON_BIN built from this fix, --ignored --test-threads=1.

No VM A/B re-run: the realign call sites are activation-time-only
(PSYNC arrival, replica registration), off the per-write hot path the
prior A/B measured -- perf surface is unchanged by this fix.

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 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Replication state now uses parking_lot::RwLock, replication writes reuse fewer guards, fanout activation occurs at PSYNC, backlogs can be realigned, and obsolete master-side PSYNC paths are removed while inline PSYNC flows remain.

Changes

Replication state lock migration

Layer / File(s) Summary
Lock migration and runtime plumbing
src/admin/..., src/cluster/..., src/server/..., src/shard/..., src/replication/replica.rs, src/scripting/bridge.rs
Replication state wrappers and accessors use parking_lot::RwLock; poisoning-related branches and unwraps are removed across metrics, persistence, gossip, connection handlers, replica tasks, scripting, and shard wiring.

Fanout gating and backlog alignment

Layer / File(s) Summary
Fanout activation and write-path locking
src/replication/state.rs, src/server/conn/handler_monoio/*, src/replication/backlog.rs
Backlog allocation no longer activates FANOUT_HINT; PSYNC activates the hint and realigns shard backlogs, while local replication writes reuse one state guard.
Backlog coordination and tests
src/shard/spsc_handler.rs, src/replication/backlog.rs, src/replication/state.rs, CHANGELOG.md
Backlog realignment is applied during replica registration and sync preparation, with unit tests covering skew correction and exact record reads.

Inline PSYNC lifecycle cleanup

Layer / File(s) Summary
Inline PSYNC handlers and replica lifecycle
src/replication/master.rs, src/server/conn/handler_monoio/mod.rs, CHANGELOG.md
Unreachable master-side PSYNC handlers and registration wiring are deleted; inline PSYNC retains snapshot transfer, replica registration, ACK tracking, cleanup, and wait behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant Replica
  participant try_handle_psync
  participant ReplicationState
  participant ReplicationBacklog
  participant handle_psync_inline_multi_shard
  participant ShardMessage

  Replica->>try_handle_psync: PSYNC
  try_handle_psync->>ReplicationState: ensure_backlogs_allocated()
  try_handle_psync->>ReplicationState: mark_fanout_active()
  try_handle_psync->>ReplicationState: realign_backlog(shard_id)
  ReplicationState->>ReplicationBacklog: realign_to(current_offset)
  try_handle_psync->>handle_psync_inline_multi_shard: start snapshot and live stream
  handle_psync_inline_multi_shard->>ShardMessage: PrepareReplicaSync
  ShardMessage->>ReplicationState: register replica
Loading

Possibly related PRs

Suggested reviewers: tindang97

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main changes: replication lock migration, FANOUT_HINT fix, and dead PSYNC path removal.
Description check ✅ Passed The description covers the summary, verification, performance impact, and notes; only the template's checklist format is not explicitly followed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-agent-ad25fc066a7440219

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.

Caution

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

⚠️ Outside diff range comments (2)
src/replication/reason_del.rs (1)

248-305: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Consolidate lock acquisitions to reuse a single state guard per write.

The PR objective and AI summary highlight that "replication write recording reuses a single state guard" and "reduces per-write lock acquisitions." However, push_record_db continues to acquire the parking_lot::RwLock read guard multiple times per write path when needs_select is true (once for needs_select, and twice more via the push_record calls).

Refactoring to read the global state once and pass the reference down significantly reduces lock contention on the hot path.

♻️ Proposed refactor to consolidate lock acquisitions
-fn push_record_db(
-    repl_state: &Option<std::sync::Arc<parking_lot::RwLock<ReplicationState>>>,
-    shard_id: usize,
-    num_shards: usize,
-    db: usize,
-    bytes: Bytes,
-) {
-    if num_shards > 1 {
-        let select = crate::persistence::aof::serialize_select_record(db);
-        let mut combined = Vec::with_capacity(select.len() + bytes.len());
-        combined.extend_from_slice(&select);
-        combined.extend_from_slice(&bytes);
-        push_record(repl_state, shard_id, Bytes::from(combined));
-        return;
-    }
-    let needs_select = repl_state.as_ref().is_some_and(|rs| {
-        rs.read().stream_db.get(shard_id).is_some_and(|slot| {
-            if slot.load(std::sync::atomic::Ordering::Relaxed) != db as i64 {
-                slot.store(db as i64, std::sync::atomic::Ordering::Relaxed);
-                true
-            } else {
-                false
-            }
-        })
-    });
-    if needs_select {
-        push_record(
-            repl_state,
-            shard_id,
-            crate::persistence::aof::serialize_select_record(db),
-        );
-    }
-    push_record(repl_state, shard_id, bytes);
-}
-
-fn push_record(
-    repl_state: &Option<std::sync::Arc<parking_lot::RwLock<ReplicationState>>>,
-    shard_id: usize,
-    bytes: Bytes,
-) {
-    let mut end_offset = u64::MAX;
-    if let Some(rs) = repl_state.as_ref() {
-        let g = rs.read();
-        if let Some(slot) = g.per_shard_backlogs.get(shard_id) {
-            if let Some(backlog) = slot.lock().as_mut() {
-                backlog.append(&bytes);
-            }
-        }
-        end_offset = g.increment_shard_offset(shard_id, bytes.len() as u64);
-    }
-    crate::shard::self_msg::push(crate::shard::dispatch::ShardMessage::ReplicaLiveFanout {
-        bytes,
-        end_offset,
-    });
-}
+fn append_to_backlog(
+    g: &ReplicationState,
+    shard_id: usize,
+    bytes: &Bytes,
+) -> u64 {
+    if let Some(slot) = g.per_shard_backlogs.get(shard_id) {
+        if let Some(backlog) = slot.lock().as_mut() {
+            backlog.append(bytes);
+        }
+    }
+    g.increment_shard_offset(shard_id, bytes.len() as u64)
+}
+
+fn push_record_db(
+    repl_state: &Option<std::sync::Arc<parking_lot::RwLock<ReplicationState>>>,
+    shard_id: usize,
+    num_shards: usize,
+    db: usize,
+    bytes: Bytes,
+) {
+    if num_shards > 1 {
+        let select = crate::persistence::aof::serialize_select_record(db);
+        let mut combined = Vec::with_capacity(select.len() + bytes.len());
+        combined.extend_from_slice(&select);
+        combined.extend_from_slice(&bytes);
+        push_record(repl_state, shard_id, Bytes::from(combined));
+        return;
+    }
+    
+    let mut messages = Vec::with_capacity(2);
+    if let Some(rs) = repl_state.as_ref() {
+        let g = rs.read();
+        let needs_select = g.stream_db.get(shard_id).is_some_and(|slot| {
+            if slot.load(std::sync::atomic::Ordering::Relaxed) != db as i64 {
+                slot.store(db as i64, std::sync::atomic::Ordering::Relaxed);
+                true
+            } else {
+                false
+            }
+        });
+        
+        if needs_select {
+            let select_bytes = Bytes::from(crate::persistence::aof::serialize_select_record(db));
+            let end_offset = append_to_backlog(&g, shard_id, &select_bytes);
+            messages.push((select_bytes, end_offset));
+        }
+        let end_offset = append_to_backlog(&g, shard_id, &bytes);
+        messages.push((bytes, end_offset));
+    } else {
+        messages.push((bytes, u64::MAX));
+    }
+    
+    for (b, offset) in messages {
+        crate::shard::self_msg::push(crate::shard::dispatch::ShardMessage::ReplicaLiveFanout {
+            bytes: b,
+            end_offset: offset,
+        });
+    }
+}
+
+fn push_record(
+    repl_state: &Option<std::sync::Arc<parking_lot::RwLock<ReplicationState>>>,
+    shard_id: usize,
+    bytes: Bytes,
+) {
+    let end_offset = if let Some(rs) = repl_state.as_ref() {
+        append_to_backlog(&rs.read(), shard_id, &bytes)
+    } else {
+        u64::MAX
+    };
+    
+    crate::shard::self_msg::push(crate::shard::dispatch::ShardMessage::ReplicaLiveFanout {
+        bytes,
+        end_offset,
+    });
+}
🤖 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/replication/reason_del.rs` around lines 248 - 305, Refactor
push_record_db and push_record to acquire the ReplicationState read guard once
per write and reuse it for stream database selection, backlog appends, and
offset advancement. Pass the existing guard or state reference through both
push_record calls, including the needs_select path, while preserving the current
selection and fanout ordering.
src/server/conn/handler_monoio/dispatch.rs (1)

582-644: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fanout activation + backlog realign happen before PSYNC args are validated.

mark_fanout_active() (line 597) and g.realign_backlog(ctx.shard_id) (line 607) run inside the block at lines 582-608, which executes before cmd_args[0]/cmd_args[1] are validated as well-formed repl_id/offset (lines 609-644). A PSYNC with exactly 2 args but malformed content (wrong Frame variant, non-UTF8 or non-numeric offset) still triggers the side effects, then fails validation and returns an error.

Since FANOUT_HINT is sticky and never cleared (per the Task #70 doc comment two lines above), a single malformed-but-correctly-sized PSYNC frame permanently activates the fanout tax for the process lifetime — exactly the regression this PR's fix was meant to prevent, just via a different trigger (bad PSYNC content instead of bare REPLCONF). It can also discard an already-aligned backlog needed by another connected replica's future partial resync (forcing an unnecessary full resync), though realign_to's described no-op-when-aligned behavior limits this to the skewed case.

Move the repl_id/offset parsing (currently at 609-644) above the side-effecting block so only a syntactically valid PSYNC can activate fanout/realign the backlog.

🐛 Proposed fix: validate before mutating state
     let Some(ref rs) = ctx.repl_state else {
         responses.push(Frame::Error(Bytes::from_static(
             b"ERR replication is not enabled on this server",
         )));
         return None;
     };
-    {
-        let g = rs.read();
-        let is_master = matches!(g.role, crate::replication::state::ReplicationRole::Master);
-        if !is_master {
-            responses.push(Frame::Error(Bytes::from_static(
-                b"ERR PSYNC is only valid on a master",
-            )));
-            return None;
-        }
-        g.ensure_backlogs_allocated();
-        crate::replication::state::mark_fanout_active();
-        g.realign_backlog(ctx.shard_id);
-    }
     let repl_id = match &cmd_args[0] {
         Frame::BulkString(b) | Frame::SimpleString(b) => String::from_utf8_lossy(b).into_owned(),
         _ => {
             responses.push(Frame::Error(Bytes::from_static(
                 b"ERR PSYNC: invalid replid",
             )));
             return None;
         }
     };
     let offset_bytes = match &cmd_args[1] {
         Frame::BulkString(b) | Frame::SimpleString(b) => b.as_ref(),
         _ => {
             responses.push(Frame::Error(Bytes::from_static(
                 b"ERR PSYNC: invalid offset",
             )));
             return None;
         }
     };
     let offset_str = match std::str::from_utf8(offset_bytes) {
         Ok(s) => s,
         Err(_) => {
             responses.push(Frame::Error(Bytes::from_static(
                 b"ERR PSYNC: offset must be an integer",
             )));
             return None;
         }
     };
     let offset: i64 = match offset_str.parse() {
         Ok(n) => n,
         Err(_) => {
             responses.push(Frame::Error(Bytes::from_static(
                 b"ERR PSYNC: offset must be an integer",
             )));
             return None;
         }
     };
+    {
+        let g = rs.read();
+        let is_master = matches!(g.role, crate::replication::state::ReplicationRole::Master);
+        if !is_master {
+            responses.push(Frame::Error(Bytes::from_static(
+                b"ERR PSYNC is only valid on a master",
+            )));
+            return None;
+        }
+        g.ensure_backlogs_allocated();
+        crate::replication::state::mark_fanout_active();
+        g.realign_backlog(ctx.shard_id);
+    }
     Some((repl_id, offset))
 }
🤖 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_monoio/dispatch.rs` around lines 582 - 644, Move the
repl_id and offset parsing/validation currently following the master-role block
to occur before mark_fanout_active and g.realign_backlog in the PSYNC handler.
Ensure malformed frame variants, non-UTF8 repl IDs or offsets, and non-numeric
offsets return their existing errors without activating fanout or mutating the
backlog; preserve the existing side effects for syntactically valid PSYNC
commands.
🧹 Nitpick comments (2)
src/replication/state.rs (1)

491-507: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared record_local_write core logic to avoid drift between the two copies. Both sites implement the identical backlog-append → increment_shard_offsetReplicaLiveFanout-push sequence, differing only in how the ReplicationState guard is obtained; a future fix to one is easy to miss in the other.

  • src/replication/state.rs#L491-L507: extract the body (after let g = repl_state_arc.read();) into a shared fn record_local_write_locked(g: &ReplicationState, shard_id: usize, bytes: Bytes) and call it here.
  • src/server/conn/handler_monoio/ft.rs#L92-L110: replace this function's body with a direct call to the new record_local_write_locked helper from state.rs.
🤖 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/replication/state.rs` around lines 491 - 507, Extract the shared
backlog-append, shard-offset-increment, and ReplicaLiveFanout dispatch sequence
into record_local_write_locked in src/replication/state.rs:491-507, accepting
&ReplicationState, shard_id, and Bytes. Update record_local_write_global there
to obtain its read guard and call the helper, and replace the duplicate
implementation in src/server/conn/handler_monoio/ft.rs:92-110 with a direct call
to the shared helper, preserving each function’s existing guard acquisition.
src/shard/spsc_handler.rs (1)

383-424: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Both dispatch functions have grown well past the file/function-size guideline. Root cause is the same in both files: a single monolithic match/if-arm dispatcher that keeps absorbing every new command/message type instead of being split into submodules, pushing both files far beyond the 1500-line file limit (and the function itself past the 1000-line read/write-implementation limit).

  • src/shard/spsc_handler.rs#L383-L424: handle_shard_message_shared's match body runs to ~line 3046 in a file that is itself ~3047 lines; split per-ShardMessage-variant handling into dedicated submodules (mirrors how command-group files are expected to be split per the guideline).
  • src/server/conn/handler_single.rs#L137-L154: handle_connection's inline dispatch loop runs to ~line 2767; extract the connection-level intercepts (AUTH/HELLO/CLIENT/REPLICAOF/etc.) and the read/write dispatch runs into separate helper modules.

Not a blocker for this PR (neither file was restructured here, and the specific changes reviewed above are correct), but worth scheduling before either file grows further.

🤖 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/spsc_handler.rs` around lines 383 - 424, Refactor the oversized
dispatchers into dedicated helper modules: in src/shard/spsc_handler.rs lines
383-424, split handle_shard_message_shared’s ShardMessage-variant handling by
message type; in src/server/conn/handler_single.rs lines 137-154, extract
handle_connection’s connection-level intercepts and read/write dispatch into
separate helpers. Preserve existing behavior while keeping each dispatcher and
file within the project size guidelines.

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.

Outside diff comments:
In `@src/replication/reason_del.rs`:
- Around line 248-305: Refactor push_record_db and push_record to acquire the
ReplicationState read guard once per write and reuse it for stream database
selection, backlog appends, and offset advancement. Pass the existing guard or
state reference through both push_record calls, including the needs_select path,
while preserving the current selection and fanout ordering.

In `@src/server/conn/handler_monoio/dispatch.rs`:
- Around line 582-644: Move the repl_id and offset parsing/validation currently
following the master-role block to occur before mark_fanout_active and
g.realign_backlog in the PSYNC handler. Ensure malformed frame variants,
non-UTF8 repl IDs or offsets, and non-numeric offsets return their existing
errors without activating fanout or mutating the backlog; preserve the existing
side effects for syntactically valid PSYNC commands.

---

Nitpick comments:
In `@src/replication/state.rs`:
- Around line 491-507: Extract the shared backlog-append,
shard-offset-increment, and ReplicaLiveFanout dispatch sequence into
record_local_write_locked in src/replication/state.rs:491-507, accepting
&ReplicationState, shard_id, and Bytes. Update record_local_write_global there
to obtain its read guard and call the helper, and replace the duplicate
implementation in src/server/conn/handler_monoio/ft.rs:92-110 with a direct call
to the shared helper, preserving each function’s existing guard acquisition.

In `@src/shard/spsc_handler.rs`:
- Around line 383-424: Refactor the oversized dispatchers into dedicated helper
modules: in src/shard/spsc_handler.rs lines 383-424, split
handle_shard_message_shared’s ShardMessage-variant handling by message type; in
src/server/conn/handler_single.rs lines 137-154, extract handle_connection’s
connection-level intercepts and read/write dispatch into separate helpers.
Preserve existing behavior while keeping each dispatcher and file within the
project size guidelines.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 35798bcc-d199-4c62-9de2-c3e57864ad01

📥 Commits

Reviewing files that changed from the base of the PR and between 9dadd70 and 34ac7e8.

📒 Files selected for processing (29)
  • CHANGELOG.md
  • src/admin/metrics_setup.rs
  • src/cluster/gossip.rs
  • src/command/server_admin.rs
  • src/main.rs
  • src/persistence/aof/pool.rs
  • src/replication/backlog.rs
  • src/replication/master.rs
  • src/replication/reason_del.rs
  • src/replication/replica.rs
  • src/replication/state.rs
  • src/scripting/bridge.rs
  • src/server/conn/blocking.rs
  • src/server/conn/core.rs
  • src/server/conn/handler_monoio/dispatch.rs
  • src/server/conn/handler_monoio/ft.rs
  • src/server/conn/handler_monoio/mod.rs
  • src/server/conn/handler_monoio/txn.rs
  • src/server/conn/handler_sharded/dispatch.rs
  • src/server/conn/handler_single.rs
  • src/server/conn_state.rs
  • src/server/embedded.rs
  • src/server/listener.rs
  • src/shard/conn_accept.rs
  • src/shard/coordinator.rs
  • src/shard/event_loop.rs
  • src/shard/mq_exec.rs
  • src/shard/persistence_tick.rs
  • src/shard/spsc_handler.rs

@pilotspacex-byte pilotspacex-byte merged commit e2d8789 into main Jul 14, 2026
10 checks passed
@TinDang97 TinDang97 deleted the worktree-agent-ad25fc066a7440219 branch July 14, 2026 07:18
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