diff --git a/docs/integrations/aiogram.md b/docs/integrations/aiogram.md index 8cff6daa..eff13c69 100644 --- a/docs/integrations/aiogram.md +++ b/docs/integrations/aiogram.md @@ -99,13 +99,34 @@ to a WebSocket. See [the scope hierarchy](../providers/scopes.md#what-each-scope ## Root container lifecycle -`setup_di` registers `container.open` on `dispatcher.startup` and `container.close_async` on +`setup_di` opens the root container on `dispatcher.startup` and closes it with `close_async()` on `dispatcher.shutdown`, and closing runs APP-scoped finalizers. `start_polling()` emits both events, and so does an aiohttp app wired with aiogram's `setup_application()`. +Since modern-di-aiogram 4.3.0, shutdown runs in this order: the plugin waits for the updates still +in flight, then the shutdown handlers run (the dispatcher's, then every sub-router's), and the root +closes last. aiogram can still be handling updates when it emits shutdown, for example when polling +with `handle_as_tasks=True` (the default), so without the wait those handlers could outlive the +container. Shutdown handlers registered after `setup_di` or on sub-routers can resolve +APP-scoped dependencies, and APP finalizers run last. `setup_di` sets this order on each startup, +so a shutdown handler added during or after startup gets it from the next startup on. + +The wait has no limit by default. Pass `shutdown_timeout` in seconds to cap it: + +```python +timed_dispatcher = Dispatcher() +setup_di(timed_dispatcher, Container(groups=[AppGroup]), shutdown_timeout=10) +``` + +When it expires, a warning on the `modern_di_aiogram` logger names how many updates are still in +flight, and the root closes anyway, so those updates get `ContainerClosedError` on their next +APP-scoped resolve. Some things can close before the wait starts, such as the bot session in +webhook mode; see the [modern-di-aiogram README](https://github.com/modern-python/modern-di-aiogram#shutdown). + Since modern-di-aiogram 4.2.0, `setup_di(dispatcher, container, manage_lifespan=False)` registers neither hook and leaves the root container's open and close to you, for a container shared with -another app; see [Lifecycle](../providers/lifecycle.md#closing-and-reopening). +another app; see [Lifecycle](../providers/lifecycle.md#closing-and-reopening). The plugin then does +not wait for updates in flight either. ## Framework context objects @@ -360,7 +381,7 @@ await test_report() | Symbol | Description | |---|---| -| `setup_di(dispatcher, container, *, auto_inject=False, manage_lifespan=True)` | Stores the container on the dispatcher, registers `aiogram_update_provider`/`aiogram_event_provider`, wires `dispatcher.startup`/`dispatcher.shutdown` to open/close the container (not with `manage_lifespan=False`), and installs the per-update middleware. With `auto_inject=True`, also wraps every handler already registered on the dispatcher at startup. | +| `setup_di(dispatcher, container, *, auto_inject=False, manage_lifespan=True, shutdown_timeout=None)` | Stores the container on the dispatcher, registers `aiogram_update_provider`/`aiogram_event_provider`, and installs the per-update middleware. Unless `manage_lifespan=False`, opens the container on `dispatcher.startup`; on shutdown waits for updates in flight (at most `shutdown_timeout` seconds when set) before the other shutdown handlers and closes the container after all of them. With `auto_inject=True`, also wraps every handler already registered on the dispatcher at startup. | | `FromDI(dependency)` | Marker (used with `@inject`) that resolves a provider or type from the per-update child container. | | `inject` | Decorator for a sync or `async def` aiogram handler; resolves its `FromDI`-annotated parameters. Not needed when `setup_di(..., auto_inject=True)` is used. Raises `RuntimeError` naming `setup_di` when an update reaches it without the middleware installed. | | `fetch_di_container(dispatcher)` | Returns the root `Container` stored on the dispatcher. | diff --git a/docs/integrations/celery.md b/docs/integrations/celery.md index fa2675a1..c82c0910 100644 --- a/docs/integrations/celery.md +++ b/docs/integrations/celery.md @@ -105,6 +105,12 @@ Under prefork each child process gets its own APP-scoped instances. `worker_init container and closing one with nothing cached are both no-ops, so the overlap is harmless. +Since modern-di-celery 4.2.0, the root closes after every other `worker_shutdown` and +`worker_process_shutdown` receiver, including ones connected after `setup_di`, so they can still +resolve APP-scoped dependencies and APP finalizers run last. The plugin moves its close to the end +of each signal when the worker or child process starts, so a receiver connected after that runs +after the close. On a warm shutdown, Celery lets running tasks finish before it sends these signals. + !!! note "Prefork with the spawn start method" Celery starts prefork children with fork on Linux and macOS. Spawn is used on Windows or when you force it with `billiard.set_start_method("spawn", force=True)`, and Celery then @@ -186,7 +192,7 @@ with container: | Symbol | Description | |---|---| -| `setup_di(app, container, *, manage_lifespan=True)` | Wire the APP-scope container into Celery: registers it for the app and opens/closes it on `worker_process_init`/`worker_process_shutdown` and `worker_init`/`worker_shutdown`, unless `manage_lifespan=False`. Returns the container. | +| `setup_di(app, container, *, manage_lifespan=True)` | Wire the APP-scope container into Celery: registers it for the app and opens/closes it on `worker_process_init`/`worker_process_shutdown` and `worker_init`/`worker_shutdown`, unless `manage_lifespan=False`. The close runs after every other receiver of the shutdown signal connected before the worker starts. Returns the container. | | `FromDI(provider_or_type)` | Marker for `Annotated[T, FromDI(...)]` in task signatures; accepts a provider instance or a plain type. | | `@inject` | Decorator that builds a `Scope.REQUEST` child container per call, resolves `FromDI`-annotated parameters from it, and closes the child container with `close_sync()` afterwards. Raises `RuntimeError` naming `setup_di` when a task reaches it without `setup_di` called. A task with `FromDI` parameters that also declares `*args`/`**kwargs` raises `TypeError` at decoration. | | `DITask` | `Task` subclass that applies `@inject` to a task's `run` method automatically; pass `task_cls=DITask` to `Celery(...)` or `base=DITask` to `@app.task(...)`. | diff --git a/docs/integrations/flask.md b/docs/integrations/flask.md index dc2c108e..eaae3c7f 100644 --- a/docs/integrations/flask.md +++ b/docs/integrations/flask.md @@ -156,6 +156,16 @@ Flask has no websocket concept, so the integration only ever opens one child sco `Scope.REQUEST` child of the root container and stores it on `flask.g`, and `teardown_appcontext` closes it with `close_sync()` once the request, error handling included, is done. +Since modern-di-flask 4.2.0, the child is usable from every request hook, whatever the order it was +registered in. It is built on first use, so a `before_request` hook registered before `setup_di` +can use `@inject`, and the close runs after every `teardown_appcontext` handler, including ones +registered after `setup_di`. + +!!! warning "`stream_with_context` on Flask 3.1.2 and later" + Flask 3.1.2 and later tear the request context down before the response body is iterated, so + the child is already closed by then. A `stream_with_context` generator that resolves REQUEST + dependencies gets `ContainerClosedError`. modern-di-flask 4.1.0 behaves the same. + Because the child closes with `close_sync()`, REQUEST-scoped finalizers must be sync. An async finalizer raises [`AsyncFinalizerInSyncCloseError`](../troubleshooting/async-finalizer-in-sync-close-error.md), and it or any other failing finalizer reaches Flask as a `FinalizerError` from the teardown. The diff --git a/docs/integrations/grpc.md b/docs/integrations/grpc.md index 03bcd7fc..3ce9b547 100644 --- a/docs/integrations/grpc.md +++ b/docs/integrations/grpc.md @@ -125,14 +125,18 @@ greeter_pb2_grpc.add_GreeterServicer_to_server(GreeterService(), server) container.validate() # after DIInterceptor(container) — it registers ServicerContext's provider server.add_insecure_port("[::]:50051") server.start() -server.wait_for_termination() +try: + server.wait_for_termination() +finally: + server.stop(grace=5).wait() # let in-flight RPCs finish before APP finalizers run + container.close_sync() ``` @@ -260,16 +265,11 @@ argument. gRPC has no server startup/shutdown hook, so the root container's lifecycle is yours to own (as with Flask). Build the container, pass it to the interceptor, and -close it after the server stops to run APP-scoped finalizers: - - - -```python -server.stop(grace=5).wait() -container.close_sync() # or: await container.close_async() on grpc.aio -``` +close it after the server stops to run APP-scoped finalizers. The sync example above +does this in a `finally`, so Ctrl+C still runs them. `server.stop(grace=5)` stops taking +new RPCs and gives the ones in flight up to 5 seconds to finish, and `.wait()` blocks until +the server has stopped, so the close does not run under them. On `grpc.aio`, use +`await server.stop(5)` and then `await container.close_async()`. ## Resolving without `@inject` diff --git a/docs/integrations/writing-integrations.md b/docs/integrations/writing-integrations.md index 8b91c0f6..f3af8528 100644 --- a/docs/integrations/writing-integrations.md +++ b/docs/integrations/writing-integrations.md @@ -237,7 +237,8 @@ in for a type at call sites. can then resolve APP-scoped dependencies, and APP finalizers run last. With a context-manager lifespan, make the container the outer context, as `setup_di` above does. With callback hooks, the close must run after every shutdown hook, including ones registered after `setup_di`. - FastStream registers its close at startup, and taskiq moves its close to the end at shutdown. + FastStream and aiogram register their close at startup, Celery moves its close to the end when the + worker starts, and taskiq moves its close to the end at shutdown. Where the framework can't give that order, say on the docs page which user code runs outside it, as the [aiohttp](aiohttp.md) and [FastMCP](fastmcp.md) pages do. - Reopen the root container on startup. A container closed on shutdown stays closed, and the next