aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-01 19:58:16 +0100
committertv <tv@krebsco.de>2026-11-01 19:58:16 +0100
commita70409f0d26d64e411382301eb11fbb8ac8e0036 (patch)
tree23c5d2b37b2323a3573e28d34a9f842dd119ba17 /src
parent7993cdfe8f8e24b39091b10a0a1bd45d66e3ccd8 (diff)
add lost vote with restart test
Diffstat (limited to 'src')
-rw-r--r--src/simtest/remakes.rs82
1 files changed, 81 insertions, 1 deletions
diff --git a/src/simtest/remakes.rs b/src/simtest/remakes.rs
index 1c76520..6a1f0a9 100644
--- a/src/simtest/remakes.rs
+++ b/src/simtest/remakes.rs
@@ -2,7 +2,8 @@
//! proposals across a partition, a node that misses the switch, a coordinator that is
//! 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; electorates of a partition
-//! older than the electorate window), and a node with its clock ahead that would abort
+//! older than the electorate window; a vote lost with a restart), and a node with its
+//! clock ahead that would abort
//! a remake as expired while its coordinator still runs it.
//!
//! The simulated nodes' timings make a remake quick: settle 2 s (two `dormant_after`),
@@ -494,3 +495,82 @@ async fn abort_from_a_node_with_its_clock_off_is_refused() {
.is_some_and(|report| report["phase"] != "aborted")
);
}
+
+/// A vote lost with a restart: v votes on one side of a partition, restarts without a
+/// document file, joins the other side, which never saw the remake, and votes there
+/// for another. Each side reaches its majority with v, and both commit. Once the
+/// partition heals, the side fewer nodes are on rejoins the other's epoch sideways.
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn a_vote_lost_with_a_restart_and_the_swarm_converges() {
+ let sim = Sim::new(40);
+ let a = sim.start("a", &[], Start::Genesis);
+ let [b, v, d, e] = ["b", "v", "d", "e"].map(|id| sim.start(id, &["a"], Start::Join));
+ sim.wait_ready(&[&a, &b, &v, &d, &e], SOON).await;
+ let nodes = [a.clone(), b.clone(), v.clone(), d.clone(), e.clone()];
+ sim.wait_until("every node is registered", SOON, || {
+ let nodes = nodes.clone();
+ async move {
+ for node in &nodes {
+ if crate::swarm::members::members(&node.doc.read().await.doc).len() < 5 {
+ return false;
+ }
+ }
+ true
+ }
+ })
+ .await;
+ write(&a, "data", json!(1)).await;
+ sim.wait_converged(&[&a, &b, &v, &d, &e], SOON).await;
+
+ let cut = LinkParams {
+ loss: 1.0,
+ ..LinkParams::default()
+ };
+ let link = |side: &[&str], other: &[&str], params: &LinkParams| {
+ for one in side {
+ for two in other {
+ sim.net.set_link(one, two, params.clone());
+ sim.net.set_link(two, one, params.clone());
+ }
+ }
+ };
+ link(&["a", "b", "v"], &["d", "e"], &cut);
+
+ // a, b and v: three of five, X commits.
+ let x = propose(&a, json!({ "kind": "compact" })).await;
+ let (a_ref, x_ref) = (&a, x.as_str());
+ sim.wait_until("X committed", REMAKE, move || async move {
+ testing::report(a_ref, x_ref)
+ .await
+ .is_some_and(|report| report["phase"] == "committed")
+ })
+ .await;
+
+ // v restarts on the other side, with nothing of what it had: no record of X.
+ link(&["v"], &["a", "b"], &cut);
+ link(&["v"], &["d", "e"], &LinkParams::default());
+ let v = sim.restart_with("v", &["d"], Start::Join, |_| {});
+ sim.wait_ready(&[&v], SOON).await;
+
+ // d, e and v: three of five, Y commits too.
+ propose(&d, json!({ "kind": "compact" })).await;
+ on_generation(&sim, &[&a, &b], 1).await;
+ on_generation(&sim, &[&d, &e, &v], 1).await;
+ let more = same_epoch(&[&d, &e, &v]);
+ assert_ne!(same_epoch(&[&a, &b]), more, "two epochs of generation 1");
+
+ link(&["a", "b"], &["v", "d", "e"], &LinkParams::default());
+ let (b_ref, more_ref) = (&b, more);
+ sim.wait_until(
+ "a and b are on the epoch more nodes are on",
+ REMAKE,
+ move || async move {
+ a_ref.local_epoch() == more_ref
+ && b_ref.local_epoch() == more_ref
+ && a_ref.is_ready().await
+ && b_ref.is_ready().await
+ },
+ )
+ .await;
+ assert_eq!(read(&b, "data").await, Some(json!(1)));
+}