Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions docs/integrations/aiogram.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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. |
Expand Down
8 changes: 7 additions & 1 deletion docs/integrations/celery.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(...)`. |
Expand Down
10 changes: 10 additions & 0 deletions docs/integrations/flask.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 12 additions & 12 deletions docs/integrations/grpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()
```

<!-- invisible-code-block: python
from unittest import mock

grpc.server = real_grpc_server
sync_server = server
container.open()
peer_context = mock.Mock(spec=grpc.ServicerContext)
peer_context.peer.return_value = "ipv4:127.0.0.1:1"
say_hello = DIInterceptor(container).intercept_service(
Expand Down Expand Up @@ -179,6 +183,7 @@ server = grpc.aio.server(interceptors=[DIAioInterceptor(container)])
```

<!-- invisible-code-block: python
del server
asyncio.get_event_loop().close()
asyncio.set_event_loop(None)
-->
Expand Down Expand Up @@ -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:

<!-- invisible-code-block: python
server = sync_server
-->

```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`

Expand Down
3 changes: 2 additions & 1 deletion docs/integrations/writing-integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading