Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion src/exec/budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,36 @@ 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 i in 0..MAX_SPINS {
if budget.elapsed() != Duration::ZERO {
return;
}
// 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; \
cannot exercise the zero-second deadline"
);
}

#[test]
fn operation_ceiling_trips_after_limit() {
let budget = ExecutionBudget::new(tiny_limits());
Expand Down Expand Up @@ -1148,7 +1178,11 @@ 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 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 })
Expand Down Expand Up @@ -1244,6 +1278,11 @@ 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 (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.
Expand Down
Loading