Skip to main content
Version: Development

Deterministic timing and cancellation tests

Use committed state and explicitly owned futures as readiness witnesses. A sleep, a yield count, a sent cancellation request, or a historical arrival is not proof that work is currently queued, executing, or retired. These helpers are test-only; production scheduling, deadline enforcement, resource receipts and phase gates do not depend on them.

Existing coordination inventory and the first migration​

Reviewed against 50f003dd006e0786494936c49e55dc683cf26fd6 on development. This is a bounded migration, not a replacement of all timing or containment tests.

OwnerExisting mechanismDecision
latent-testkit/src/deterministic.rsDeterministic IDs, raw nanosecond ManualClock, temporary directories, thread-parking block_onReuse IDs and executor. Preserve ManualClock for raw counters; its freely settable nanos are not an ActivationClock monotonic domain. Pair parked futures with a finite watchdog.
latent-admission/src/tests/stress.rsA 75 ms policy sleep expires a 50 ms live admission deadlineReplace this one deadline suite with shared TestClock, eight exact policy-time/wall-jump combinations, the production injected-clock path, unchanged deadline and quota assertions.
latent-admission/src/tests/incoming.rsLocal clock with read/sample counters and deadline diagnostic observationsRetain specialized observer/read-count tests. Shared helpers do not add hidden clock reads or replace these diagnostics.
latent-scheduler/src/fixed_pool/tests/races.rsRepeated barrier races, one explicit first-poll handshake, bounded yield-loop settlingMigrate the entire race suite. Explicitly poll the actual acquisition, assert live queue/accounting state, exercise both linearization orders and join cancelled owners. Add retained native-work/buffer ownership after client abandonment.
latent-scheduler/src/fixed_pool/tests/support.rsPrivate wall-clock adapter and observation/yield helpers used by other suitesKeep legacy wall-clock and real timer coverage outside this selection. Do not pretend that advancing TestClock controls these Tokio timers.
latent-scheduler/src/fixed_pool/mod.rsCommitted test-transition history and coalescing production change notificationsPreserve both production seams. A transition notification alone does not prove a task is still blocked; resample live state.
latent-node/tests/activation_runner_race_support/backend_and_pools.rsEntry/proceed barriers, backend and disposition doubles, finite rendezvous timeoutRetain activation-runner integration. Future migrations can use the same stages without importing the node into low-level tests.
latent-core/src/deadline_wait_observer.rsAffine counters around the actual manager-owned deadline waitRetain this production-boundary observer; manual timer registrations are a different measurement and never substitute for it.
latent-wasmtime/tests/containment_backend/rendezvous.rsMixed-memory guest arrivals plus explicit unfinished-task verification, timeout/failure cleanupRetain native containment: its current-liveness requirement informs the shared pause contract. No real Wasmtime or physical-resource test is replaced with a mock or virtual-time result.

The provisional local measurement IDs are admission.policy-deadline and scheduler.fixed-pool-races. Issues #426 (stage timing) and #427 (suite inventory) were still open without a landed shared contract at this baseline. The measurement script retains exact package, selector, names, source revision and counts so those owners can adopt these selections. It is not a new CI classifier or general Actions comparison engine. No additional phase dependency is introduced.

Clock domains​

TestClock::new(wall_unix_millis, monotonic, maximum_waiters) implements the existing latent_core::ActivationClock interface. Clones share one coherent sample. advance moves only monotonic time forward with checked arithmetic; set_wall_unix_millis independently changes wall observations. Convert an incoming wall deadline once through the existing production admission seam; never restart an already granted monotonic deadline when wall time changes.

sleep_until registers on its first poll, not construction. At the exact deadline, advance removes due registrations and wakes them outside the clock lock. Dropping a pending sleep removes its registration. Capacity is fixed, stale dropped timers do not consume reused capacity, and counter arithmetic never wraps.

Advancing this clock wakes only its own manual sleeps. An ActivationClock consumer's existing production wait mechanism is unchanged. Tokio start_paused and advance affect Tokio's timer driver, not this clock or std::time::Instant. Neither clock controls a native backend or child process. Use the real execution and process supervision boundaries for those. The helper tests retain a real SystemActivationClock/Tokio timer integration check and verify the separation between all three host clock domains.

The executable deadline example registers a timer, jumps wall time backwards, advances to just before expiry, and then asserts the exact wake and zero remaining registrations:

cargo run -p latent-testkit --no-default-features --example deadline --locked

Current readiness and owner retirement​

Rendezvous::new(capacity) retains fixed-size metadata, never work or buffers. track(owner) returns an opaque registration and a Tracked<T> owner. Record Requested -> Queued -> Entered -> CancellationObserved only after the relevant subsystem action commits. Legal bypasses include immediate entry and cancellation before entry. Retired cannot be manually committed: it is published only after the tracked value is dropped, including unwinding.

Tracked::pause registers a live gate on poll. blocked(registration, stage) checks the current stage and pause and issues a generation-bound ticket. Release, dropped pauses, recycled slots and tickets from another fixture cannot establish readiness for a later pause. A release clears the blocked witness immediately, not when the task happens to be scheduled again. Observers retain no owner.

PollProbe::pending performs one poll without executor luck. Also check the actual subsystem queue, reservation or lease state: an arbitrary pending future is not proof of queue registration. The scheduler migration deliberately keeps a queued acquisition unpolled after grant delivery so task abort tests cover both queued cleanup and delivery-before-acceptance cleanup exactly.

An abort request is not retirement. Join the task that actually owns the resource, then check both retirement and actual quota/cell/buffer accounting. Cancelling a client waiting on independently owned native work must not refund the worker's lease or buffer. The migrated scheduler test keeps that worker at a live gate, asserts charged ownership after client cancellation, and only permits retirement after the worker drops its buffer and cell owner.

The executable cancellation example uses a weak buffer reference to verify actual destruction, not only an observer counter:

cargo run -p latent-testkit --no-default-features --example cancellation --locked

Every new asynchronous wait uses with_watchdog or the watchdog inside pause. These finite real-clock guards detect deadlocks even with paused Tokio time; they are not readiness mechanisms or performance gates. The guard owns and joins its watchdog thread on success, cancellation and panic. A watchdog cannot preempt a blocking Future::poll; retain a finite outer process/job timeout for native or blocking code. Do not retry a failed test until it passes.

Dependency isolation and validation​

Low-level admission/scheduler tests select the neutral implementation directly:

[dev-dependencies]
latent-core = { path = "../latent-core", features = ["test-support"] }

Use latent_core::test_support::{TestClock, DeterministicIds} and latent_core::test_support::coordination::{Rendezvous, PollProbe, Stage} there. The standard-library-only helpers and their 21 unit tests live together under latent-core/src/test_support/. The feature is opt-in. Tokio and tempfile are dev-dependencies only of core, used to run the relocated tests; core has no production dependencies and no back edge into a workspace crate. No new crate, production clock hook or scheduling semantics are introduced.

latent-testkit re-exports those exact modules and types, including its existing root-level exports. Existing harness users and the executable examples retain their import paths. Its default runtime feature still gates the optional activation/executor/node/telemetry harness dependencies, but feature gating is not an exception to the workspace acyclicity rule. Upstream crates must not add a testkit dependency, even with default-features = false.

python3 tools/validate_foundation.py
python3 tools/check_testkit_dependencies.py
python3 -m unittest tools.tests.test_deterministic_tests tools.tests.test_testkit_dependencies
cargo test -p latent-core --features test-support --lib --locked test_support:: -- --test-threads=1
cargo test -p latent-core --features test-support --lib --locked test_support:: -- --test-threads=4
cargo test -p latent-admission -p latent-scheduler --lib --locked
cargo test -p latent-testkit --no-default-features --lib --test test_support_compatibility --locked
cargo test -p latent-testkit --lib --test test_support_compatibility --locked

The guard first invokes the existing foundation validator over all workspace manifest edges, including optional, development, build and target-specific edges. Only then does it inspect independently selected Cargo graphs, including their test dependencies. The Python regressions reconstruct the originally missed admission/testkit/node and scheduler/testkit/node cycles and require failure before Cargo is invoked. They also cover aliases, optional/target/build edges, missing feature selection, empty graphs and heavyweight dependencies. CI runs these regressions and the guard before the expensive workspace build; it does not weaken or bypass the foundation validator.

The relocated tests retain fixed IDs, explicit wakeups, missing readiness, stale/recycled/foreign tickets, premature retirement, buffer ownership, capacity limits, abort, panic and real watchdog expiry. Both current-thread and multi-thread Tokio runtimes exercise the controlled scripts. Re-export tests ensure both import surfaces use identical types and shared clock state. Existing ignored resource/qualification tests keep their separate explicit entrypoints.

Before/after execution evidence​

Build both source revisions before measuring the selected prebuilt test binaries. On the same host, run:

git worktree add /tmp/lsf-before-433 50f003dd006e0786494936c49e55dc683cf26fd6
python3 tools/measure_deterministic_tests.py \
--baseline /tmp/lsf-before-433 --output target/deterministic-tests.json

The script verifies nonempty discovery and exact expected test counts, then runs five required passing repetitions with one and four libtest threads. Each suite contains its own current-/multi-thread runtime cases where applicable. Baseline admission has one test with the live sleep; the migrated test has eight controlled subcases. Scheduler grows from three tests with 128 probabilistic race repetitions to seven tests covering both orders and owner-lifetime boundaries. Compilation is excluded from the reported process execution durations; startup and teardown are included. This does not claim equivalent interleavings or a whole-CI speedup. Failed/partial runs are retained as failed evidence, not included in success claims. There is no timing threshold and no retry-to-green policy.

Retained run on September 20, 2026​

The raw execution receipt is retained byte-for-byte from artifact 10606779086 in successful validation run 35516638297. Both source trees were clean. The runner was Ubuntu 24.04.5, x86-64 Linux, with Rust 1.97.1. All five repetitions of each selection passed; no failed execution was discarded or retried by the measurement script.

SuiteLibtest threadsBefore testsAfter testsBefore median (ms)After median (ms)
Admission policy deadline111 (8 controlled subcases)76.8461.956
Admission policy deadline411 (8 controlled subcases)76.9861.928
Scheduler fixed-pool races13727.1385.648
Scheduler fixed-pool races43717.4604.247

The before revision is 50f003dd006e0786494936c49e55dc683cf26fd6; the tested and measured after revision is b27113253dfaead86f549087fa31363c8bc43cda. The Actions run was triggered at e1fe49d941550e404e8c5fb8a110bf43f21a872b and committed formatting before executing its checks. The receipt records the actual checked-out revision, rather than mistaking the trigger revision for the measured source.

That run also passed 42 no-default-feature testkit tests under each of one and four libtest threads, 52 admission tests, 43 scheduler tests, 61 default-feature testkit tests, five Python tests, and both executable contributor examples. The admission and scheduler commands each retained one existing ignored qualification test; these were not counted as passes. The doctest command succeeded with zero cases. The independently selected dependency graphs had 21, 33 and 35 nodes for neutral testkit, admission and scheduler respectively, with no forbidden runtime/provider dependencies. Strict testkit/admission Clippy checks and documentation validation passed. Scheduler Clippy retained existing warnings outside the migrated module; the migrated module independently denies clippy::all and clippy::pedantic.

The temporary branch-only, write-enabled formatting/validation workflow was removed after retaining this evidence. Ordinary repository CI remains unchanged; the scoped run does not replace full PR, native-runtime or containment validation.

Dependency-cycle correction​

The initial b6ae23b publication failed the unconditional foundation graph: latent-admission -> latent-testkit -> latent-node -> latent-admission, with corresponding scheduler cycles. The earlier feature-selected 21/33/35-node graph observations below were not evidence of workspace acyclicity. The retained receipt is historical execution evidence for its named revisions, not a passing foundation result or a measurement of this corrected revision. The correction moves the shared implementation and all 21 helper tests into feature-gated core, removes both upstream testkit edges, and preserves the measured admission and scheduler case bodies apart from import paths. Testkit's remaining library counts therefore change; the original receipt and its source identities are not rewritten.