aboutsummaryrefslogtreecommitdiffstats
path: root/src/simtest
diff options
context:
space:
mode:
Diffstat (limited to 'src/simtest')
-rw-r--r--src/simtest/remakes.rs60
1 files changed, 59 insertions, 1 deletions
diff --git a/src/simtest/remakes.rs b/src/simtest/remakes.rs
index ab04efe..b109f0d 100644
--- a/src/simtest/remakes.rs
+++ b/src/simtest/remakes.rs
@@ -1,6 +1,7 @@
//! Remakes on whole nodes (`docs/remakes.md`): a compaction everywhere, competing
//! proposals across a partition, a node that misses the switch, a coordinator that is
-//! gone, a skewed clock, and writes while a node starts to rejoin.
+//! gone, a skewed clock, writes while a node starts to rejoin, and two remakes committed
+//! for one epoch (a coordinator cut off after it committed).
//!
//! The simulated nodes' timings make a remake quick: settle 2 s (two `dormant_after`),
//! switch delay a keepalive, votes within half a keepalive.
@@ -312,3 +313,60 @@ async fn writes_while_a_rejoin_starts_are_kept_or_refused() {
})
.await;
}
+
+/// A coordinator cut off right after it committed: the others never hear of the commit,
+/// abort the remake once it expired and commit another. The coordinator switches to its
+/// own; once back, it sees more peers on the other epoch of that generation, rejoins
+/// that one sideways (`catchup::sideways`), and the swarm is on one epoch again.
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn coordinator_cut_off_after_committing_rejoins_the_others() {
+ let sim = Sim::new(37);
+ let (a, b, c) = three(&sim).await;
+ write(&b, "data", json!(1)).await;
+ sim.wait_converged(&[&a, &b, &c], SOON).await;
+
+ let cut = LinkParams {
+ loss: 1.0,
+ ..LinkParams::default()
+ };
+ let lost = propose(&a, json!({ "kind": "compact" })).await;
+ let (a_ref, b_ref, c_ref, lost_ref) = (&a, &b, &c, lost.as_str());
+ sim.wait_until("b and c have the proposal", SOON, move || async move {
+ testing::report(b_ref, lost_ref).await.is_some()
+ && testing::report(c_ref, lost_ref).await.is_some()
+ })
+ .await;
+ // Their votes still reach a; nothing from a reaches them.
+ for other in ["b", "c"] {
+ sim.net.set_link("a", other, cut.clone());
+ }
+ sim.wait_until("a committed", SOON, move || async move {
+ testing::report(a_ref, lost_ref)
+ .await
+ .is_some_and(|report| report["phase"] == "committed")
+ })
+ .await;
+ sim.net.take_down("a");
+ on_generation(&sim, &[&a], 1).await;
+
+ // For b and c, the remake expired undecided.
+ sim.wait_until("b can abort the lost remake", REMAKE, move || async move {
+ let (status, _) = testing::abort(b_ref.clone(), lost_ref, None).await;
+ status == StatusCode::OK
+ })
+ .await;
+ propose(&b, json!({ "kind": "compact" })).await;
+ on_generation(&sim, &[&b, &c], 1).await;
+ let theirs = same_epoch(&[&b, &c]);
+ assert_ne!(a.local_epoch(), theirs, "two epochs of generation 1");
+
+ for other in ["b", "c"] {
+ sim.net.set_link("a", other, LinkParams::default());
+ }
+ sim.net.bring_up("a");
+ sim.wait_until("a is on b's and c's epoch", REMAKE, move || async move {
+ a_ref.local_epoch() == theirs && a_ref.is_ready().await
+ })
+ .await;
+ assert_eq!(read(&a, "data").await, Some(json!(1)));
+}