4.5 KiB
4.5 KiB
T1.1 — Ctx and RunScope
| Field | Value |
|---|---|
| Phase | P1 — Walking skeleton |
| Size | M — 1 to 3 days |
| Status | Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | T1.3, T7.x |
Goal
Explicit context threaded through every call, and a run-scoped task tree that cannot be escaped through the public API.
Facts (inlined — no spec read needed)
- Runtime is tokio. Chosen for ecosystem access (
sqlx,rdkafka,aws-sdk,tonic,axum,reqwest, object-store clients) — for a distributed framework those are the distribution layer, not optional deps. RunScopewrapsTaskTracker+CancellationTokenfromtokio-util, one tracker per run. It exists because tokio has no structural task tree: without the guard, a detached spawn outlives its run.Ctxis an explicit struct passed to every call. Nevertask_local!for anything causal — that is theAsyncLocalStoragemistake in different clothes. Trace context propagates throughCtxtoo.- Cancellation is cooperative: a
select!-dropped future stops at its next await and not before. Kernel code must never hold a lock or a half-applied state across an await that can be cancelled. - A dropped call resolves
Cancelledif no intent reachedDispatched, andIndeterminateif one did. The intent record decides, not the drop. Collapsing the two is the easy implementation and it puts a permanent noisy floor under theIndeterminatealert, which is meant to sit near zero.
Steps
- Define
Ctx { tenant, run, branch, cancel: CancellationToken, deadline, trace, log: Arc<dyn EventLog>, blobs: Arc<dyn BlobStore> }. Pass by&Ctx. - Define
RunScopeowning aTaskTrackerand aCancellationToken. Exposespawnonly as a method onRunScope; keep the inner tracker private so no caller reachestokio::spawnthrough the public API. RunScope::shutdown()/Drop: cancel the token,tracker.close(), then awaittracker.wait(). Returning before every child is complete or aborted is the bug this type exists to prevent.- In kernel code,
select!the cancellation token at every await. Audit for locks or partially-applied state held across those awaits. - Give user tool calls a hard timeout in addition to the token — a non-cooperative call must still be bounded.
- On cancellation, read the attempt's latest intent state to choose
CancelledvsIndeterminate. Do not decide at the drop site.
Acceptance
- Cancel a run mid-step; assert every spawned task has completed or aborted
before
RunScope::dropreturns. - The cancelled attempt lands in
Cancelled, notIndeterminate— asserted directly, since collapsing the two is the easy implementation and silently ruins the alert.
Verify
Harness: tokio::time::pause for deadlines; a scoped task that records its
own completion into a shared Arc<Mutex<Vec<_>>> so "aborted" and "completed"
are distinguishable.
Integration test — tests/it_runscope_cancel.rs:
- Start a run whose step spawns three children through
RunScope::spawn, each parked on a long await. - Cancel mid-step.
- Assert
RunScope::drop(orshutdown().await) returns only after all three have recorded completion-or-abort. Assert the recorded set has size 3 — a scope that returns early leaves it at 0 or 1. - Assert the attempt landed in
Cancelled, notIndeterminate, with no intent dispatched. - Second run: dispatch an intent, then cancel. Assert
Indeterminate. - Grep test (or a lint): assert no
tokio::spawnoutsideRunScope, and notask_local!anywhere in kernel crates.
Command: cargo test -p runtime runscope
False pass:
- Step 3 asserting only that
dropreturned. It always returns. The recorded child set is the evidence. - Steps 4 and 5 written against the same fixture with
intent_dispatchedhardcoded — they must run through the real intent record, or the distinction is asserted against itself. - A child that finishes on its own before cancellation lands, making the test green regardless of the tracker. Park children on a token that only the cancel releases.
Traps
- A public
spawnhelper "for convenience" that bypasses the tracker. - Storing tenant or run id in a task-local. It works until a spawn boundary, then attributes work to the wrong run.
Background (not required to do this task): rust-agentic-sys.md §5.1, §6, §15 · rust-agentic-task.md