From 2475e8f20fd87ebebdc826aa007769054eb26b6b Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Tue, 16 Jun 2026 17:04:17 +0300 Subject: [PATCH] fix: reopen root container on startup across broker restarts setup_di closed the root container via after_shutdown but never reopened it, so a broker restart (or repeated TestApp cycles) left the root closed and the DI middleware raised ContainerClosedError when building a request child. Pair the shutdown close with `app.on_startup(container.open)`, which reopens the root before the broker consumes. Requires modern-di>=2.19.0 for Container.open(). Co-Authored-By: Claude Opus 4.8 (1M context) --- modern_di_faststream/main.py | 5 +++++ pyproject.toml | 2 +- tests/test_lifespan.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 tests/test_lifespan.py diff --git a/modern_di_faststream/main.py b/modern_di_faststream/main.py index b83cc10..c0eac32 100644 --- a/modern_di_faststream/main.py +++ b/modern_di_faststream/main.py @@ -68,6 +68,11 @@ def setup_di( container.providers_registry.add_providers(faststream_message_provider) app.context.set_global("di_container", container) + # FastStream's lifecycle is callback-based, so the root container can't be + # wrapped in ``async with``. Reopen it on startup (before the broker consumes) + # to pair with the shutdown close, so a broker restart works instead of + # raising ContainerClosedError. Reopening an already-open container is a no-op. + app.on_startup(container.open) app.after_shutdown(container.close_async) # _DIMiddlewareFactory.__call__ ParamSpec doesn't structurally match BrokerMiddleware[Any, Any]. app.broker.add_middleware(_DIMiddlewareFactory(container)) # ty: ignore[invalid-argument-type] diff --git a/pyproject.toml b/pyproject.toml index 17fe9ec..7b61183 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Typing :: Typed", "Topic :: Software Development :: Libraries", ] -dependencies = ["faststream>=0.7,<0.8", "modern-di>=2.16.1,<3"] +dependencies = ["faststream>=0.7,<0.8", "modern-di>=2.19.0,<3"] version = "0" [project.urls] diff --git a/tests/test_lifespan.py b/tests/test_lifespan.py new file mode 100644 index 0000000..0b0ea27 --- /dev/null +++ b/tests/test_lifespan.py @@ -0,0 +1,36 @@ +import typing + +import faststream +from faststream import TestApp +from faststream.nats import NatsBroker, TestNatsBroker + +from modern_di_faststream import FromDI, fetch_di_container +from tests.dependencies import Dependencies, SimpleCreator + + +TEST_SUBJECT = "test" + + +async def test_startup_reopens_container_across_cycles(app: faststream.FastStream) -> None: + broker = typing.cast(NatsBroker, app.broker) + container = fetch_di_container(app) + + @broker.subscriber(TEST_SUBJECT) + async def index_subscriber( + message: str, + instance: typing.Annotated[SimpleCreator, FromDI(Dependencies.app_factory)], + ) -> None: + assert message == "test" + assert isinstance(instance, SimpleCreator) + + async with TestNatsBroker(broker) as br: + # First lifecycle: after_shutdown closes the root container. + async with TestApp(app): + await br.publish("test", TEST_SUBJECT) + assert container.closed + + # Second lifecycle: on_startup must reopen the same container, so the + # middleware can build a request child instead of raising ContainerClosedError. + async with TestApp(app): + assert not container.closed + await br.publish("test", TEST_SUBJECT)