Skip to content

symfonic.capabilities.delegation.lifecycle

lifecycle

Releasing the children the parent built — and only those.

The ownership rule is the whole design. A child the parent compiled from a spec holds resources the parent caused to exist (a checkpointer pool, background consolidation work) and nobody else will ever release them. A child the caller handed over belongs to the caller, who may well be using it elsewhere; closing it on the parent's shutdown would break a perfectly healthy agent.

Two verbs, in one order, for one reason. flush waits for fire-and-forget background work; aclose releases resources. Flushing after closing would wait for work whose backing store had already gone away, which is why the order is fixed here rather than left to each caller — and why a flush that fails still reaches the close, from an inner finally. A child closed with its consolidation unflushed loses memory the adopter believes was written; a child left open because its flush raised leaks a pool forever. The second is worse.

ChildLifecycle

ChildLifecycle(owned: Sequence[Any])

Owns the teardown of the children a compilation constructed.

Source code in src/symfonic/capabilities/delegation/lifecycle.py
def __init__(self, owned: Sequence[Any]) -> None:
    # Copied at construction: a caller's list that grew afterwards would
    # hand this object children it never agreed to own.
    self._owned: tuple[Any, ...] = tuple(owned)
    self._closed = False

owned property

owned: tuple[Any, ...]

The children this lifecycle will release.

Still readable after teardown. Idempotency is a flag, not an emptied list, because "what did this parent own?" is a question asked during an investigation of a shutdown that went wrong.

aclose async

aclose() -> TeardownReport

Release each owned child's resources. Idempotent.

Source code in src/symfonic/capabilities/delegation/lifecycle.py
async def aclose(self) -> TeardownReport:
    """Release each owned child's resources. Idempotent."""
    if self._closed:
        return TeardownReport(verb="aclose")
    self._closed = True
    return await self._sweep("aclose", "aclose")

flush async

flush() -> TeardownReport

Await each child's pending background work.

Never raises for a child's sake. One child whose consolidation is wedged must not strand the others' — including their aclose, which runs after this.

Source code in src/symfonic/capabilities/delegation/lifecycle.py
async def flush(self) -> TeardownReport:
    """Await each child's pending background work.

    Never raises for a child's sake. One child whose consolidation is
    wedged must not strand the others' — including their ``aclose``, which
    runs after this.
    """
    return await self._sweep("flush", "flush_background_tasks")

shutdown async

shutdown() -> TeardownReport

Flush, then close, whatever the flush did.

The inner finally is the contract: a raising flush cannot skip the close. Both phases report into one record so a caller sees everything that went wrong, not only whatever failed last.

Source code in src/symfonic/capabilities/delegation/lifecycle.py
async def shutdown(self) -> TeardownReport:
    """Flush, then close, whatever the flush did.

    The inner ``finally`` is the contract: a raising flush cannot skip the
    close. Both phases report into one record so a caller sees everything
    that went wrong, not only whatever failed last.
    """
    flushed = TeardownReport(verb="flush")
    try:
        flushed = await self.flush()
    finally:
        closed = await self.aclose()
    return flushed.merge(closed)