Skip to content

fix(replication): replicas apply streamed SWAPDB, emitted exactly once (#386) - #442

Merged
TinDang97 merged 1 commit into
mainfrom
fix/386-replica-swapdb
Aug 7, 2026
Merged

fix(replication): replicas apply streamed SWAPDB, emitted exactly once (#386)#442
TinDang97 merged 1 commit into
mainfrom
fix/386-replica-swapdb

Conversation

@TinDang97

@TinDang97 TinDang97 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #386.

Two stacked defects

  1. Replica no-op: apply_local had no SWAPDB intercept — the record fell through to generic dispatch, which hard-errors ("SWAPDB must be issued at the connection handler level"), and warn_on_error only logs. Every streamed SWAPDB silently no-op'd; the replica served pre-swap data for BOTH databases until a full resync.

  2. Wire multiplicity (found during design): a multi-shard master emitted the record once per REMOTE shard leg (SwapDb SPSC arm → wal_append_and_fanout) and never for the coordinator's own leg. Today's replica applies the merged wire as ONE stream, record by record — N−1 emissions swap N−1 times, a net NO-OP whenever N−1 is even (e.g. --shards 3). Correctness depended on the master's shard-count parity.

The fix — exactly-once wire contract

  • coordinator.rs (coordinate_swapdb): emit the replication record via record_local_write_global AFTER the durability gate and the local swap — an aborted SWAPDB can never reach replicas.
  • spsc_handler.rs (SwapDb arm): remote legs keep their per-shard WAL v3 + AOF writes (per-shard crash recovery replays each shard's own record) but no longer touch the replication backlog/offset/fanout.
  • apply.rs: SWAPDB intercept before generic dispatch — same slice-split swap as the WAL replay intercept; out-of-range / same-index / malformed args skip with a warn instead of poisoning the stream.
  • handler_single.rs (legacy non-sharded tokio listener, no production callers): emits the record after its swap for contract consistency.

When #406 lands per-shard demuxed multi-shard replicas, this must flip to per-shard emission + per-stream apply (noted at all three sites).

Tests (red/green TDD)

tests/replication_swapdb.rs — real master (shards=1/3/4) + real replica. All three failed before the fix (db0 k0=\"before-0\" — replica kept pre-swap state), all green after. shards=3 is the load-bearing case: two remote legs = even swap count, so a replica-apply-only fix nets to no-op and the test catches the multiplicity defect. shards=4 pins the coordinator-leg emission. Plus post-swap write replication, and 3 unit tests for apply_swapdb (swap, integer args, reversed order, out-of-range, same-index, malformed).

The integration tests are #[ignore]d like the other replication suites: PSYNC-as-master is monoio-only (-ERR PSYNC requires runtime-monoio on the master), so they run explicitly against a monoio release binary and can never pass in the CI tokio job.

Gates

  • Red→green: 3/3 integration + 3/3 unit tests
  • Kill-9 durability: crash_matrix_per_shard_aof 4/4 incl. crash_133_swapdb_multishard_durability_after_sigkill
  • Full monoio release suite green (one pre-existing client_tracking_invalidation flake — fails identically on pristine main, A/B verified)
  • Unit tests green under runtime-tokio,jemalloc; clippy -D warnings both feature sets; fmt
  • No hot-path code touched (SWAPDB paths only) — bench waived

Summary by CodeRabbit

  • Bug Fixes

    • Fixed SWAPDB replication across single- and multi-shard deployments.
    • Ensured database swaps are applied exactly once on replicas.
    • Improved replication reliability after swaps, including subsequent writes.
    • Added safe handling for invalid, malformed, or out-of-range database indexes.
    • Prevented duplicate replication events during distributed swaps.
  • Tests

    • Added integration coverage for one-, three-, and four-shard replication scenarios.

#386)

Two stacked defects made SWAPDB silently diverge master and replica:

1. Replica no-op: `apply_local` had no SWAPDB intercept — the record fell
   through to generic dispatch, which hard-errors ("SWAPDB must be issued
   at the connection handler level"), and `warn_on_error` only logs.
   Every streamed SWAPDB no-op'd; the replica served pre-swap data for
   BOTH databases until a full resync.

2. Wire multiplicity: a multi-shard master emitted the record once per
   REMOTE shard leg (SwapDb SPSC arm -> wal_append_and_fanout) and never
   for the coordinator's own leg. Today's replica applies the merged wire
   as ONE stream, record by record — N-1 emissions swap N-1 times, a net
   NO-OP whenever N-1 is even (e.g. --shards 3). Correctness depended on
   the master's shard-count parity.

The wire contract is now: exactly ONE SWAPDB record per client SWAPDB.

- coordinator.rs (`coordinate_swapdb`): emit the replication record via
  `record_local_write_global` AFTER the durability gate and the local
  swap — an aborted SWAPDB can never reach replicas. debug_assert
  relaxed to >= 1 (monoio routes all shard counts here).
- spsc_handler.rs (SwapDb arm): remote legs keep their per-shard WAL v3
  + AOF writes (per-shard crash recovery replays each shard's own
  record) but no longer touch the replication backlog/offset/fanout.
- apply.rs: SWAPDB intercept before generic dispatch — same slice-split
  swap as the WAL replay intercept; out-of-range / same-index /
  malformed args skip with a warn instead of poisoning the stream
  (a replica with fewer --databases must survive the record).
- handler_single.rs (legacy non-sharded tokio listener, no production
  callers): emits the record after its swap for contract consistency.

When #406 lands per-shard demuxed multi-shard replicas, this must flip
to per-shard emission + per-stream apply (noted at all three sites).

Tests (red/green): tests/replication_swapdb.rs — master at shards=1/3/4
with a live replica; shards=3 (even remote-leg count) pins the
multiplicity defect, shards=4 pins the coordinator-leg emission; plus
post-swap write replication. `#[ignore]`d like the other replication
suites: PSYNC-as-master is monoio-only ("-ERR PSYNC requires
runtime-monoio on the master"), so they run explicitly against a monoio
release binary, never in the CI tokio job. Unit tests for
`apply_swapdb` (both runtimes) cover swap, integer args, reversed
order, out-of-range, same-index, malformed args.
Gates: crash_matrix_per_shard_aof (SWAPDB kill-9 durability, #133)
green; full monoio release suite green (one pre-existing
client-tracking flake, fails identically on pristine main); clippy
both feature sets; fmt. No hot-path code touched (SWAPDB paths only).

Fixes #386
author: Tin Dang
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

SWAPDB replication now emits one durable record after a successful local swap. Replicas apply the command by swapping local database slices. Tests cover single- and multi-shard masters, invalid indices, and post-swap replication.

Changes

SWAPDB replication

Layer / File(s) Summary
Durable coordinator-owned emission
src/server/conn/handler_single.rs, src/shard/coordinator.rs, src/shard/spsc_handler.rs
SWAPDB retains its serialized WAL frame, persists it before emission, supports single-shard execution, and records replication exactly once after the local swap succeeds. Remote shard legs no longer fan out duplicate records.
Replica-side slice application
src/replication/apply.rs
Replicated SWAPDB commands parse bulk-string and integer indices, swap valid distinct local slices, treat identical indices as no-ops, and skip invalid or out-of-range requests with warnings. Unit tests cover these cases.
End-to-end replication validation
tests/replication_swapdb.rs, CHANGELOG.md
Ignored integration tests cover one-, three-, and four-shard masters, exactly-once replica swaps, and post-swap writes. The changelog documents the updated behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • pilotspace/moon#385: Both changes update multi-shard SWAPDB coordination and replication durability behavior.
  • pilotspace/moon#333: This PR implements the multi-shard replication behavior described by the related release work.
  • pilotspace/moon#278: Both changes modify replica-side command application in src/replication/apply.rs.

Suggested reviewers: pilotspacex-byte

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SWAPDBHandler
  participant ShardCoordinator
  participant AOF
  participant GlobalReplication
  participant ReplicaApply
  participant ReplicaSlices

  Client->>SWAPDBHandler: Execute SWAPDB
  SWAPDBHandler->>AOF: Enqueue serialized WAL frame
  SWAPDBHandler->>GlobalReplication: Record after durability and local swap
  ShardCoordinator->>GlobalReplication: Emit one SWAPDB record
  GlobalReplication->>ReplicaApply: Deliver replicated record
  ReplicaApply->>ReplicaSlices: Swap local database slices
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR meets the replica and exactly-once emission goals, but the required injected local-fsync-failure end-to-end test is not present in the summarized changes. Add an end-to-end test that forces local fsync failure and verifies the aborted SWAPDB is not emitted or applied on the replica.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the streamed SWAPDB replication fix and exactly-once emission change.
Description check ✅ Passed The description clearly covers the fix, design, tests, performance impact, and notes, although it does not use the repository template headings or checklist.
Out of Scope Changes check ✅ Passed All code, changelog, and test changes directly support streamed SWAPDB replication, exactly-once emission, durability, or replica application.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 fix/386-replica-swapdb

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

Fix replica SWAPDB apply and enforce exactly-once SWAPDB replication record (#386)

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Ensure replicas apply streamed SWAPDB and never silently no-op.
• Enforce exactly-once SWAPDB emission on the replication wire across shard counts.
• Add unit/integration coverage and document the replication contract in changelog.
Diagram

graph TD
  C["Client SWAPDB"] --> CO["Master coordinator"] --> P[("AOF/WAL durability")]
  CO --> RLG["record_local_write_global"] --> W["Replication wire stream"] --> RA["Replica apply_local"]
  CO --> SPSC["Remote shard SPSC"] --> P
  HS["Single-shard handler"] --> RLG
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Replica-side deduplication (idempotency key on SWAPDB)
  • ➕ Would tolerate accidental multi-emission on the master without correctness loss
  • ➕ Keeps master emission paths simpler in multi-shard flows
  • ➖ Requires adding a unique operation id to the replication protocol and stateful tracking on replicas
  • ➖ Still leaves unnecessary wire traffic and complexity around ordering vs other commands
2. Per-shard demuxed replication streams now (align with planned #406 direction)

Recommendation: The PR’s chosen approach (define an exactly-once SWAPDB wire contract and centralize emission at the coordinator/handler) is the best near-term fix: it preserves ordering, avoids protocol changes, and eliminates shard-count-parity dependence while keeping remote legs’ AOF/WAL durability for crash recovery. The alternatives either expand scope significantly (per-shard demux now) or add protocol/state complexity (replica dedupe) for limited benefit given the current single merged replica stream design.

Files changed (6) +498 / -36

Bug fix (4) +183 / -35
apply.rsIntercept and apply streamed SWAPDB on replicas (+ unit tests) +112/-0

Intercept and apply streamed SWAPDB on replicas (+ unit tests)

• Adds an early SWAPDB intercept in apply_local to prevent falling through to generic dispatch (which hard-errors and was previously only logged). Introduces apply_swapdb with robust argument parsing and safe slice-split swapping; out-of-range/same-index/malformed inputs warn or no-op rather than poisoning the replication stream. Adds unit tests validating swap behavior, integer args, reversed order, and error cases.

src/replication/apply.rs

handler_single.rsEmit SWAPDB replication record exactly once in single-shard handler +22/-16

Emit SWAPDB replication record exactly once in single-shard handler

• Refactors SWAPDB serialization to be reused for both durability and replication, cloning bytes when needed for AOF append. Records the SWAPDB replication write after durability and after performing the swap to match the coordinator’s contract.

src/server/conn/handler_single.rs

coordinator.rsCoordinator emits SWAPDB record post-durability and post-swap +20/-5

Coordinator emits SWAPDB record post-durability and post-swap

• Relaxes the assertion to allow num_shards >= 1 (monoio routes all shard counts through this path). Ensures serialized SWAPDB is appended durably first, then performs the local swap, then emits exactly one replication-plane record via record_local_write_global so aborted swaps never reach replicas.

src/shard/coordinator.rs

spsc_handler.rsRemove SWAPDB replication fanout from remote shard legs; keep AOF/WAL writes +29/-14

Remove SWAPDB replication fanout from remote shard legs; keep AOF/WAL writes

• Stops remote shard SWAPDB handling from calling wal_append_and_fanout (which previously emitted to the replication plane per remote leg). Retains per-shard WAL v3 and AOF writes needed for shard-local crash recovery replay, aligning with the new exactly-once wire contract.

src/shard/spsc_handler.rs

Tests (1) +300 / -0
replication_swapdb.rsAdd integration tests for SWAPDB replication across shard counts +300/-0

Add integration tests for SWAPDB replication across shard counts

• Adds an ignored (manual) integration suite spawning real master/replica processes to validate that a single client SWAPDB results in exactly one logical swap on the replica. Covers shards=1,3,4 to catch parity-related multi-emission issues and coordinator-leg omission, and verifies post-swap writes replicate into the correct logical DB.

tests/replication_swapdb.rs

Documentation (1) +15 / -1
CHANGELOG.mdDocument #386 SWAPDB replication correctness and exactly-once contract +15/-1

Document #386 SWAPDB replication correctness and exactly-once contract

• Adds a detailed changelog entry explaining the two defects (replica no-op and multi-shard wire multiplicity) and the new exactly-once SWAPDB replication contract. Clarifies replica behavior when database indexes are out of range (warn + skip).

CHANGELOG.md

@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

🤖 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 `@CHANGELOG.md`:
- Around line 10-24: Move the SWAPDB changelog bullet so it follows the complete
existing changelog item that begins before the current entry and ends after it.
Preserve the SWAPDB text unchanged and ensure the preceding c10k item remains a
single complete Markdown bullet.

In `@src/replication/apply.rs`:
- Around line 806-831: The apply_swapdb function currently ignores arguments
beyond the first two, allowing malformed SWAPDB records to execute. Require
args.len() == 2 before parsing and swapping, otherwise follow the existing
warning-and-skip path; add a test covering an extra-argument record and
confirming databases remain unchanged.

In `@src/shard/coordinator.rs`:
- Around line 2978-2989: Delay the coordinator’s record_local_write_global call
in src/shard/coordinator.rs:2978-2989 until every remote leg confirms successful
application and required durability, retaining serialized for that final
emission; update src/shard/spsc_handler.rs:2479-2499 to propagate
send_append_bounded_blocking failures to the coordinator and prevent SWAPDB or
success acknowledgement when the remote AOF append fails.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cec22b47-acdc-43f6-a0ae-2efe45f59060

📥 Commits

Reviewing files that changed from the base of the PR and between e46d1fa and 5e1dda7.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • src/replication/apply.rs
  • src/server/conn/handler_single.rs
  • src/shard/coordinator.rs
  • src/shard/spsc_handler.rs
  • tests/replication_swapdb.rs

Comment thread CHANGELOG.md
Comment on lines +10 to +24
- **Replicas now apply streamed `SWAPDB` (#386), and the record reaches the
wire exactly once per client call.** Two stacked defects: (1) the replica's
apply path had no SWAPDB intercept — generic dispatch hard-errors ("must be
issued at the connection handler level") and the error was only logged, so
every streamed SWAPDB silently no-op'd and the replica served pre-swap data
for both databases until a full resync; (2) a multi-shard master emitted the
record once per REMOTE shard leg and never for the coordinator's own leg —
against today's single merged replica stream that means N−1 swaps, a net
no-op whenever N−1 is even (e.g. `--shards 3`). The coordinator now emits
the replication record exactly once, after the durability gate and the
local swap (an aborted SWAPDB can never ship to replicas); remote SPSC legs
keep their per-shard AOF/WAL writes but stay off the replication plane; the
tokio single-shard handler emits it too. Replicas apply it with the same
slice-split swap as WAL replay, skipping (with a warning) indexes outside
their own `--databases` range instead of poisoning the stream.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Move the SWAPDB entry after the existing changelog item.

Line 10 starts a new bullet before the item that continues at Line 25 ends. Markdown attaches the c10k text to the new SWAPDB bullet and leaves the prior item incomplete. Move lines 10-24 after the complete existing item.

🤖 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 `@CHANGELOG.md` around lines 10 - 24, Move the SWAPDB changelog bullet so it
follows the complete existing changelog item that begins before the current
entry and ends after it. Preserve the SWAPDB text unchanged and ensure the
preceding c10k item remains a single complete Markdown bullet.

Comment thread src/replication/apply.rs
Comment on lines +806 to +831
fn apply_swapdb(cmd: &[u8], args: &[Frame], databases: &mut [crate::storage::Database]) {
let parse_idx = |f: &Frame| match f {
Frame::BulkString(b) => std::str::from_utf8(b).ok()?.parse::<usize>().ok(),
Frame::Integer(n) => usize::try_from(*n).ok(),
_ => None,
};
match (
args.first().and_then(parse_idx),
args.get(1).and_then(parse_idx),
) {
(Some(a), Some(b)) if a != b && a < databases.len() && b < databases.len() => {
let (lo, hi) = if a < b { (a, b) } else { (b, a) };
// Split the slice to get two non-overlapping mutable references.
let (left, right) = databases.split_at_mut(lo + 1);
std::mem::swap(&mut left[lo], &mut right[hi - lo - 1]);
}
(Some(a), Some(b)) if a == b => {} // same-index: no-op, matches Redis
_ => {
tracing::warn!(
"replication apply: skipping {} with unusable args (out of range for {} local dbs)",
String::from_utf8_lossy(cmd),
databases.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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject extra SWAPDB arguments.

Line 812 parses only the first two arguments. A malformed record such as SWAPDB 0 1 extra still swaps databases. Require args.len() == 2 before parsing. Warn and skip all other argument counts. Add a test for the extra-argument case.

🤖 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/apply.rs` around lines 806 - 831, The apply_swapdb function
currently ignores arguments beyond the first two, allowing malformed SWAPDB
records to execute. Require args.len() == 2 before parsing and swapping,
otherwise follow the existing warning-and-skip path; add a test covering an
extra-argument record and confirming databases remain unchanged.

Comment thread src/shard/coordinator.rs
Comment on lines +2978 to +2989

// #386 — replication plane, exactly once per client SWAPDB. Today's
// replica applies the merged wire as ONE stream, so the record must
// appear on it exactly once: the coordinator emits it here, AFTER
// the durability gate (an abort above never reaches this line, so a
// failed SWAPDB can never ship to replicas) and after the local
// swap; the remote legs' SPSC arms write AOF/WAL only. Safe on both
// runtimes: this runs on the shard's own OS thread (monoio shard
// thread / tokio per-shard LocalSet), whose event loop drains
// `self_msg`. When #406 lands per-shard demuxed replicas this must
// flip to per-shard emission.
crate::replication::state::record_local_write_global(my_shard, serialized);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Confirm every shard leg before global replication emission.

The coordinator records SWAPDB before remote legs report completion. A remote leg also discards a failed AOF append and still sends its success acknowledgement. A closed reply channel can return an error after the replica receives the global swap. An AOF enqueue failure can return +OK with no recoverable remote record.

  • src/shard/coordinator.rs#L2978-L2989: retain serialized and call record_local_write_global only after every remote leg confirms successful application and required durability.
  • src/shard/spsc_handler.rs#L2479-L2499: propagate send_append_bounded_blocking failure to the coordinator. Do not swap or acknowledge success when the remote AOF append fails.
📍 Affects 2 files
  • src/shard/coordinator.rs#L2978-L2989 (this comment)
  • src/shard/spsc_handler.rs#L2479-L2499
🤖 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 2978 - 2989, Delay the coordinator’s
record_local_write_global call in src/shard/coordinator.rs:2978-2989 until every
remote leg confirms successful application and required durability, retaining
serialized for that final emission; update src/shard/spsc_handler.rs:2479-2499
to propagate send_append_bounded_blocking failures to the coordinator and
prevent SWAPDB or success acknowledgement when the remote AOF append fails.

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Tokio calls shard-only fanout 🐞 Bug ≡ Correctness
Description
handler_single calls replication::state::record_local_write_global, but that function explicitly
requires running on the shard OS thread because it pushes to the shard thread-local self_msg
queue; calling it from the tokio connection handler can cause the SWAPDB replication record to never
be fanned out (and can also corrupt replication offsets). This breaks the stated “exactly-once on
the wire” contract whenever handler_single is exercised.
Code

src/server/conn/handler_single.rs[R910-912]

+                                            crate::replication::state::record_local_write_global(
+                                                0, serialized,
+                                            );
Relevance

●●● Strong

Cross-thread call likely breaks replication contract; team prioritizes SWAPDB correctness fixes.

PR-#100

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR adds a record_local_write_global(0, serialized) call inside the tokio handler_single
SWAPDB path, but record_local_write_global and shard::self_msg both explicitly document that
pushing to self_msg must only happen on shard OS threads (not tokio work-stealing threads). This
makes the new call site a contract violation with likely lost fanout.

src/server/conn/handler_single.rs[898-913]
src/replication/state.rs[473-507]
src/shard/self_msg.rs[26-34]

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/server/conn/handler_single.rs` invokes `crate::replication::state::record_local_write_global(...)` from a **tokio** connection handler. That helper pushes to `crate::shard::self_msg` (thread-local queue) and is documented as **shard-thread-only**; tokio tasks must not push there. This means the replication fanout for SWAPDB can be silently lost.

## Issue Context
- `record_local_write_global` explicitly states the caller must be on the shard OS thread and warns that tokio tasks must not push to `self_msg`.
- `shard::self_msg` module docs reiterate the same constraint.
- The PR added this call specifically for SWAPDB replication plane emission.

## Fix Focus Areas
- src/server/conn/handler_single.rs[905-913]
- src/replication/state.rs[473-507]
- src/shard/self_msg.rs[26-34]

## Suggested fix approach
- **Do not call** `record_local_write_global` from `handler_single`.
- Either:
 1) Implement/use a **tokio-safe** replication emission path for the single-thread handler (e.g., append to the replication backlog + fanout via a tokio-owned sender list / channel that is actually drained in this runtime), or
 2) If `handler_single` is truly non-production / non-replicating, remove the SWAPDB replication emission and document that this handler does not support replication-plane emission.

Acceptance criteria:
- No shard-thread-only (`self_msg`) APIs are invoked from tokio handler code.
- SWAPDB replication emission from this handler is either correct (delivered) or intentionally absent with explicit documentation.

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



Informational

2. Unneeded Bytes copy in SWAPDB 🐞 Bug ➹ Performance
Description
The SWAPDB SPSC arm allocates and copies the already-owned Bytes from serialize_command via
Bytes::copy_from_slice(&serialized) before sending it to send_append_bounded_blocking, adding
avoidable heap work on each SWAPDB remote leg. This should pass serialized.clone() (cheap) or move
serialized when possible.
Code

src/shard/spsc_handler.rs[R2494-2497]

+                    0,
+                    0,
+                    bytes::Bytes::copy_from_slice(&serialized),
+                    &mut aof_budget,
Relevance

●●● Strong

Avoidable buffer copy; repo often accepts perf/memory cleanups removing unnecessary allocations.

PR-#270

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
serialize_command returns a Bytes, and send_append_bounded_blocking accepts a Bytes by
value; thus cloning the existing Bytes is sufficient. The PR-added SWAPDB SPSC arm currently
re-copies the entire payload into a new buffer via copy_from_slice, which is extra work for no
benefit.

src/shard/spsc_handler.rs[2487-2499]
src/persistence/aof/mod.rs[509-514]
src/persistence/aof/pool.rs[498-505]

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

## Issue description
In the SWAPDB SPSC arm, `serialized` is already a `bytes::Bytes` (from `aof::serialize_command`). The code currently does `Bytes::copy_from_slice(&serialized)` before calling `send_append_bounded_blocking`, which allocates and copies the payload unnecessarily.

## Issue Context
- `aof::serialize_command` returns `Bytes`.
- `AofWriterPool::send_append_bounded_blocking` takes `Bytes` by value.
- Therefore, `serialized.clone()` is the correct low-cost way to pass ownership.

## Fix Focus Areas
- src/shard/spsc_handler.rs[2487-2499]
- src/persistence/aof/mod.rs[509-514]
- src/persistence/aof/pool.rs[498-505]

## Suggested fix approach
- Replace `bytes::Bytes::copy_from_slice(&serialized)` with `serialized.clone()` (or move `serialized` if no longer needed after WAL append).
- Keep WAL append using `&serialized` as-is.

Result:
- Eliminates an avoidable allocation/copy on SWAPDB remote legs.

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


3. MOON_BIN fallback in tests 📘 Rule violation ▣ Testability
Description
The new integration test falls back to env!("CARGO_BIN_EXE_moon") when MOON_BIN is unset,
instead of requiring MOON_BIN explicitly. This violates the requirement to avoid default binary
path discovery for server-spawning integration tests.
Code

tests/replication_swapdb.rs[R37-42]

+fn moon_bin() -> std::path::PathBuf {
+    if let Ok(p) = std::env::var("MOON_BIN") {
+        return std::path::PathBuf::from(p);
+    }
+    std::path::PathBuf::from(env!("CARGO_BIN_EXE_moon"))
+}
Relevance

● Weak

Team previously rejected enforcing explicit MOON_BIN over env!/binary-discovery fallbacks in
integration tests.

PR-#216
PR-#421

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 992389 requires integration tests to set and use MOON_BIN explicitly and to avoid
fallback helpers/paths. The new moon_bin() function explicitly falls back to
env!("CARGO_BIN_EXE_moon"), which violates the requirement.

Rule 992389: Integration tests must set MOON_BIN explicitly for server binaries
tests/replication_swapdb.rs[37-42]
tests/replication_swapdb.rs[60-66]

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

## Issue description
`tests/replication_swapdb.rs` falls back to `env!("CARGO_BIN_EXE_moon")` when `MOON_BIN` is not set. Compliance requires integration tests that spawn the server to require `MOON_BIN` explicitly and to fail fast when it is missing.

## Issue Context
This repo’s policy is that integration tests must not guess or auto-discover the server binary path; they should require `MOON_BIN` to be provided by the runner.

## Fix Focus Areas
- tests/replication_swapdb.rs[37-66]

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


4. unwrap() missing allow comment 📘 Rule violation ✧ Quality
Description
The new test file introduces .unwrap() calls without the required // ... justification comment
and adjacent #[allow(clippy::unwrap_used)]. This violates the unwrap-audit requirement for any
remaining unwrap() usages in diffs, including tests.
Code

tests/replication_swapdb.rs[R178-181]

+    let master_port = spawn_into(
+        &mut guard,
+        mdir.path().to_str().unwrap(),
+        master_shards,
Relevance

● Weak

Unwrap-annotation requirement has been explicitly rejected for tests; similar unwrap-in-test
feedback was rejected.

PR-#427
PR-#211

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 302083 requires each .unwrap() to be paired with an immediate justification
comment and #[allow(clippy::unwrap_used)]. The added test uses .unwrap() on to_str() calls
without any such annotation/comment.

Rule 302083: Annotate safe unwrap calls with allow and justification
tests/replication_swapdb.rs[178-181]
tests/replication_swapdb.rs[184-187]

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

## Issue description
New `.unwrap()` calls were added without the required adjacent `#[allow(clippy::unwrap_used)]` attribute and a one-line justification comment immediately above.

## Issue Context
The compliance policy requires every `.unwrap()` in diffs (including tests) to be explicitly justified and clippy-annotated, or rewritten to avoid `unwrap()`.

## Fix Focus Areas
- tests/replication_swapdb.rs[175-190]

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


Grey Divider

Context used
✅ Compliance rules (platform): 55 rules

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

Qodo Logo

Comment on lines +910 to +912
crate::replication::state::record_local_write_global(
0, serialized,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Tokio calls shard-only fanout 🐞 Bug ≡ Correctness

handler_single calls replication::state::record_local_write_global, but that function explicitly
requires running on the shard OS thread because it pushes to the shard thread-local self_msg
queue; calling it from the tokio connection handler can cause the SWAPDB replication record to never
be fanned out (and can also corrupt replication offsets). This breaks the stated “exactly-once on
the wire” contract whenever handler_single is exercised.
Agent Prompt
## Issue description
`src/server/conn/handler_single.rs` invokes `crate::replication::state::record_local_write_global(...)` from a **tokio** connection handler. That helper pushes to `crate::shard::self_msg` (thread-local queue) and is documented as **shard-thread-only**; tokio tasks must not push there. This means the replication fanout for SWAPDB can be silently lost.

## Issue Context
- `record_local_write_global` explicitly states the caller must be on the shard OS thread and warns that tokio tasks must not push to `self_msg`.
- `shard::self_msg` module docs reiterate the same constraint.
- The PR added this call specifically for SWAPDB replication plane emission.

## Fix Focus Areas
- src/server/conn/handler_single.rs[905-913]
- src/replication/state.rs[473-507]
- src/shard/self_msg.rs[26-34]

## Suggested fix approach
- **Do not call** `record_local_write_global` from `handler_single`.
- Either:
  1) Implement/use a **tokio-safe** replication emission path for the single-thread handler (e.g., append to the replication backlog + fanout via a tokio-owned sender list / channel that is actually drained in this runtime), or
  2) If `handler_single` is truly non-production / non-replicating, remove the SWAPDB replication emission and document that this handler does not support replication-plane emission.

Acceptance criteria:
- No shard-thread-only (`self_msg`) APIs are invoked from tokio handler code.
- SWAPDB replication emission from this handler is either correct (delivered) or intentionally absent with explicit documentation.

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

Comment thread src/shard/spsc_handler.rs
Comment on lines +2494 to +2497
0,
0,
bytes::Bytes::copy_from_slice(&serialized),
&mut aof_budget,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

4. Unneeded bytes copy in swapdb 🐞 Bug ➹ Performance

The SWAPDB SPSC arm allocates and copies the already-owned Bytes from serialize_command via
Bytes::copy_from_slice(&serialized) before sending it to send_append_bounded_blocking, adding
avoidable heap work on each SWAPDB remote leg. This should pass serialized.clone() (cheap) or move
serialized when possible.
Agent Prompt
## Issue description
In the SWAPDB SPSC arm, `serialized` is already a `bytes::Bytes` (from `aof::serialize_command`). The code currently does `Bytes::copy_from_slice(&serialized)` before calling `send_append_bounded_blocking`, which allocates and copies the payload unnecessarily.

## Issue Context
- `aof::serialize_command` returns `Bytes`.
- `AofWriterPool::send_append_bounded_blocking` takes `Bytes` by value.
- Therefore, `serialized.clone()` is the correct low-cost way to pass ownership.

## Fix Focus Areas
- src/shard/spsc_handler.rs[2487-2499]
- src/persistence/aof/mod.rs[509-514]
- src/persistence/aof/pool.rs[498-505]

## Suggested fix approach
- Replace `bytes::Bytes::copy_from_slice(&serialized)` with `serialized.clone()` (or move `serialized` if no longer needed after WAL append).
- Keep WAL append using `&serialized` as-is.

Result:
- Eliminates an avoidable allocation/copy on SWAPDB remote legs.

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

@TinDang97
TinDang97 merged commit cfb4bd6 into main Aug 7, 2026
15 checks passed
@TinDang97
TinDang97 deleted the fix/386-replica-swapdb branch August 7, 2026 04:39
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.

Replicas cannot apply streamed SWAPDB — records silently no-op (master/replica divergence)

1 participant