aboutsummaryrefslogtreecommitdiffstats
path: root/src/simtest/remakes.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-01 13:41:04 +0100
committertv <tv@krebsco.de>2026-11-01 13:41:04 +0100
commit399271b035e222d791f2505946fcb453f7df8f8d (patch)
tree82655b42056dc313f3dc52ef715e908d6d6797f1 /src/simtest/remakes.rs
parent83ed4e966bf4b7a2d145264ccb086d3ce3e3dd36 (diff)
add off clock abort test
Diffstat (limited to 'src/simtest/remakes.rs')
-rw-r--r--src/simtest/remakes.rs50
1 files changed, 49 insertions, 1 deletions
diff --git a/src/simtest/remakes.rs b/src/simtest/remakes.rs
index 2a53c65..1f8356a 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).
+//! older than the electorate window), 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`),
//! switch delay a keepalive, votes within half a keepalive.
@@ -445,3 +446,50 @@ async fn both_sides_of_an_old_partition_commit_and_converge() {
.await;
assert_eq!(read(&e, "data").await, Some(json!(1)));
}
+
+/// A node whose clock is ahead sees a remake expire early: aborting it from there is
+/// refused while the node knows its clock is off (`?force=clock` overrides), as its
+/// coordinator, alive, may still commit it.
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+async fn abort_from_a_node_with_its_clock_off_is_refused() {
+ use crate::remake::Host;
+
+ let sim = Sim::new(39);
+ let (a, b, c) = three(&sim).await;
+ // Far beyond the grace period (1 s) and --clock-skew-warn (2 s), and the settle
+ // period below: by c's clock, the remake is expired once proposed.
+ c.clock.set_offset_ms(60_000);
+ let c_ref = &c;
+ sim.wait_until("c knows its clock is off", SOON, move || async move {
+ c_ref.clock_skew().await.off
+ })
+ .await;
+
+ // No vote reaches a, so its remake stays undecided for its settle period.
+ let cut = LinkParams {
+ loss: 1.0,
+ ..LinkParams::default()
+ };
+ sim.net.set_link("b", "a", cut.clone());
+ sim.net.set_link("c", "a", cut);
+ let id = propose(&a, json!({ "kind": "compact", "settle": "30s" })).await;
+ let id_ref = id.as_str();
+ sim.wait_until(
+ "c has the proposal, expired by its clock",
+ SOON,
+ move || async move {
+ testing::report(c_ref, id_ref)
+ .await
+ .is_some_and(|report| report["phase"] == "expired")
+ },
+ )
+ .await;
+ let (status, refused) = testing::abort(c.clone(), &id, None).await;
+ assert_eq!(status, StatusCode::CONFLICT, "{refused}");
+ assert_eq!(refused["force"], "clock");
+ assert!(
+ testing::report(&b, &id)
+ .await
+ .is_some_and(|report| report["phase"] != "aborted")
+ );
+}