symfonic.kernel.contracts.run_census¶
run_census ¶
Tasks the kernel's task factory never saw: they fail closed.
run_spawn records the spawner's lineage only for a task built through the
kernel's factory. A host can build one without it: replace the loop's factory
(and even restore it before the task's run), or construct asyncio.Task
directly. Such a task, spawned with a stripped context inside a run, would
start a fresh root and launder what it was handed after a forget (CORE-199).
Nothing records when such a task was created, so the kernel takes a census of
the loop's tasks: when a task it never saw asks for the lineage it was spawned
under, and at least every :data:CENSUS_EVERY runs issued on the loop. Each
unseen task the census finds descends from every run that could have been
live when it was created -- every run alive at the previous census, held until
this one, and every run issued since. That over-approximates on purpose (the
owner's fail-closed rule): a forget of any scope those runs captured blocks
the unseen task, as it blocks a detached worker. A host that wants exact
lineage keeps the kernel's factory or chains to it -- wrapping the coroutine
first, as Sentry's patch_asyncio does, keeps it exact too, since the
kernel records the task its inner factory returns. A task the census misses
-- not enumerated, or gone from its registry before the lookup -- still
descends from a window, never starts a root.
When a loop's first run is issued from a task -- which predates the kernel's factory -- the census runs before that run exists, so tasks already on the loop start as roots. If a run precedes the loop's first census (one issued from a callback outside any task), earlier tasks fail closed like the rest.
Retention: a run's lineage, and every fence it captured, stays alive until
the next census on its loop -- forced within :data:CENSUS_EVERY further runs
there; a loop that issues no more runs keeps it until it closes and another
loop begins -- so a task built during the run can still inherit it. A run
issued meanwhile that names the ended run as its parent or root descends from
it too.
Cost (measured on one CPython 3.12 host): a census walks
asyncio.all_tasks(loop) once, about 0.45 us per live task -- 0.5 ms at
2,000 tasks, 3 ms at 12,000, 23 ms at 52,000. A loop pays it once every
:data:CENSUS_EVERY runs, which adds about 1.5-2.5 us per run up to 1,000
live tasks and about 12 us at 10,000, and again whenever an unseen task the
last census missed asks for its lineage. Building a task through the kernel's
factory costs about 4.5 us more on a CPython 3.12 dev host (1.9 us before
the build mark), since every task is recorded and every build is marked
where the new task runs, for an eager first step (run_build).
A run in an
unseen task re-proves every fence its window holds on each mutation -- one
generation read per distinct fence -- so the bypass path, not the default one,
pays for the over-approximation.
census ¶
The census of loop, begun now if this is its first run.
Source code in src/symfonic/kernel/contracts/run_census.py
note_issued ¶
Hold a run issued on the running loop until a census has seen it.
Source code in src/symfonic/kernel/contracts/run_census.py
take_census ¶
Classify every task on loop the kernel has not seen; started loops only.
unseen_lineage ¶
The lineage an unseen task descends from; None for a root.