Skip to content

Runner: per-case execution budget and timeout (limit-exceeded provenance) #45

Description

@lann

The runner has no defense against hanging cases: one wedged case hangs the whole run (at --jobs 8, a worker) until the CI job limit kills everything with zero attribution. The no-retries policy makes a guard the only defense. Design settled in discussion; recording here.

Two budgets for two hang classes (each enforceable only where its class manifests):

  • --case-execution-budget <secs> (default 10, 0 disables): budget on actual wasm execution, approximated as epoch ticks observed by Store::epoch_deadline_callback (the callback only fires while wasm executes and missed ticks collapse on resume, so preemption under-counts — contention-robust, ~thread time). Catches CPU spins, where a host-side timer is useless because the executor thread is stuck inside wasm. Trips as a distinguishable error through the normal trap plumbing.
  • --case-timeout <secs> (default 120, 0 disables): wall clock from case start, enforced by racing the run future against a timer. Catches async wedges (pending().await, wedged stream reads — the findings-wasi:cli runner #3 class), where the epoch callback is useless because no wasm executes. Scheduled-time exclusion (wake→poll latency) deliberately omitted: one case per worker executor today makes it ~zero; revisit with Future: additive concurrent-tests interface (concurrent runs + streaming enumeration) #20.

Containment = the existing trap path: mark the case failed, abandon the session (store drop unwinds suspended fibers safely), fresh session next case. Both --jobs paths via a shared per-case helper (extracting it first — the sequential/parallel copy-paste divergence from #43).

Wire form: Provenance::HangGuard (reserved, never emitted) is renamed in its last safe window to limit-exceeded(string) — externally tagged, so unit provenances stay flat strings and this one serializes as {"limit-exceeded":"execution-budget"} / {"limit-exceeded":"case-timeout"}, leaving room for future limits (memory, fuel) as payload vocabulary rather than schema change. Status fail, one-line detail with the configured value, diagnostics-complete: false, fold's not-reached cause-attribution updated to match.

Enumeration is guarded too (a wedged registry constructor): trips there are run errors (exit 2), not case results.

Fixture: components/hang-fixture (broken by design): hang/spin (loop {} — epoch path), hang/pend (std::future::pending().await — timer path), hang/after (containment). Gated tests run it at 1s/2s budgets.

Out of scope: composed runner (it is the wasm; its guard is the outer wasmtime invocation) and jco/Node (can't preempt wasm). Follow-up issue covers context.progress keepalive + --hard-timeout (requires a wit/tests.wit growth-channel investigation: @since gates vs version-linkage across wit-bindgen/jco/wac).

Metadata

Metadata

Assignees

No one assigned

    Labels

    runnerComponents or hosts that execute suites

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions