Skip to content

test(partitions): expose bugs when a slow replica rejoins the cluster - #4653

Merged
hubcio merged 2 commits into
masterfrom
repro/slow-replica-transfer
Oct 26, 2026
Merged

hubcio merged 2 commits into
masterfrom
repro/slow-replica-transfer

Conversation

@mmodzelewski

Copy link
Copy Markdown
Member

A replica that lags or runs slow can disturb its partition after the
fault ends. A state transfer it installs can leave the partition in an
endless view change. Add a test for that failure and for two related
ones. Each test is ignored as a known bug, so CI stays green until the
fixes land.

After a replica adopts a StartView and then installs a transfer
below the view head, the install rewinds the sequencer but keeps the
view. Its next DoViewChange carries the transfer op in the header
but starts the suffix at the old head, so peers drop it as
unreadable. If the transfer is more than 128 ops below the head, the
vote also leaves uncommitted ops outside its window. Two shard unit
tests replay the consensus steps of the install, build the vote, and
decode it as a peer does.

Under sustained writes, a backup that fell past the evicted ring
never installs a transfer. The first request pins the offer at the
primary's commit point, and the pull runs one retry interval later.
By then the backup journaled a newer op and refuses the offer as
stale. An integration spec cuts node 2 off, writes one message per
second after the heal, and waits for an install.

A replica whose messages arrive later than the view-change status
timeout keeps the two healthy replicas leaving their working view. A
simulator test shows this with latency alone. For this, the packet
simulator gains a directed extra delay per link. The delay does not
draw from the PRNG, so a run with zero extra delay replays the same
trace.

A replica that lags or runs slow can disturb its partition after the
fault ends. A state transfer it installs can leave the partition in an
endless view change. Add a test for that failure and for two related
ones. Each test is ignored as a known bug, so CI stays green until the
fixes land.

After a replica adopts a `StartView` and then installs a transfer
below the view head, the install rewinds the sequencer but keeps the
view. Its next `DoViewChange` carries the transfer op in the header
but starts the suffix at the old head, so peers drop it as
unreadable. If the transfer is more than 128 ops below the head, the
vote also leaves uncommitted ops outside its window. Two shard unit
tests replay the consensus steps of the install, build the vote, and
decode it as a peer does.

Under sustained writes, a backup that fell past the evicted ring
never installs a transfer. The first request pins the offer at the
primary's commit point, and the pull runs one retry interval later.
By then the backup journaled a newer op and refuses the offer as
stale. An integration spec cuts node 2 off, writes one message per
second after the heal, and waits for an install.

A replica whose messages arrive later than the view-change status
timeout keeps the two healthy replicas leaving their working view. A
simulator test shows this with latency alone. For this, the packet
simulator gains a directed extra delay per link. The delay does not
draw from the PRNG, so a run with zero extra delay replays the same
trace.
@github-actions github-actions Bot added the S-waiting-on-review PR is waiting on a reviewer label Oct 26, 2026
@codecov

codecov Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 22.22222% with 147 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.09%. Comparing base (6ce2f92) to head (4cccef6).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
core/shard/src/lib.rs 0.00% 143 Missing ⚠️
core/simulator/src/network.rs 0.00% 3 Missing ⚠️
core/simulator/src/packet.rs 97.67% 1 Missing ⚠️

❌ Your patch check has failed because the patch coverage (22.22%) is below the target coverage (50.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@              Coverage Diff              @@
##             master    #4653       +/-   ##
=============================================
- Coverage     90.77%   81.09%    -9.68%     
  Complexity     2989     2989               
=============================================
  Files          1459     1452        -7     
  Lines        348165   241903   -106262     
  Branches     292747   186485   -106262     
=============================================
- Hits         316042   196176   -119866     
- Misses        25485    39323    +13838     
+ Partials       6638     6404      -234     
Components Coverage Δ
Rust Core 79.98% <22.22%> (-12.11%) ⬇️
Java SDK 77.78% <ø> (-0.02%) ⬇️
C# SDK 83.53% <ø> (-0.21%) ⬇️
Python SDK 85.69% <ø> (ø)
PHP SDK 90.67% <ø> (ø)
Node SDK 96.44% <ø> (ø)
Go SDK 70.09% <ø> (+0.12%) ⬆️
C++ SDK 85.51% <ø> (ø)
Files with missing lines Coverage Δ
core/simulator/src/lib.rs 97.44% <ø> (ø)
core/simulator/src/packet.rs 95.71% <97.67%> (+0.10%) ⬆️
core/simulator/src/network.rs 83.56% <0.00%> (-3.59%) ⬇️
core/shard/src/lib.rs 90.71% <0.00%> (-1.43%) ⬇️

... and 488 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@hubcio
hubcio merged commit 8c9b9d2 into master Oct 26, 2026
111 checks passed
@hubcio
hubcio deleted the repro/slow-replica-transfer branch October 26, 2026 22:58
@github-actions github-actions Bot removed the S-waiting-on-review PR is waiting on a reviewer label Oct 26, 2026
hubcio added a commit that referenced this pull request Oct 27, 2026
A replica that fell behind and rejoined through state transfer hit
three bugs. The tests that #4653 added as ignored now run.

A transfer installed below the head of an adopted StartView rewound
the sequencer to the transfer op. Peers then dropped its
DoViewChange as unreadable, and a transfer past the 128-entry suffix
window left a gap between the vote's commit point and its suffix.
The install now raises the commit floor and never lowers the head,
like the metadata install and a restart. It also counts its journal
wipe and checkpoint restore as journal mutations, so the next vote
rebuilds its cached suffix.

Under steady writes, journal repair fetched committed ops past the
op the offer pinned. The stale guard protected every held op up to
the higher of the WAL head and commit_max, so it refused every
offer. It now protects held ops only up to the accepted WAL head, or
commit_max without a WAL: every partition ack waits for that head,
so this replica never acknowledged an op above it. Such a refusal
now has its own error.

A replica in a view change moved to the next view on each status
timeout. When its messages took longer than that to arrive, each
StartViewChange that landed pulled the healthy pair out of the view
they had just settled in. It now holds its view while it lacks the
votes for its DoViewChange and an older view's primary still sends
it heartbeats. Without such heartbeats it still escalates, because
Recovering peers never send a StartViewChange. A newer view ends the
hold, because the cluster already elected past it. Each hold logs at
INFO.
hubcio added a commit that referenced this pull request Oct 27, 2026
A replica that fell behind and rejoined through state transfer hit
three bugs. The tests that #4653 added as ignored now run.

A transfer installed below the head of an adopted StartView rewound
the sequencer to the transfer op. Peers then dropped its
DoViewChange as unreadable, and a transfer past the 128-entry suffix
window left a gap between the vote's commit point and its suffix.
The install now raises the commit floor and never lowers the head,
like the metadata install and a restart. It also counts its journal
wipe and checkpoint restore as journal mutations, so the next vote
rebuilds its cached suffix.

A restart before repair still lost that head. The install and a
fenced view window both certify the adopted view for the prefix that
the WAL holds. Recovery then counted the view as proven and skipped
the adopted head. The replica voted for the view with its head at
the install op. That can stall a view change, and with persisted
durability it can cut a committed op. A restart now also resumes the
adopted view when the recovered head is below the adopted head.

Under steady writes, journal repair fetched committed ops past the
op the offer pinned. The stale guard protected every held op up to
the higher of the WAL head and commit_max, so it refused every
offer. It now protects held ops only up to the accepted WAL head, or
commit_max without a WAL: every partition ack waits for that head,
so this replica never acknowledged an op above it. Such a refusal
now has its own error.

A replica in a view change moved to the next view on each status
timeout. When its messages took longer than that to arrive, each
StartViewChange that landed pulled the healthy pair out of the view
they had just settled in. It now holds its view while it lacks the
votes for its DoViewChange and an older view's primary still sends
it heartbeats. Without such heartbeats it still escalates, because
Recovering peers never send a StartViewChange. A newer view ends the
hold, because the cluster already elected past it. Each hold logs at
INFO. Metadata consensus runs the same code, so the hold covers its
view changes too.
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.

3 participants