From d3ccc69761eedb897c2c83da2460607bb80a9fb7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 08:56:36 +0000 Subject: [PATCH 1/3] test(exec): deflake zero-second deadline budget tests on coarse timers The nightly build failed on `exec::budget::tests::deadline_trips_when_elapsed` with `left: Ok(())`, `right: Err(Deadline { limit_secs: 0 })`. Root cause: the deadline trips when `started.elapsed() > limit`. With a zero-second `max_duration`, that requires the monotonic clock to have advanced at least one tick past creation. `Instant`'s resolution is coarse on some platforms (notably the Windows nightly runner), so the first `charge_operation` could land within the same tick and read `elapsed() == 0`, making `0 > 0` false and returning `Ok(())` instead of the expected deadline error. Fix is test-only and leaves the production `>` ("trip once you exceed") deadline semantics unchanged: spin until `elapsed()` moves off zero before asserting, so the zero-second deadline is genuinely elapsed. Apply the same guard to the sibling `pattern_meter_deadline_exemption_is_read_live`, which shares the latent timing dependency. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01S8qrho14PuMRudxgh1hVUm --- src/exec/budget.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/exec/budget.rs b/src/exec/budget.rs index 8f5a4eea..fe64a19c 100644 --- a/src/exec/budget.rs +++ b/src/exec/budget.rs @@ -1148,7 +1148,15 @@ mod tests { let mut limits = tiny_limits(); limits.max_duration = Some(Duration::from_secs(0)); let budget = ExecutionBudget::new(limits); - // A zero-second deadline is already elapsed at the first sample point. + // The deadline trips when `elapsed() > limit`, so a zero-second deadline + // is only "elapsed" once the monotonic clock has advanced at least one + // tick past creation. `Instant`'s resolution is coarse on some platforms + // (notably Windows), where the first charge can otherwise land within the + // same tick and read `elapsed() == 0`. Spin until the clock moves so the + // assertion is deterministic rather than racing the timer. + while budget.elapsed() == Duration::ZERO { + std::hint::spin_loop(); + } assert_eq!( budget.charge_operation(true), Err(BudgetExceeded::Deadline { limit_secs: 0 }) @@ -1244,6 +1252,13 @@ mod tests { let mut limits = tiny_limits(); limits.max_duration = Some(Duration::from_secs(0)); let budget = Arc::new(ExecutionBudget::new(limits)); + // See `deadline_trips_when_elapsed`: the zero-second deadline only trips + // once the monotonic clock has moved past creation. Wait for the first + // tick so the post-main-loop assertion below is deterministic on + // coarse-resolution timers (e.g. Windows). + while budget.elapsed() == Duration::ZERO { + std::hint::spin_loop(); + } // ONE meter reused across a main-loop boundary (the VM-reuse case); // `reset()` runs per top-level op, so each op's first charge (index 0) is // a sample point. From 7e74a202871a2c68f54c08b9421c3f2abe9c270c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 09:10:52 +0000 Subject: [PATCH 2/3] test(exec): bound the deadline-test clock wait so it fails loudly Address Copilot review on #634: the spin-wait that lets the monotonic clock advance past a zero-second deadline was unbounded, so a pathologically frozen clock could hang the test suite instead of failing. Factor the wait into a shared `wait_for_clock_to_advance` helper bounded by an iteration cap (not a wall-clock timeout, which would depend on the very clock under suspicion). If the clock never advances it panics with a clear message rather than deadlocking CI. Both `deadline_trips_when_elapsed` and `pattern_meter_deadline_exemption_is_read_live` now use it. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01S8qrho14PuMRudxgh1hVUm --- src/exec/budget.rs | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/exec/budget.rs b/src/exec/budget.rs index fe64a19c..fb52e5d2 100644 --- a/src/exec/budget.rs +++ b/src/exec/budget.rs @@ -1107,6 +1107,27 @@ mod tests { } } + /// Spin until the budget's monotonic clock has advanced past creation, so a + /// zero-second deadline is genuinely elapsed (`elapsed() > 0`). `Instant` + /// resolution is coarse on some platforms (notably Windows), where the first + /// charge can otherwise land within the same tick and read `elapsed() == 0`. + /// The wait is bounded by an iteration cap — not a wall-clock timeout, which + /// would depend on the very clock we suspect — so a pathologically frozen + /// clock fails loudly instead of hanging the suite. + fn wait_for_clock_to_advance(budget: &ExecutionBudget) { + const MAX_SPINS: u64 = 100_000_000; + for _ in 0..MAX_SPINS { + if budget.elapsed() != Duration::ZERO { + return; + } + std::hint::spin_loop(); + } + panic!( + "monotonic clock did not advance past budget creation within {MAX_SPINS} spins; \ + cannot exercise the zero-second deadline" + ); + } + #[test] fn operation_ceiling_trips_after_limit() { let budget = ExecutionBudget::new(tiny_limits()); @@ -1149,14 +1170,10 @@ mod tests { limits.max_duration = Some(Duration::from_secs(0)); let budget = ExecutionBudget::new(limits); // The deadline trips when `elapsed() > limit`, so a zero-second deadline - // is only "elapsed" once the monotonic clock has advanced at least one - // tick past creation. `Instant`'s resolution is coarse on some platforms - // (notably Windows), where the first charge can otherwise land within the - // same tick and read `elapsed() == 0`. Spin until the clock moves so the - // assertion is deterministic rather than racing the timer. - while budget.elapsed() == Duration::ZERO { - std::hint::spin_loop(); - } + // is only "elapsed" once the monotonic clock has advanced past creation. + // Wait for that (bounded) so the assertion is deterministic rather than + // racing the timer resolution. + wait_for_clock_to_advance(&budget); assert_eq!( budget.charge_operation(true), Err(BudgetExceeded::Deadline { limit_secs: 0 }) @@ -1253,12 +1270,10 @@ mod tests { limits.max_duration = Some(Duration::from_secs(0)); let budget = Arc::new(ExecutionBudget::new(limits)); // See `deadline_trips_when_elapsed`: the zero-second deadline only trips - // once the monotonic clock has moved past creation. Wait for the first - // tick so the post-main-loop assertion below is deterministic on - // coarse-resolution timers (e.g. Windows). - while budget.elapsed() == Duration::ZERO { - std::hint::spin_loop(); - } + // once the monotonic clock has moved past creation. Wait (bounded) so the + // post-main-loop assertion below is deterministic on coarse-resolution + // timers (e.g. Windows). + wait_for_clock_to_advance(&budget); // ONE meter reused across a main-loop boundary (the VM-reuse case); // `reset()` runs per top-level op, so each op's first charge (index 0) is // a sample point. From bd2b35697c2a0634134305eed9e390263a8984e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 09:14:25 +0000 Subject: [PATCH 3/3] test(exec): yield periodically in the deadline-test clock wait Address Copilot review on #634: the bounded wait was a tight busy-wait that could monopolise a core until the next timer tick (~15ms on Windows) on a loaded CI runner. Yield to the scheduler every 1024 iterations instead of spinning every one; this stays deterministic, is friendlier under load, and lets the wall clock advance sooner so the wait usually exits earlier. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01S8qrho14PuMRudxgh1hVUm --- src/exec/budget.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/exec/budget.rs b/src/exec/budget.rs index fb52e5d2..64e4de8f 100644 --- a/src/exec/budget.rs +++ b/src/exec/budget.rs @@ -1116,11 +1116,20 @@ mod tests { /// clock fails loudly instead of hanging the suite. fn wait_for_clock_to_advance(budget: &ExecutionBudget) { const MAX_SPINS: u64 = 100_000_000; - for _ in 0..MAX_SPINS { + for i in 0..MAX_SPINS { if budget.elapsed() != Duration::ZERO { return; } - std::hint::spin_loop(); + // Cheap CPU hint most iterations, but yield to the scheduler + // periodically so we don't monopolise a core while waiting for the + // next timer tick (which can be ~15ms on Windows) on a loaded CI + // runner. Yielding also lets the wall clock advance sooner under + // contention, so the wait usually exits earlier. + if i % 1024 == 0 { + std::thread::yield_now(); + } else { + std::hint::spin_loop(); + } } panic!( "monotonic clock did not advance past budget creation within {MAX_SPINS} spins; \