Repository navigation
The root container closes before after_shutdown hooks registered after setup_di #82
Copy link
Copy link
Closed
Labels
bugSomething isn't workingSomething isn't workingready-for-agentFully specified, ready for an AFK agentFully specified, ready for an AFK agent
Description
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_shutdownhooks registered aftersetup_diresolve from a closed container (ContainerClosedError), and APP finalizers have already run.- A
FastStream(lifespan=...)exits after everyafter_shutdownhook, 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_shutdownhook registered aftersetup_dican resolve an APP dependency, and the finalizer runs after it. - Register the close so it runs after every
after_shutdownhook, for example by appending it from anon_startuphook, with a guard so a restarted app does not register it twice. -
manage_lifespan=Falsestill neither opens nor closes the root. - The README says that a
FastStream(lifespan=...)teardown runs after the close, and points tomanage_lifespan=Falsefor 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())Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't workingready-for-agentFully specified, ready for an AFK agentFully specified, ready for an AFK agent
Activity