symfonic.kernel.finalizers¶
finalizers ¶
The run's finalizer stack, unwound in two phases around the drain (RCX-10).
Its own module because the stack and the coordinator answer different
questions. :class:~symfonic.kernel.lifecycle.RunLifecycle decides what a run
owns and in what order the pieces of teardown happen; this module knows only
how one pile of callables is popped, bounded, and reported. Keeping the second
here is what lets the first stay readable as a sequence of teardown steps.
The phase split is the load-bearing idea. Owned background work is drained
between the two passes, so post_drain — the default — keeps a resource
alive for as long as the run's own tasks might still write through it, while
pre_drain covers the inverse dependency: a finalizer the drain is itself
waiting on.
FinalizerStack ¶
Cleanup registered by a run, popped LIFO within each phase.
Source code in src/symfonic/kernel/finalizers.py
unwind
async
¶
Pop this phase's finalizers LIFO, never letting one stop the rest.
A finalizer that raises is recorded, not propagated: teardown runs on the failure path too, and a cleanup error that replaced the original exception would hide the reason the run failed behind the reason the cleanup did. Finalizers belonging to the other phase are put back in registration order, so the second pass still unwinds LIFO.
Source code in src/symfonic/kernel/finalizers.py
teardown_budget ¶
The teardown bound, or an unexpiring scope when the grace is zero.
0 is documented as "cancel owned work immediately" (RCX-10), and the
background drain honours exactly that. Applying it to teardown's awaits
as well would cancel every flush, close, release, and terminal delivery
that suspends even once — silent data loss sold as a bound.
asyncio.timeout(None) expresses "no bound" without a second code path,
and reports expired() is False forever, which is the truth.