Skip to content

fix: close the request container even inside a cancelled anyio scope - #89

Merged
lesnik512 merged 1 commit into
mainfrom
fix/shield-child-close
Oct 19, 2026
Merged

lesnik512 merged 1 commit into
mainfrom
fix/shield-child-close

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Closes #88.

Problem

A concurrent subscriber (max_workers > 1) runs its handlers in an anyio task group inside the subscriber's consume task. When the subscriber stops, TasksMixin.stop cancels that task, and the task group turns the cancellation into a cancel of its own scope. The DI middleware used to close the request container inside that cancelled scope. anyio cancellation is level-triggered, so every await in the close was cancelled too. The first async finalizer stopped at its first await, the rest never ran, and nothing was logged. Any anyio scope a user puts around message handling has the same effect.

Sequential subscribers weren't affected: a native task.cancel() is delivered once, and the close then runs in full.

Change

The middleware now closes the request container in anyio.move_on_after(10, shield=True), the same as modern-di-starlette 4.3.3 and modern-di-fastapi 4.2.2. If the close takes longer than 10 s, the remaining finalizers are abandoned and a warning goes to the modern_di_faststream.main logger.

  • The bound is a module constant, with no new API. 10 s is the value in anyio's own cleanup example.
  • When the bound expires, the code logs a warning and doesn't raise. A TimeoutError from fail_after would replace the outer cancellation, and anyio needs that cancellation to unwind its scope.
  • anyio>=4.14.2,<5 is now declared, because the module imports it. FastStream itself pins that range.

Not covered

A native asyncio task.cancel() that lands while the close itself is awaiting still interrupts it. An anyio shield can't intercept that.

Cost

Entering and leaving the shielded scope costs about 2.1 µs per message on the asyncio backend.

Tests

The test broker calls the handler inline, so each test publishes under anyio.move_on_after and cancels the handler while it runs. Both tests failed before the change.

  • Both REQUEST finalizers still run, newest first.
  • With the bound patched to 0.05 s, a hung finalizer is abandoned and the warning is logged.

Ran on 3.11, 3.14 and 3.14t (25 passed each), and at the declared floors (anyio 4.14.2, faststream 0.7.0).

@lesnik512
lesnik512 merged commit c20d844 into main Oct 19, 2026
12 checks passed
@lesnik512
lesnik512 deleted the fix/shield-child-close branch October 19, 2026 05:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A cancelled task group around a handler skips the child container's async finalizers

1 participant