RobustMQ ISR Replication Drill

SkillDev tools

Lets your agent run a chaos drill that kills and restarts RobustMQ replica nodes to test data replication and failover.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the RobustMQ ISR Replication Drill skill

About this capability

Run the ISR rolling-restart chaos drill for RobustMQ — 10 rounds of kill-one-node + write + restart + verify across a 3-replica shard. Use when the user wants to verify replica replication / ISR health, failover, or behaviour under churn — e.g. "演练 isr 副本同步", "验证三副本同步", "kill leader 演练", "验证 leader

What this skill tells your AI

The instructions your AI receives, as published by robustmq/robustmq in .claude/skills/isr-replication-drill/SKILL.md and read by ahel’s review.

Verifies ISR (In-Sync Replica) replication, leader failover, and recovery under repeated rolling restarts on a running 3-node cluster.

The single drill (three_replica_chaos_rolling_kill) covers all three flows the user cares about:

  1. Write + observe — acks=all writes in full ISR; all 3 replicas leo==hw==cumulative, lso==0.
  2. Kill (leader or follower), observe — ISR shrinks to 2; leader switches if victim was leader; epoch advances; survivors hold all committed data; acks=all still commits on 2 survivors.
  3. Restart, observe follower role — victim rejoins ISR as follower (not leader); all 3 replicas catch up to cumulative; all nodes agree on leader/epoch/ISR/leo/hw.

Runs 10 rounds; victim rotates n1→n2→n3→n1…, covering both follower-kill and leader-kill scenarios in the same run.

When to Use

  • Verify a 3-replica shard genuinely places replicas across all 3 nodes
  • Confirm followers replicate the leader's log (LEO converges, HW advances)
  • Verify leader failover: kill leader → switch + no committed-data loss + epoch bump
  • Verify ISR recovery: killed node rejoins as follower, offsets fully caught up
  • Regression-check fixed defects (see "Defects This Guards" below)

Key Semantics — what "ISR healthy" means

SignalMeaningHealthy when
segment.replicas.len()replicas actually placed== 3 always
segment.isrin-sync replica setall 3 node ids present
replica.leolog end offsetevery replica == cumulative
replica.high_watermarkcommitted/visible offsetevery replica == cumulative
replica.in_isr / availablemembership + reachableall true
segment.leader_epochfencing termmonotonic; bumps on every leader switch

Storage type. ISR replication only works with EngineRocksDB or EngineMemory. EngineSegment returns NotLeaderForPartition on follower fetch — use EngineRocksDB.

HW lags by one fetch round. A follower learns the leader's HW from the next fetch response. Always poll until convergence — never assert HW immediately after a write.

Prerequisites

# Build the executable (NOT `cargo build -p broker-server` — that only builds the lib).
cargo build --bin broker-server

Cluster configs: config/cluster/server-{1,2,3}.toml (broker_id 1/2/3; meta/grpc 1228/2228/3228; engine tcp 1779/2779/3779; admin http 58080/58082/58083). Data dirs: data/broker-{1,2,3}.

Test: tests/tests/engine/isr.rs::three_replica_chaos_rolling_kill (#[ignore]). Test package: robustmq-test.

Drill Steps

0. Clean state (mandatory)

for c in 1 2 3; do p=$(pgrep -f "server-$c.toml"); [ -n "$p" ] && kill -INT "$p"; done
for c in 1 2 3; do while pgrep -f "server-$c.toml" >/dev/null; do sleep 1; done; done
rm -rf data/broker-1 data/broker-2 data/broker-3 data/logs /tmp/n1.log /tmp/n2.log /tmp/n3.log

NEVER pkill -9 — SIGKILL leaves an ESTABLISHED socket on port 1228 that mac_agent keeps alive, blocking the next bind. Always stop with kill -INT.

1. Start the 3-node cluster (staggered so node1 bootstraps first)

./target/debug/broker-server --conf config/cluster/server-1.toml > /tmp/n1.log 2>&1 &
sleep 8
./target/debug/broker-server --conf config/cluster/server-2.toml > /tmp/n2.log 2>&1 &
sleep 9
./target/debug/broker-server --conf config/cluster/server-3.toml > /tmp/n3.log 2>&1 &
sleep 13

Verify:

grep -oE "bootstrapping single-node cluster|Successfully joined cluster via peer|Meta Service cluster is ready|Failed to start GRPC" /tmp/n1.log /tmp/n2.log /tmp/n3.log | sort | uniq -c
curl -s http://localhost:58080/api/info | python3 -c "import sys,json;d=json.load(sys.stdin).get('data',{});print('nodes:',sorted(n.get('node_id') for n in d.get('broker_node_list',[])))"

Expect: node1 bootstrap, node2/3 joined, nodes: [1, 2, 3].

2. Run the drill

cargo test -p robustmq-test three_replica_chaos_rolling_kill -- --ignored --nocapture

Each round:

  1. wait_full_and_caught_up — precondition: all 3 replicas leo==hw==cumulative, lso==0, in ISR.
  2. write_acks_all(BATCH=30) → cumulative += 30. Confirm all 3 replicas caught up.
  3. kill_node(victim) (victim rotates n1→n2→n3→…).
  4. Wait for victim to leave ISR; if was-leader, assert new leader elected, epoch advanced.
  5. Assert survivors: available, leo==hw==cumulative, lso==0, replicas=={1,2,3}.
  6. write_acks_all(BATCH=30) on 2 survivors (degraded ISR). Confirm both commit.
  7. restart_node(victim).
  8. wait_full_and_caught_up — all 3 replicas back in ISR, fully caught up.
  9. Assert restarted node is follower (not the new leader).
  10. Cross-node check: all 3 nodes agree on leader/epoch/ISR/leo/hw.
  11. Log blacklist scan.

After all 10 rounds: read back all cumulative records; final log scan.

Expected tail:

===== round 1: leader=n3, restarting n1 (was_leader=false), cumulative=30 =====
  n1 left ISR after 11.3s: leader=n3 epoch=0 isr=[2, 3]
  degraded write committed on 2 survivors at leo=hw=60
===== round 1 OK: leader=n3 epoch=0 isr=[1, 2, 3] all leo=hw=60 lso=0 =====
...
===== round 3: leader=n3, restarting n3 (was_leader=true), cumulative=150 =====
  n3 left ISR after 28.3s: leader=n1 epoch=1 isr=[1]
  degraded write committed on 2 survivors at leo=hw=180
===== round 3 OK: leader=n1 epoch=1 isr=[1, 2, 3] all leo=hw=180 lso=0 =====
...
CHAOS COMPLETE: 10 rounds, 600 committed records read back OK, 6 leader-epoch switches, all replicas consistent

Timings:

  • Dead follower leaves ISR in ~6–11s (replica_lag_time_max_ms).
  • Dead leader triggers switch in ~28–30s (heartbeat timeout).
  • Follower rejoin after restart in ~4–10s (reconcile self-heal).

Check Conditions (per round)

CheckWhat it catches
hw <= leo on every checked_detail callHW/offset bookkeeping corruption
replicas == {1,2,3} alwaysreplica drop / unintended migration
ISR shrinks to {survivors} after killISR not removing dead replica
ISR recovers to {1,2,3} after rejoinfailure to re-admit rejoining follower
leader_epoch monotonic, bumps on switchfencing broken
All 3 nodes agree: leader, epoch, ISR, leo, hwsplit-brain / stale metadata view
Survivors: leo==hw==cumulative after killcommitted data loss on failover
Restarted node is follower, not leaderspurious election on rejoin
acks=all commits in full (3) AND degraded (2) ISRwrite stall under ISR churn
Final read-back == cumulative recordscontent lost despite correct counters
Log blacklist: no blacklisted linesraft panics, watchdog kills, unexplained exits

Log Blacklist

Lines that must NEVER appear (each is an unambiguous defect):

PatternMeaning
"invalid state: expect"raft purge_upto > snapshot regression
"last_log_id=None"raft log read back empty on restart
"Clean the hole"raft storage hole
"forcing exit"shutdown watchdog fired (ungraceful)
"acks=all timed out"committed write never acked
"NotEnoughReplicas"acks=all rejected by server
"panicked at"any Rust panic

Benign noise (NOT blacklisted): Unreachable node, fetcher retry, reconcile: follower has no fetcher, diverged-tail truncation on leader switch.

Quick scan:

for c in 1 2 3; do echo "=== n$c ==="; grep -E "invalid state: expect|last_log_id=None|Clean the hole|forcing exit|acks=all timed out|NotEnoughReplicas|panicked at" /tmp/n$c.log | head -5; done

Pass / Fail Criteria

PASS only if all hold after all 10 rounds:

  • All 10 rounds complete without assertion failure.
  • Every replica: available, in_isr, leo == hw == cumulative, lso == 0.
  • segment.replicas always [1, 2, 3].
  • leader_epoch monotonic across all rounds; advances on leader-kill rounds.
  • All 3 nodes agree on leader/epoch/ISR/leo/hw after every round.
  • Final read-back returns exactly cumulative (= ROUNDS × 2 × BATCH = 600) records.
  • No blacklisted lines in any node log.

Common FAIL signatures:

SymptomLikely cause
follower leo never reaches cumulativereplication pipe broken (fetcher thread / transport)
follower hw stuck at 0 while leo caught upfollower not applying leader_hw from fetch response
isr shrinks below 3 after rejoinreconcile self-heal not firing / replica not re-admitted
acks=all timed outwrite stall; ISR too small or fetcher stuck
"NotEnoughReplicas"acks=all rejected; ISR collapsed below min_in_sync_replicas
last_epoch went backwardbug in epoch tracking / metadata sync
restarted node assert_ne!(leader, victim) failsspurious election on rejoin

Defects This Drill Guards

Each defect below was found by this drill; a regression re-introduces it and the drill fails.

FixBug it prevents
broker-core heartbeat reports to every meta nodeheartbeat table is per-meta-node; after a meta-leader change the new leader had never seen heartbeats → expired ALL nodes every heartbeat_timeout_ms → endless ISR churn
isr_manager::compute_new_isr shrinks by fetch recency (last_fetch_ts), not leo >= leader_leodead replica that was fully caught up kept leo == leader_leo vs a non-advancing leader → pinned in ISR forever → acks=all blocked
dynamic_cache.rs Create handler: remove_leader_segment when this node is NOT the new leaderdemoted node kept segment in leader_segments, kept running ISR maintenance → stale proposals / divergent view
dynamic_cache.rs Create handler: only inits offsets when get_offset_state().is_none()leader-switch "Create" notification reset latest_offset→0 on a node that already held data → committed data lost
WriteReqBody.timeout_ms threaded into batch_write acks=all waitacks=all reused 500ms replica_fetch_max_wait_ms as commit timeout → spurious timeout
reconcile.rs self-heal: start fetcher when follower has no fetch_staterestarted follower missed leader-switch notification → never resumed replication → never rejoined ISR
isr/apply.rs::apply_as_follower passes leader_epoch_changed flagevery ISR membership notification triggered needs_truncation: true → fetcher stopped for OffsetsForLeaderEpoch round-trip → raced against acks=all 30s window
grpc-clients/src/utils.rs 5s PER_CALL_TIMEOUT per attemptretry_call_inner blocked 153s on a dead address during Raft snapshot install → acks=all in round 10 timed out

Cleanup

for c in 1 2 3; do p=$(pgrep -f "server-$c.toml"); [ -n "$p" ] && kill -INT "$p"; done
for c in 1 2 3; do while pgrep -f "server-$c.toml" >/dev/null; do sleep 1; done; done

Troubleshooting

SymptomCause / fix
Failed to start GRPC server on port 1228 but lsof -iTCP:1228 -sTCP:LISTEN emptypkill -9 left an orphan ESTABLISHED socket; mac_agent keeps it alive. Check netstat -anp tcp | grep '\.1228 '. Kill the mac_agent pid (launchd respawns it) or reboot. Prevent with kill -INT.
Rebuilt code "didn't take"cargo build -p broker-server builds only the lib. Use cargo build --bin broker-server.
segment_detail returns nullSegment not in cache yet — wait 10s and confirm segment_seq exists via the segment list.
EngineSegment replicas never syncISR replication not implemented for EngineSegment. Use EngineRocksDB.

Run Report

After each drill session emit a structured summary:

=== ISR REPLICATION DRILL RUN REPORT ===
Date:        <YYYY-MM-DD>
Code commit: <git rev-parse --short HEAD>

Drill — rolling-restart chaos (10 rounds)
  Result:    PASS / FAIL
  Rounds:    <N>/10 passed
  Records:   <N> committed records read back OK
  Epochs:    <N> leader-epoch switches
  Details:   <any notable anomalies or the failure round + panic line>

Log errors:
  node1: <N> ERRORs  (grep -c ' ERROR' /tmp/n1.log)
  node2: <N> ERRORs
  node3: <N> ERRORs

Overall:  PASS / FAIL
========================================

Gather inputs:

git rev-parse --short HEAD
for c in 1 2 3; do echo "node$c ERROR: $(grep -c ' ERROR' /tmp/n$c.log)"; done

Signals

GitHub stars
2k
Forks
256
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
isr-replication-drill
Source
github.com/robustmq/robustmq