aboutsummaryrefslogtreecommitdiffstats
path: root/src/simtest
diff options
context:
space:
mode:
Diffstat (limited to 'src/simtest')
-rw-r--r--src/simtest/remakes.rs110
1 files changed, 105 insertions, 5 deletions
diff --git a/src/simtest/remakes.rs b/src/simtest/remakes.rs
index 1c76520..0b4ee53 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`),
@@ -327,9 +328,29 @@ async fn coordinator_cut_off_after_committing_rejoins_the_others() {
write(&b, "data", json!(1)).await;
sim.wait_converged(&[&a, &b, &c], SOON).await;
- let cut = LinkParams {
- loss: 1.0,
- ..LinkParams::default()
+ // Nothing of a's reaches b and c, its epoch least of all (every message that carries
+ // it is lost), but its PONGs: b and c keep hearing a, so a stays their neighbour, and
+ // their votes go to it however late they come. With every message of a's lost, b and
+ // c would hold a dormant after a second, push nothing to it any more, and a vote cast
+ // later than that would never reach it.
+ let cut = {
+ use crate::transport::MessageKind::*;
+ LinkParams {
+ drop: vec![
+ Hello,
+ HelloReply,
+ Sync,
+ Status,
+ StatusReply,
+ Blob,
+ Who,
+ Me,
+ Ping,
+ Links,
+ LinksReply,
+ ],
+ ..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());
@@ -338,7 +359,7 @@ async fn coordinator_cut_off_after_committing_rejoins_the_others() {
&& testing::report(c_ref, lost_ref).await.is_some()
})
.await;
- // Their votes still reach a; nothing from a reaches them.
+ // Their votes still reach a; nothing of a's but its PONGs reaches them.
for other in ["b", "c"] {
sim.net.set_link("a", other, cut.clone());
}
@@ -494,3 +515,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)));
+}