Skip to content

feat(cluster): control plane on the default runtime + formation fixes (v0.9 W0/C-1, #405) - #450

Merged
TinDang97 merged 2 commits into
mainfrom
feat/c1-cluster-ctl-monoio
Aug 7, 2026
Merged

feat(cluster): control plane on the default runtime + formation fixes (v0.9 W0/C-1, #405)#450
TinDang97 merged 2 commits into
mainfrom
feat/c1-cluster-ctl-monoio

Conversation

@TinDang97

@TinDang97 TinDang97 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

First PR of the v0.9 milestone (Wave 0, C-1 — #405): the cluster control plane (bus listener on port+10000, 100 ms gossip ticker, failover election) now runs under the default monoio runtime. Red test first: a 3-node monoio fleet was stuck at known_nodes [3, 1, 1] forever — MEET wrote local state that no peer ever learned about.

Control plane placement (C-1)

  • One tokio-native implementation, on a dedicated cluster-ctl std thread hosting a current-thread tokio runtime — on BOTH runtimes. Under monoio there is no tokio runtime to share; under tokio, sharing the listener runtime made gossip compete with the accept loop and every connection, which starved the ticker under load (observed as PFAIL detection stalling in the new e2e until the thread was dedicated).
  • The never-called, never-tested monoio duplicates of run_cluster_bus / run_gossip_ticker / run_election_task (+ monoio_read_exact) are deleted per the milestone plan ("do NOT port to monoio: !Send task model, zero payoff") — net −232 lines in src/. Base tokio features gain time + io-util.
  • Shared wiring extracted to run_cluster_control_plane(); the tokio block's inline duplicate removed.

Formation fixes (pre-existing product bugs, both runtimes)

Found by the new e2e — a real 3-node cluster had never formed on either runtime (nothing ever tested it end-to-end):

  1. MEET placeholder never retired: CLUSTER MEET registers the peer under a random placeholder id; the handshake merged the real id as a NEW entry and the placeholder lived forever (known_nodes 5 in a 3-node cluster). Handshake now retires same-address/different-id entries (also covers a node restarting with a fresh id).
  2. Gossip sections only fed failure reports: nodes MEET-ed into a common seed never learned about each other — the mesh could not complete. Healthy rumors are now adopted, guarded against self-address rumors and already-known addresses.
  3. Rumor-adopted nodes could never go PFAIL: they started with pong_recv_ms = 0, which check_failure_states skips — a rumored node that died before first direct contact was permanently unflaggable (reproduced as one survivor never flagging a killed peer). Adoption and the MEET placeholder now stamp a freshness baseline.

Verification (C-1 exit: "3-node monoio cluster forms, gossips, elects")

  • New e2e tests/cluster_formation.rs (spawns real MOON_BIN binaries): 3-node formation via one seed's MEETs — the load-bearing criterion is identity convergence (all three real node ids resolved on every node, which placeholder rumors can't satisfy); plus a kill test asserting both survivors flag the victim pfail within the node timeout. Hard FAIL needs quorum ≥ 2 external reporters — unreachable for 2 survivors of 3 masters — so full FAIL/election e2e lands with C-3's replica legs.
  • Unit tests pin all three merge behaviors.
  • e2e stability: 8/8 consecutive green (4× monoio, 4× tokio); each product bug was watched failing first.

Gates

Refs #405

Summary by CodeRabbit

  • New Features

    • Added cluster mode support on the default Monoio runtime.
    • Added a dedicated control-plane thread for cluster coordination across runtimes.
  • Bug Fixes

    • Improved cluster formation and membership convergence.
    • Healthy peers are now learned through gossip, while stale handshake placeholders are retired.
    • Improved failure detection by initializing freshness tracking correctly.
  • Tests

    • Added end-to-end coverage for three-node formation, membership, gossip traffic, and failure detection.

… (v0.9 W0/C-1, #405)

The cluster control plane (bus listener on port+10000, 100ms gossip
ticker, failover election) was tokio-only: the monoio startup path — the
default build — never spawned it, so `--cluster-enabled` accepted
CLUSTER MEET but no peer ever learned anything. Red test first: a
3-node monoio fleet stuck at known_nodes [3, 1, 1] forever.

Control plane placement (C-1):
- One tokio-native implementation, spawned on a dedicated `cluster-ctl`
  std thread hosting a current-thread tokio runtime — on BOTH runtimes.
  Under monoio there is no tokio runtime to share; under tokio, sharing
  the listener runtime made gossip compete with the accept loop and
  every connection, which starved the ticker under load (observed as
  PFAIL detection stalling in the e2e until the thread was dedicated).
  Not latency-critical, so no shard-thread involvement; O5 aux pinning
  applies.
- The never-called, never-tested monoio duplicates of run_cluster_bus /
  run_gossip_ticker / run_election_task (and monoio_read_exact) are
  DELETED per the milestone plan ("do NOT port to monoio: !Send task
  model, zero payoff") — the tokio variants are the ones all CI has
  ever exercised. Base tokio features gain "time" + "io-util" (the
  control plane's only new footprint in the monoio build).
- Shared wiring extracted to run_cluster_control_plane() in main.rs;
  the tokio block's inline duplicate is gone.

Formation fixes (pre-existing product bugs, both runtimes — found by
the new e2e, which cannot pass without them):
- CLUSTER MEET registers the peer under a RANDOM placeholder id
  ("replaced by handshake") that nothing ever replaced: the handshake
  merged the real id as a NEW entry and the placeholder lived forever
  (known_nodes 5 in a 3-node cluster). merge_gossip_into_state now
  retires same-address/different-id entries when a sender's real
  identity arrives — also covers a node restarting with a fresh id.
- Gossip sections were consumed ONLY for PFAIL/FAIL reports: two nodes
  MEET-ed into a common seed never learned about each other, so the
  mesh could not complete. Healthy rumors are now adopted, guarded
  against self-address rumors (a peer's unresolved placeholder for us)
  and already-known addresses (the real entry wins).
- Rumor-adopted nodes started with pong_recv_ms = 0, which
  check_failure_states skips — a rumored node that died before first
  direct contact could NEVER go PFAIL on that observer (reproduced as
  one survivor permanently not flagging a killed peer). Adoption and
  the MEET placeholder now stamp a freshness baseline so the staleness
  clock always runs.

Tests:
- tests/cluster_formation.rs (new, runs against MOON_BIN, both
  runtimes): 3-node formation via one seed's MEETs — the load-bearing
  assertion is nodes 2/3 resolving ALL THREE real node ids (identity
  convergence, not just known_nodes, which placeholder rumors can
  satisfy); plus a kill test asserting both survivors flag the victim
  pfail within the node timeout. Hard FAIL needs quorum ≥ 2 EXTERNAL
  reporters, unreachable for 2 survivors of 3 masters — full
  FAIL/election e2e lands with C-3's replica legs.
- Unit tests pin the three merge behaviors (placeholder retirement,
  rumor adoption incl. bus_port + freshness baseline, self/known-addr
  rumor rejection).
- e2e stability: 8/8 consecutive green (4× monoio, 4× tokio) after the
  fixes; each of the three product bugs was watched failing first.

Gates: fmt; clippy -D warnings (default + tokio,jemalloc); macOS full
monoio suite; VM Linux lib + e2e on both runtimes. Bench gates waived:
no hot-path change — the control plane thread exists only with
--cluster-enabled, and the merge fixes run at gossip rate (10 Hz).

Refs #405
author: Tin Dang
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@TinDang97, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 91524d94-6abf-4417-9117-a31c925b8137

📥 Commits

Reviewing files that changed from the base of the PR and between d573036 and fd1ab67.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • src/cluster/bus.rs
  • src/cluster/command.rs
  • src/main.rs
  • tests/cluster_formation.rs
📝 Walkthrough

Walkthrough

The cluster control plane now runs on a dedicated Tokio thread for both runtimes. Gossip convergence handles placeholders and healthy rumors correctly. New end-to-end tests validate three-node formation, bus traffic, and failure detection.

Changes

Cluster control plane and convergence

Layer / File(s) Summary
Dedicated cluster control plane
Cargo.toml, src/main.rs, src/cluster/bus.rs, src/cluster/failover.rs, src/cluster/gossip.rs, CHANGELOG.md
Cluster bus, gossip, and failover use Tokio I/O. A dedicated cluster-ctl thread hosts the control plane on both runtimes.
Gossip membership convergence
src/cluster/command.rs, src/cluster/gossip.rs, CHANGELOG.md
CLUSTER MEET initializes liveness timestamps. Gossip retires address placeholders and adopts valid healthy peer rumors while filtering duplicates and self-addresses.
Three-node formation and failure detection
tests/cluster_formation.rs
End-to-end tests form a three-node cluster, verify node IDs and bus traffic, then verify pfail or fail detection after a node stops.

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

Possibly related issues

  • pilotspace/moon#405 — The PR implements the dedicated Tokio cluster control plane and validates monoio startup with three-node cluster formation and gossip.

Suggested reviewers: pilotspacex-byte

Sequence Diagram(s)

sequenceDiagram
  participant Main as main
  participant Control as cluster-ctl
  participant Bus as cluster bus
  participant Gossip as gossip ticker
  participant Nodes as cluster nodes
  Main->>Control: start dedicated Tokio runtime
  Control->>Bus: run cluster bus
  Control->>Gossip: run gossip ticker
  Nodes->>Bus: send MEET and gossip traffic
  Bus->>Gossip: update cluster state
  Gossip->>Nodes: propagate peer rumors
  Nodes-->>Gossip: report liveness and failure state
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the control-plane changes, formation fixes, verification results, and performance impact, although it does not use every template heading.
Title check ✅ Passed The title concisely identifies the cluster control-plane change and formation fixes, with relevant issue and milestone references.
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 feat/c1-cluster-ctl-monoio

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

Run cluster control plane on default runtime and fix 3-node formation

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Run the cluster bus, gossip ticker, and failover election on a dedicated tokio control-plane
 thread.
• Fix cluster formation convergence by retiring MEET placeholders, adopting healthy rumors, and
 seeding liveness timestamps.
• Add end-to-end tests that prove 3-node formation and PFAIL detection across runtimes.
Diagram

graph TD
  A["main.rs server startup"] --> B["cluster-ctl thread"] --> C["tokio current-thread runtime"]
  C --> D["cluster bus listener"] --> F["cluster state"]
  C --> E["gossip ticker + election"] --> F
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Port control plane fully to monoio
  • ➕ Single async stack (monoio) everywhere
  • ➕ Avoids embedding tokio runtime/thread under monoio builds
  • ➖ High complexity due to monoio !Send task model and ownership-based I/O
  • ➖ Duplicates logic or forces significant abstractions
  • ➖ Low payoff for 100ms-rate control-plane traffic; higher correctness risk
2. Keep control plane on existing tokio listener runtime (tokio build)
  • ➕ Fewer threads/runtimes
  • ➕ Simpler lifecycle management
  • ➖ Observed starvation risk: accept loop + per-connection work can delay the 100ms ticker
  • ➖ Harder to reason about scheduling under load; can regress failure detection
3. Run control plane on shard threads
  • ➕ Potentially reduces context switching; stays within existing executor topology
  • ➖ Adds latency-sensitive shard contention for non-critical work
  • ➖ Complicates pinning/NUMA assumptions and increases cross-cutting risk

Recommendation: The chosen approach (one tokio-native control-plane implementation running on a dedicated cluster-ctl thread for both runtimes) is the best balance of correctness and maintenance. It eliminates untested monoio duplicates, avoids tokio-runtime starvation under load, and keeps cluster control-plane work off shard threads while still functioning in monoio builds.

Files changed (8) +604 / -550

Enhancement (1) +103 / -56
main.rsStart cluster control plane on a dedicated tokio thread for both runtimes +103/-56

Start cluster control plane on a dedicated tokio thread for both runtimes

• Introduces 'run_cluster_control_plane()' to centralize cluster bus/ticker/election wiring and spawns it on a named 'cluster-ctl' OS thread hosting a current-thread tokio runtime. Removes the prior tokio-only inline control-plane startup and updates monoio comments/behavior to reflect cluster support via the dedicated thread.

src/main.rs

Bug fix (2) +183 / -160
command.rsSeed MEET placeholder nodes with a liveness baseline +6/-1

Seed MEET placeholder nodes with a liveness baseline

• When handling 'CLUSTER MEET', initializes the placeholder node’s 'pong_recv_ms' to a current timestamp. Prevents MEET-created entries from being permanently exempt from PFAIL detection prior to handshake replacement.

src/cluster/command.rs

gossip.rsFix formation via placeholder retirement + healthy rumor adoption; add unit tests +177/-159

Fix formation via placeholder retirement + healthy rumor adoption; add unit tests

• Makes 'now_ms' reusable across cluster code and updates gossip merge behavior to retire same-address/different-id placeholders on handshake. Extends gossip section processing to adopt healthy unknown nodes (with guards against self-address and already-known addresses) and stamps 'pong_recv_ms' so rumored nodes can become PFAIL. Removes the monoio gossip ticker implementation and adds focused unit tests for the new formation rules.

src/cluster/gossip.rs

Refactor (2) +0 / -333
bus.rsRemove monoio bus implementation and standardize on tokio +0/-194

Remove monoio bus implementation and standardize on tokio

• Drops the monoio-specific cluster bus listener, peer handler, and 'monoio_read_exact' helper. Leaves the tokio implementation as the single codepath used by the new shared control-plane runner.

src/cluster/bus.rs

failover.rsRemove monoio election-task implementation +0/-139

Remove monoio election-task implementation

• Deletes the monoio variant of 'run_election_task', consolidating elections onto the tokio-native implementation used by the shared control plane. Reduces duplicated, previously unexercised codepaths.

src/cluster/failover.rs

Tests (1) +286 / -0
cluster_formation.rsAdd multi-process e2e tests for 3-node formation and PFAIL detection +286/-0

Add multi-process e2e tests for 3-node formation and PFAIL detection

• Adds an end-to-end test suite that spawns three real server binaries, performs MEET from a single seed, and asserts identity convergence across all nodes. Includes a kill test that verifies both survivors flag the victim as PFAIL within the configured node timeout, and implements robust port reservation to avoid 'port+10000' collisions.

tests/cluster_formation.rs

Documentation (1) +27 / -0
CHANGELOG.mdDocument cluster control-plane enablement and formation fixes +27/-0

Document cluster control-plane enablement and formation fixes

• Adds an Unreleased changelog entry describing the new dedicated cluster control-plane thread, removal of monoio duplicates, and the new e2e formation test. Documents the three cluster formation bugs fixed (placeholder retirement, healthy rumor adoption, and liveness baseline).

CHANGELOG.md

Other (1) +5 / -1
Cargo.tomlEnable tokio time + io-util for shared control plane +5/-1

Enable tokio time + io-util for shared control plane

• Extends base tokio dependency features with 'time' and 'io-util' so the tokio-native cluster control plane can run regardless of server runtime. Keeps runtime-tokio as the full-feature superset while ensuring cluster bus/ticker dependencies are available in monoio builds too.

Cargo.toml

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

Caution

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

⚠️ Outside diff range comments (1)
src/cluster/gossip.rs (1)

13-13: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep the Tokio feature behind a runtime-independent dependency.

Cargo.toml only adds Tokio features under runtime-tokio, but src/cluster/gossip.rs and src/cluster/bus.rs import tokio::io unconditionally. Make Tokio available as a base feature for the cluster control-plane module, or gate these imports/exclusions consistently, so runtime-monoio builds without failing while the module is selected.

🤖 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/cluster/gossip.rs` at line 13, Update the cluster dependency/features
configuration and the unconditional Tokio I/O imports used by gossip.rs and
bus.rs so runtime-monoio builds successfully when the cluster control-plane
module is enabled. Either expose the required tokio::io functionality through
the base cluster feature or consistently gate/exclude both imports and their
dependent code by runtime, preserving Tokio support.

Source: Coding guidelines

🧹 Nitpick comments (3)
src/cluster/gossip.rs (2)

275-296: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider refreshing the sender address on an existing entry.

or_insert_with sets addr only at insert time. If a known peer moves to a new address, the entry keeps the stale address, and the gossip ticker keeps probing the old addr/bus_port. The retain below removes other entries at sender_addr, so the stale entry survives as the only one and never self-heals.

♻️ Proposed refresh of address and bus port
     entry.pong_recv_ms = now_ms();
+    entry.addr = sender_addr;
+    entry.bus_port = msg.sender_bus_port;
     if msg.config_epoch > entry.epoch {
🤖 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/cluster/gossip.rs` around lines 275 - 296, Update the existing-node path
around the state.nodes entry lookup to refresh the matched ClusterNode address
and bus port from the current sender information, not only when ClusterNode::new
inserts it. Preserve the insertion behavior while ensuring known peers that move
are subsequently probed at their new address.

336-371: 🔒 Security & Privacy | 🔵 Trivial | ⚖️ Poor tradeoff

Rumor adoption has no bound on the node map.

Every gossip section arrives from the cluster bus, which accepts any TCP peer. Each new (node_id, addr) pair now inserts a ClusterNode. A hostile or misconfigured peer can therefore grow state.nodes without limit, and every entry is then probed by the ticker rotation.

Consider a cap on adopted nodes, or accept rumors only from senders already known through CLUSTER MEET.

🤖 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/cluster/gossip.rs` around lines 336 - 371, Bound rumor adoption in the
unknown-node branch around ClusterNode::new and state.nodes.insert: only insert
an adopted node when the node map remains within an explicit configured cap, or
require the rumor sender to be an already-known CLUSTER MEET peer. Preserve
existing address, port, self-address, and duplicate checks, and skip adoption
when the trust or capacity condition is not met.
src/main.rs (1)

1903-1931: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

The cluster-ctl thread handle is dropped and never joined.

cancel_token.cancel() at shutdown stops the ticker, but the process does not wait for the bus listener and its peer tasks to finish. In-flight gossip and vote handling can be cut mid-write. Keeping the JoinHandle and joining it after the shard joins would make shutdown deterministic.

🤖 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/main.rs` around lines 1903 - 1931, The cluster control-plane thread
spawned in the cluster_state block is not retained or joined during shutdown.
Store the std::thread::JoinHandle returned by spawn, then join it after the
shard threads have joined and cancellation has completed, preserving the
existing run_cluster_control_plane execution and propagating or handling join
failures consistently with nearby shutdown joins.
🤖 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/cluster/command.rs`:
- Around line 277-284: Update the placeholder insertion logic in the CLUSTER
MEET handling around ClusterNode::new to check for an existing node with the
same address, rather than only checking state.nodes by the freshly generated
peer_id. Reuse or skip insertion when that address is already present, while
preserving insertion for genuinely new addresses and the existing handshake
behavior.

In `@src/main.rs`:
- Around line 2101-2129: Update the tokio task invoking run_cluster_bus so an
error from that function, including bind failure, cancels the node’s control
plane or otherwise aborts startup instead of only logging and allowing
run_gossip_ticker to continue. Use the existing cancel_token/control-flow
mechanisms around run_cluster_bus and run_gossip_ticker, while preserving the
error log before triggering shutdown.
- Line 2085: Validate the port before computing cluster_port in the surrounding
startup flow: reject any port above 55535 with the existing configuration-error
handling, then perform the addition only for valid values so the result cannot
truncate when cast to u16. Preserve the current cluster-port behavior for ports
within the valid range.

In `@tests/cluster_formation.rs`:
- Around line 23-53: Extend the retry logic in the shared startup flow around
spawn_cluster_node and connect_retry to monitor child liveness while connecting;
when a node exits before accepting either its client port or +10000 cluster-bus
sibling, reserve a fresh port pair, respawn the affected fleet, and retry with
the new ports instead of waiting for the full deadline.

---

Outside diff comments:
In `@src/cluster/gossip.rs`:
- Line 13: Update the cluster dependency/features configuration and the
unconditional Tokio I/O imports used by gossip.rs and bus.rs so runtime-monoio
builds successfully when the cluster control-plane module is enabled. Either
expose the required tokio::io functionality through the base cluster feature or
consistently gate/exclude both imports and their dependent code by runtime,
preserving Tokio support.

---

Nitpick comments:
In `@src/cluster/gossip.rs`:
- Around line 275-296: Update the existing-node path around the state.nodes
entry lookup to refresh the matched ClusterNode address and bus port from the
current sender information, not only when ClusterNode::new inserts it. Preserve
the insertion behavior while ensuring known peers that move are subsequently
probed at their new address.
- Around line 336-371: Bound rumor adoption in the unknown-node branch around
ClusterNode::new and state.nodes.insert: only insert an adopted node when the
node map remains within an explicit configured cap, or require the rumor sender
to be an already-known CLUSTER MEET peer. Preserve existing address, port,
self-address, and duplicate checks, and skip adoption when the trust or capacity
condition is not met.

In `@src/main.rs`:
- Around line 1903-1931: The cluster control-plane thread spawned in the
cluster_state block is not retained or joined during shutdown. Store the
std::thread::JoinHandle returned by spawn, then join it after the shard threads
have joined and cancellation has completed, preserving the existing
run_cluster_control_plane execution and propagating or handling join failures
consistently with nearby shutdown joins.
🪄 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: 063597bd-d029-4aa0-a22f-a5b168b5c5a9

📥 Commits

Reviewing files that changed from the base of the PR and between 106943e and d573036.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • Cargo.toml
  • src/cluster/bus.rs
  • src/cluster/command.rs
  • src/cluster/failover.rs
  • src/cluster/gossip.rs
  • src/main.rs
  • tests/cluster_formation.rs
💤 Files with no reviewable changes (2)
  • src/cluster/bus.rs
  • src/cluster/failover.rs

Comment thread src/cluster/command.rs Outdated
Comment thread src/main.rs Outdated
Comment thread src/main.rs
Comment thread tests/cluster_formation.rs
@qodo-code-review

qodo-code-review Bot commented Aug 7, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Bus bind failure ignored ✓ Resolved 🐞 Bug ≡ Correctness
Description
run_cluster_control_plane spawns the cluster bus in a fire-and-forget task and always proceeds to
run the gossip ticker; if the bus fails to bind (e.g., port already in use), cluster mode remains
enabled but the node can’t accept inbound cluster traffic. This yields a partially broken cluster
node while logging "started" unconditionally.
Code

src/main.rs[R2105-2108]

+    tokio::spawn(async move {
+        if let Err(e) = moon::cluster::bus::run_cluster_bus(
+            &bus_bind,
+            cluster_port,
Relevance

●●● Strong

Similar “don’t ignore critical task failures” issues were fixed; bind failure should disable/abort
cluster control plane.

PR-#291
PR-#65

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The control plane spawns run_cluster_bus and only logs its error, then immediately logs "started"
and runs the ticker. The bus itself returns an error immediately if bind fails, so this degraded
mode is real and reachable.

src/main.rs[2085-2130]
src/cluster/bus.rs[41-56]

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

### Issue description
The cluster bus listener’s startup failure (especially bind failure) is not treated as fatal for the control plane. `run_cluster_bus` returns `Err` on bind failure, but it’s only logged inside a detached `tokio::spawn`, while the gossip ticker continues.

### Issue Context
This can happen under port conflicts (including tests, side-by-side processes, or misconfiguration). The control plane then runs in a degraded state.

### Fix
- Ensure the bus bind succeeds before starting the ticker and before logging "Cluster bus and gossip ticker started".
- Options:
 - Refactor `run_cluster_bus` to split bind vs accept-loop, so `run_cluster_control_plane` can bind synchronously and abort/disable cluster on error.
 - Or use a oneshot/startup barrier where the bus task reports successful bind; if it reports an error, stop the ticker and return.
 - Or run bus and ticker under a supervisor task that terminates the whole control plane if the bus future returns `Err` early.

### Fix Focus Areas
- src/main.rs[2085-2130]
- src/cluster/bus.rs[41-84]

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


2. Control thread not supervised ✓ Resolved 🐞 Bug ☼ Reliability
Description
src/main.rs spawns the cluster control plane on a detached std::thread and drops the JoinHandle, so
a panic in the control plane (including the tokio runtime build .expect) will not stop the process
and leaves a cluster-enabled server running without cluster bus/gossip. The global panic hook only
aborts shard-* threads, so a cluster-ctl panic is not escalated beyond the default panic printout.
Code

src/main.rs[R1910-1913]

+        std::thread::Builder::new()
+            .name("cluster-ctl".to_string())
+            .spawn(move || {
+                // O5: escape the shard-core mask this thread would otherwise
Relevance

●● Moderate

Team handles spawn/expect risks before, but escalating panics from auxiliary threads is a bigger
policy change.

PR-#65
PR-#361

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The control plane is started via std::thread::Builder::spawn(...) without saving the JoinHandle,
so thread failure is not observed or joined. The process-wide panic hook explicitly aborts only for
shard-* thread names, meaning cluster-ctl panics do not trigger abort and can leave the server
running without cluster control-plane tasks.

src/main.rs[1894-1931]
src/main.rs[99-120]
PR-#65

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

### Issue description
The cluster control plane is started on a detached `std::thread` and its `JoinHandle` is discarded. If that thread panics (e.g., tokio runtime build `.expect(...)`), the process continues with cluster mode enabled but no functioning cluster control plane.

### Issue Context
The repo’s panic hook only aborts for `shard-*` threads, so `cluster-ctl` panics are not fail-fast and are not observed/handled by the main shutdown/join path.

### Fix
- Keep the `JoinHandle` for `cluster-ctl` and join it during shutdown (or otherwise supervise it).
- Convert control-plane thread failure into a process-level failure when `--cluster-enabled` is on (e.g., `catch_unwind` inside the thread and `abort()` / trigger global shutdown).
- Avoid `.expect(...)` inside the detached thread; propagate errors to the main thread via a channel and decide whether to disable cluster mode or exit.

### Fix Focus Areas
- src/main.rs[1894-1931]
- src/main.rs[99-120]
- src/main.rs[2049-2064]

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



Remediation recommended

3. Cluster e2e port TOCTOU ✗ Dismissed 🐞 Bug ☼ Reliability
Description
tests/cluster_formation.rs reserve_cluster_ports binds sockets to pick free ports but returns only
port numbers; the listeners are dropped on return, reopening a TOCTOU window before the child
processes bind, which can lead to EADDRINUSE flakes. The suite also retries connect() without
checking whether the child exited, reintroducing the dead-server blind-poll failure mode documented
in tests/common/mod.rs.
Code

tests/cluster_formation.rs[R47-50]

+        ports.push(candidate);
+        if ports.len() == n {
+            return ports;
+        }
Relevance

●●● Strong

They’ve accepted prior work to reduce test flakiness/parallelism races; fixing TOCTOU reservations
fits that pattern.

PR-#446

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The function claims to keep reservations but returns only ports, which drops the local held
listeners immediately. The repo’s shared test harness explicitly calls out this TOCTOU and
dead-child polling problem and provides helpers to address it.

tests/cluster_formation.rs[21-53]
tests/common/mod.rs[3-21]
tests/cluster_formation.rs[87-101]

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

### Issue description
The port-reservation helper in `tests/cluster_formation.rs` does not actually hold the reservations across the critical window where servers bind, and the connection retry loop does not detect early child exit.

### Issue Context
`tests/common/mod.rs` already documents these exact integration-test failure modes and provides a safer pattern (`spawn_listening`) to reduce flakiness and improve diagnostics.

### Fix
- Make `reserve_cluster_ports` return a guard that owns the `TcpListener`s (both client port and `+10000` bus port) until after all children have successfully bound/accepted.
 - e.g., return `(Vec<u16>, Vec<TcpListener>)` or a small struct with `ports` + `held`.
- In `connect_retry`, check `Child::try_wait()` (or incorporate a `spawn_listening`-style helper) so a dead child fails the test immediately with better context.

### Fix Focus Areas
- tests/cluster_formation.rs[21-101]
- tests/common/mod.rs[3-110]

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



Informational

4. Unannotated unwrap() in e2e 📘 Rule violation ✧ Quality
Description
New .unwrap() calls were introduced in the integration test tests/cluster_formation.rs and in
the #[cfg(test)] test module in src/cluster/gossip.rs without the required in-scope
#[allow(clippy::unwrap_used)] and an adjacent one-line justification comment. This violates the
unwrap-annotation compliance rule (PR Compliance ID 302083) and may cause unwrap-audit/clippy policy
failures.
Code

tests/cluster_formation.rs[R73-76]

+            "1",
+            "--dir",
+            dir.to_str().unwrap(),
+            "--disk-free-min-pct",
Relevance

● Weak

Unannotated unwraps in tests have prior closely-matching rejections; team didn’t adopt
allow+justification policy changes.

PR-#427
PR-#211
PR-#217

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 302083 mandates that every .unwrap() in diffs (including tests) must be covered
by an in-scope #[allow(clippy::unwrap_used)] and have a directly-preceding one-line justification
comment. The cited changes include multiple .unwrap() usages—such as dir.to_str().unwrap() and
RESP parsing-related .unwrap() calls in the new tests/cluster_formation.rs, as well as
.unwrap() used by the added test helper id40 inside the new #[cfg(test)] module in
src/cluster/gossip.rs—without any corresponding allow attribute and justification comment present
in scope, demonstrating non-compliance.

Rule 302083: Annotate safe unwrap calls with allow and justification
tests/cluster_formation.rs[67-76]
tests/cluster_formation.rs[115-121]
src/cluster/gossip.rs[532-538]

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()` usages were added in `tests/cluster_formation.rs` and in the `#[cfg(test)]` test module in `src/cluster/gossip.rs`, but they are missing the required in-scope `#[allow(clippy::unwrap_used)]` attribute and a one-line justification comment directly above the `.unwrap()`.

## Issue Context
PR Compliance ID 302083 requires every `.unwrap()` introduced in diffs (including tests) to be covered by an in-scope `#[allow(clippy::unwrap_used)]` and accompanied by an immediately-adjacent, directly-preceding one-line justification comment; missing these can trigger unwrap-audit/clippy policy failures.

## Fix Focus Areas
- tests/cluster_formation.rs[67-121]
- src/cluster/gossip.rs[532-616]

ⓘ 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 thread src/main.rs
Comment thread src/main.rs Outdated
Comment thread tests/cluster_formation.rs
…rt refusal, ctl-thread supervision

Addresses the CodeRabbit + Qodo findings on #450:

- CLUSTER MEET is idempotent by ADDRESS: the placeholder id is fresh
  random per call, so the old contains_key(&peer_id) check never fired
  and every repeated MEET for one address stacked another placeholder.
  Also refuses MEET-ing the node's own advertised address (a self
  placeholder can never be retired — the handshake's sender==self
  early-return skips the merge that kills placeholders).
- Cluster bus bind failure aborts startup: the listener is now bound in
  run_cluster_control_plane BEFORE anything spawns, and EADDRINUSE (or
  any bind error) exits the process — a cluster node without its bus
  keeps serving clients while being invisible to every peer, the
  half-alive state both bots flagged. run_cluster_bus takes the bound
  listener and no longer returns a Result.
- --cluster-enabled refuses --port > 55535 at startup (REFUSING TO
  START, exit 2): the bus port is port + 10000 and previously wrapped
  silently to a port no peer would ever compute.
- cluster-ctl thread supervision: the existing shard-panic abort hook
  now also matches the cluster-ctl thread, so a control-plane panic
  aborts the process instead of leaving the node cluster-deaf. With the
  bind fail-loud change, the only remaining thread-exit paths are
  graceful shutdown (cancel) and abort.

Tests: unit (MEET idempotence ×3 calls, self-MEET error) + e2e
occupied_bus_port_aborts_startup (bus sibling held by the harness →
child must exit nonzero) + wire-level repeat-MEET / self-MEET asserts
in the formation e2e.

Gates: fmt; clippy -D warnings ×2 feature sets; cluster unit 54/54;
cluster_formation e2e 3/3 on BOTH runtime binaries.

Refs #405
author: Tin Dang
@TinDang97
TinDang97 merged commit 34bcfe7 into main Aug 7, 2026
32 checks passed
@TinDang97
TinDang97 deleted the feat/c1-cluster-ctl-monoio branch August 7, 2026 15:27
TinDang97 added a commit that referenced this pull request Aug 14, 2026
CodeRabbit finding on #486, verified against the encoding rather than taken at
face value. It is real in both directions:

  flags | v1 meaning | v2 meaning        | consequence
  ------|------------|-------------------|-----------------------------------
    3   | FAIL       | replica + PFAIL   | v2 reader downgrades a CONFIRMED
        |            |                   | failure to a mere suspicion
   4,5  | (unused)   | master/replica    | v1 reader tests `== 2 || == 3`
        |            | + FAIL            | exactly, so the failure is INVISIBLE

`GOSSIP_VERSION` was serialized and then thrown away at [8..10] with the comment
"we don't enforce version for forward compat". That is fine while an encoding
only ever gains values; it is not fine when values change meaning.

Bumps GOSSIP_VERSION 1 -> 2 and translates v1 sections at the wire boundary, so
nothing downstream ever sees a v1 discriminant. Unknown/future versions are
still parsed as before — the header layout is fixed and unknown health bits
already decode as healthy.

The reverse direction cannot be fixed from this side: a v1 peer ignores the
version too, so it will read our FAIL as healthy no matter what we send. That is
accepted rather than worked around, and logged loudly when a v1 peer is seen. No
v1 population exists to protect: a multi-node cluster could not form at all
before #450, so no released build ever exchanged these bytes in anger.

Two tests. One pins the collision itself (`parse_gossip_flags(3)` IS
replica+PFAIL) so the translation cannot be "simplified" away later; the other
stamps a v1 version into a serialized message and asserts the FAIL rumor
survives deserialization as FAIL.

Also re-walks the ADD tests->build crossing so the cached scope anchor picks up
`tests/cluster_formation.rs`, which the amended §5 scope already listed — the
second CodeRabbit finding, and one this task file had flagged against itself.

author: Tin Dang
TinDang97 added a commit that referenced this pull request Aug 14, 2026
CodeRabbit finding on #486, verified against the encoding rather than taken at
face value. It is real in both directions:

  flags | v1 meaning | v2 meaning        | consequence
  ------|------------|-------------------|-----------------------------------
    3   | FAIL       | replica + PFAIL   | v2 reader downgrades a CONFIRMED
        |            |                   | failure to a mere suspicion
   4,5  | (unused)   | master/replica    | v1 reader tests `== 2 || == 3`
        |            | + FAIL            | exactly, so the failure is INVISIBLE

`GOSSIP_VERSION` was serialized and then thrown away at [8..10] with the comment
"we don't enforce version for forward compat". That is fine while an encoding
only ever gains values; it is not fine when values change meaning.

Bumps GOSSIP_VERSION 1 -> 2 and translates v1 sections at the wire boundary, so
nothing downstream ever sees a v1 discriminant. Unknown/future versions are
still parsed as before — the header layout is fixed and unknown health bits
already decode as healthy.

The reverse direction cannot be fixed from this side: a v1 peer ignores the
version too, so it will read our FAIL as healthy no matter what we send. That is
accepted rather than worked around, and logged loudly when a v1 peer is seen. No
v1 population exists to protect: a multi-node cluster could not form at all
before #450, so no released build ever exchanged these bytes in anger.

Two tests. One pins the collision itself (`parse_gossip_flags(3)` IS
replica+PFAIL) so the translation cannot be "simplified" away later; the other
stamps a v1 version into a serialized message and asserts the FAIL rumor
survives deserialization as FAIL.

Also re-walks the ADD tests->build crossing so the cached scope anchor picks up
`tests/cluster_formation.rs`, which the amended §5 scope already listed — the
second CodeRabbit finding, and one this task file had flagged against itself.

author: Tin Dang
TinDang97 added a commit that referenced this pull request Aug 14, 2026
…they did not own (#485) (#486)

* fix(cluster): peer slot ownership never merged, so nodes served keys they did not own (#485)

A multi-node cluster silently accepted writes for slots belonging to other
nodes. `CLUSTER ADDSLOTS` does not bump the config epoch, so a hand-built
cluster sits at epoch 0 forever; the gossip merge accepted a peer's slot bitmap
only at a strictly higher epoch (`0 > 0` is false), so peer ownership was never
merged and each node believed the only slots in existence were its own.
`route_slot` then found no owner for a peer's slot and fell through to a
bootstrap fallback that serves anything unclaimed locally.

The failure mode was the worst one available: the write returned `+OK`, landed
on the wrong node, and was invisible to the node that owned the slot. No MOVED,
no error, nothing the client could detect.

Batch 0 — split role from health
--------------------------------
`NodeFlags` had mutually exclusive `Master` / `Replica` / `Pfail` / `Fail`
variants, so marking a node PFAIL destroyed its role and, for a replica, the
`master_id` saying which shard it belonged to. Redis treats the two as
orthogonal: a dead master is `role: master, health: fail`, a shape the old type
could not express. Split into `NodeRole` + `NodeHealth`.

Both axes now travel together in the gossip flags word (bit 0 role, bits 1-2
health), keeping the wire header at its fixed 2130 bytes. Unknown health bits
decode as Online so a forward-compatible peer is never treated as failed by
accident. The old rumor test `flags == 2 || flags == 3` would have read a
suspected *replica* (0b011) as a confirmed failure; rumors are now read through
`parse_gossip_flags`.

CLUSTER NODES and nodes.conf render both axes in Redis's own spelling, measured
against redis-server (3 masters, node-timeout 15000, SHUTDOWN NOSAVE, polled at
100ms): the victim's flags column goes `master` -> `master,fail?` ->
`master,fail`, and nodes.conf persists `master,fail`. Moon previously emitted
`pfail`, a token Redis never produces and no client parses.

Batch 1 — make routing honest
-----------------------------
- Gossip merges a peer's bitmap at *equal* epoch. A node is authoritative for
  its own slots; a strictly lower epoch is still ignored, preserving Redis's
  highest-epoch-wins tie-break for genuinely conflicting claims.
- Direct contact with a node clears suspicion about it. Nothing else ever reset
  health — `check_failure_states` only ever sets PFAIL — so a recovered node
  stayed suspected forever and its slots never counted as covered again.
- The unclaimed-slot fallback is split by cluster size. A lone node still serves
  locally, so single-node bootstrap keeps working; a formed cluster returns the
  new `SlotRoute::Down` -> `CLUSTERDOWN The cluster is down` instead of
  inventing an answer. Both dispatch gates already funnel non-Local routes
  through `into_error_frame`, and the inline fast path is fully stood down in
  cluster mode (deep-review R6), so there is no third-path hole.

Tests
-----
New suite `tests/cluster_client_bootstrap.rs` (18 tests, written red before the
fix). cb1/cb2/cb3 flip green here and cb4 — the single-node fallback the split
narrows — stays green. Identical results on both runtime legs. The 14 tests
covering CLUSTER SHARDS, MYSHARDID, READONLY/READWRITE, cluster_state and INFO
identity are `#[ignore]`d with their owning batch named in the reason string;
each is un-ignored by the batch that makes it pass.

`cluster_formation.rs::killed_node_is_flagged_by_survivors` asserted the token
`pfail`. Its expected literal moved to `fail?`/`fail` — toward the measured
oracle — with the assertion's strength unchanged: still both survivors, same
deadline, still an exact token match. Recorded in the task's §5 with the
measurement, because "a test outside the suite failed and I edited it" is the
shape of weakening a test and has to be shown not to be that.

Verified: cargo fmt; clippy `--all-targets` clean on both feature legs; lib
tests 4620 (monoio) / 3786 (tokio) passing; cluster_formation 3/3 and
cluster_client_bootstrap 4 passed / 14 ignored on both runtimes.

Refs #485
author: Tin Dang

* test(cluster): restore the failed-master premise in the promote-epoch test

Batch 0's rename turned `NodeFlags::Fail` into `NodeRole::Master` in this
test's setup and dropped the health, so the scenario no longer stated the
thing it is about — a failover FROM a failed master. The assertions still
passed because `promote_self_to_master` does not gate on health, which is
exactly why it was easy to miss.

Sets `health = Fail` in the setup and adds the post-condition that the
demoted master keeps `role = master` while staying FAIL. That pairing is
inexpressible under the old single enum and is the reason for the split, so
it deserves a direct assertion rather than only being implied.

author: Tin Dang

* fix(cluster): an unclaimed slot must say "Hash slot not served", not "The cluster is down"

Redis has TWO CLUSTERDOWN messages and a client can tell them apart:

  CLUSTERDOWN Hash slot not served  -- THIS slot has no owner; the cluster
                                       may be perfectly healthy otherwise
  CLUSTERDOWN The cluster is down   -- cluster_state is fail

`SlotRoute::Down` shipped the second one for the first condition. Conflating
them tells an operator the whole cluster is down when a single slot lost its
owner.

Measured against redis-server: 3 masters, cluster-require-full-coverage no,
`CLUSTER DELSLOTS 12182` on the owner, then GET and SET of a key hashing to
12182 on that same node. Both answered `CLUSTERDOWN Hash slot not served`, and
cluster_state stayed `ok` — which is what confirms the per-slot reading rather
than a cluster-wide one. A peer that had not yet learned of the DELSLOTS still
answered `MOVED 12182 127.0.0.1:7403`, so this message is the ex-owner's own
reply and not a redirect.

Renames the variant to `SlotRoute::SlotNotServed` so the two conditions cannot
be confused at the call site again, and adds `cb21` to pin both the read and
the write path.

The task contract froze the wrong text at v1 — it was derived from the
fail-closed clause's wording instead of measured, which is exactly what that
contract's header forbids. Recorded as AMENDMENT 1 against the frozen §3 rather
than silently edited: the build was written to v1 and had to change to satisfy
v2, so this is a correction toward the oracle the contract names as its own
authority, not a relaxation to accommodate the build. The fail-closed clause is
unchanged and still correct — `The cluster is down` belongs there, and batch 3
still owns it.

author: Tin Dang

* docs(changelog): name the measured per-slot CLUSTERDOWN text

author: Tin Dang

* fix(cluster): honour the gossip wire version — v1 and v2 flags collide

CodeRabbit finding on #486, verified against the encoding rather than taken at
face value. It is real in both directions:

  flags | v1 meaning | v2 meaning        | consequence
  ------|------------|-------------------|-----------------------------------
    3   | FAIL       | replica + PFAIL   | v2 reader downgrades a CONFIRMED
        |            |                   | failure to a mere suspicion
   4,5  | (unused)   | master/replica    | v1 reader tests `== 2 || == 3`
        |            | + FAIL            | exactly, so the failure is INVISIBLE

`GOSSIP_VERSION` was serialized and then thrown away at [8..10] with the comment
"we don't enforce version for forward compat". That is fine while an encoding
only ever gains values; it is not fine when values change meaning.

Bumps GOSSIP_VERSION 1 -> 2 and translates v1 sections at the wire boundary, so
nothing downstream ever sees a v1 discriminant. Unknown/future versions are
still parsed as before — the header layout is fixed and unknown health bits
already decode as healthy.

The reverse direction cannot be fixed from this side: a v1 peer ignores the
version too, so it will read our FAIL as healthy no matter what we send. That is
accepted rather than worked around, and logged loudly when a v1 peer is seen. No
v1 population exists to protect: a multi-node cluster could not form at all
before #450, so no released build ever exchanged these bytes in anger.

Two tests. One pins the collision itself (`parse_gossip_flags(3)` IS
replica+PFAIL) so the translation cannot be "simplified" away later; the other
stamps a v1 version into a serialized message and asserts the FAIL rumor
survives deserialization as FAIL.

Also re-walks the ADD tests->build crossing so the cached scope anchor picks up
`tests/cluster_formation.rs`, which the amended §5 scope already listed — the
second CodeRabbit finding, and one this task file had flagged against itself.

author: Tin Dang

* test(cluster): pin nodes.conf round-trip for every role x health pairing

The flags column is one comma-separated string parsed by substring, and
`"fail?"` CONTAINS `"fail"`. Testing them in the wrong order silently reloads
every PFAIL node as a confirmed FAIL on restart — a durability-path corruption
with no error and no log line.

The order is correct today and nothing pinned it. `test_nodes_conf_roundtrip`
covers a healthy master with slots, which is precisely the shape that lets the
bug through: swapping the two branches leaves it green.

Verified the new test is not vacuous by actually swapping the branches — it
fails with `master-pfail: health did not survive`, while the pre-existing
round-trip test stays green through the same swap.

Covers all six role x health pairings, including the ones the old single
`NodeFlags` enum could not represent (a failed node that is still a master), and
asserts a replica keeps its master_id — a replica that forgets it cannot be
grouped into a shard, which is what CLUSTER SHARDS needs in batch 4.

author: Tin Dang
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant