From 465cda548c3a47dff8b3ed32fbe814df6601065b Mon Sep 17 00:00:00 2001 From: tv Date: Sun, 1 Nov 2026 13:41:04 +0100 Subject: add off clock abort test --- src/simtest/remakes.rs | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) 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") + ); +} -- cgit v1.3.1