Skip to content

The root container closes before after_shutdown hooks registered after setup_di #82

Closed

Description

@lesnik512

This was generated by AI.

Summary

The root container closes before some of the user's shutdown code runs. starlette #58, fastapi #74 and litestar #79 fixed the same bug.

Current behavior

setup_di registers the close as an after_shutdown hook when it is called. So:

  • after_shutdown hooks registered after setup_di resolve from a closed container (ContainerClosedError), and APP finalizers have already run.
  • A FastStream(lifespan=...) exits after every after_shutdown hook, so its teardown always runs after the close.

on_shutdown hooks are fine in either order.

Desired behavior

The root opens before any user startup code and closes after all user shutdown code that the integration can reach.

Acceptance criteria

  • A failing test first: an after_shutdown hook registered after setup_di can resolve an APP dependency, and the finalizer runs after it.
  • Register the close so it runs after every after_shutdown hook, for example by appending it from an on_startup hook, with a guard so a restarted app does not register it twice.
  • manage_lifespan=False still neither opens nor closes the root.
  • The README says that a FastStream(lifespan=...) teardown runs after the close, and points to manage_lifespan=False for that case.

Out of scope

  • Running the close after FastStream(lifespan=...); FastStream has no public hook for that.
Repro
import asyncio
import collections.abc
import contextlib
import importlib.metadata

import faststream
from faststream.nats import NatsBroker, TestNatsBroker
from modern_di import Container, Group, Scope, providers
from modern_di_faststream import setup_di

events: list[str] = []


class Pool:
    pass


def close_pool(_: Pool) -> None:
    events.append("finalized")


class Dependencies(Group):
    pool = providers.Factory(Pool, scope=Scope.APP, cache=providers.CacheSettings(finalizer=close_pool))


def use(container: Container) -> None:
    try:
        container.resolve(Pool)
        events.append("user:live")
    except Exception as exc:  # noqa: BLE001
        events.append(f"user:{type(exc).__name__}")


def make_lifespan(container: Container) -> collections.abc.Callable[[], contextlib.AbstractAsyncContextManager[None]]:
    @contextlib.asynccontextmanager
    async def lifespan() -> collections.abc.AsyncIterator[None]:
        yield
        use(container)

    return lifespan


async def run(way: str, order: str) -> None:
    events.clear()
    container = Container(groups=[Dependencies])
    broker = NatsBroker()
    lifespan = make_lifespan(container) if way == "lifespan=" else None
    app = faststream.FastStream(broker, lifespan=lifespan)

    async def hook() -> None:
        use(container)

    def register() -> None:
        if way == "on_shutdown":
            app.on_shutdown(hook)
        elif way == "after_shutdown":
            app.after_shutdown(hook)

    if order == "before":
        register()
    setup_di(app, container)
    if order == "after":
        register()
    started = asyncio.Event()
    app.after_startup(started.set)
    async with TestNatsBroker(broker):
        task = asyncio.create_task(app.run())
        await started.wait()
        container.resolve(Pool)
        app.exit()
        await task
    print(f"{way:15} {order:6} {events}")


async def main() -> None:
    for name in ("faststream", "modern-di", "modern-di-faststream"):
        print(name, importlib.metadata.version(name))
    for way in ("on_shutdown", "after_shutdown"):
        for order in ("before", "after"):
            await run(way, order)
    await run("lifespan=", "before")


asyncio.run(main())

Activity

  1. added
    bugSomething isn't working
    ready-for-agentFully specified, ready for an AFK agent
    on Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions