Skip to content

fix: close the root container after every shutdown handler - #54

Merged
lesnik512 merged 2 commits into
mainfrom
fix/close-root-after-shutdown-handlers
Oct 17, 2026
Merged

lesnik512 merged 2 commits into
mainfrom
fix/close-root-after-shutdown-handlers

Conversation

@lesnik512

@lesnik512 lesnik512 commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

What changed

setup_di registered the wait-and-close (close_root) on dispatcher.shutdown when it was called. aiogram runs shutdown handlers on the dispatcher first and then on sub-routers, so a dispatcher.shutdown handler registered after setup_di, and every sub-router shutdown handler, ran against a closed root and got ContainerClosedError after the APP finalizers had already run.

The #49 in-flight wait and the root close are now two handlers, wait_in_flight and close_root. Shutdown runs in this order:

  1. wait for updates in flight (with shutdown_timeout and its warning),
  2. the shutdown handlers of the dispatcher, including aiogram's FSM storage close, then those of every sub-router,
  3. close the root container, so the APP finalizers run last.

setup_di registers both on dispatcher.shutdown, wait first, as a fallback for emit_shutdown without emit_startup. The startup handler that opens the root then:

  • removes wait_in_flight from dispatcher.shutdown and puts it back at index 0, so it runs before every dispatcher handler and therefore before every router handler;
  • removes close_root from the observer it is on and appends it to the shutdown observer of the last router in dispatcher.chain_tail.

Removing before re-adding is the restart guard for both. wait_in_flight only ever lives on dispatcher.shutdown, so it is there exactly once after any number of startups. close_root is removed from wherever the previous startup (or setup_di) put it, so if routers were included between runs it moves to the new last router and neither runs twice nor gets skipped. Re-appending each startup also puts it behind shutdown handlers registered on the last router between runs. Without a startup, both stay where setup_di put them and run once each, wait then close.

manage_lifespan=False and auto_inject are unchanged.

aiogram details this relies on (3.31.0, same in the 3.2 floor)

  • Router.emit_shutdown triggers its own shutdown observer, then calls emit_shutdown on each of sub_routers in order: a pre-order depth-first walk. So the first handler of dispatcher.shutdown runs before every other shutdown handler, and the last router of chain_tail (same pre-order walk) runs its handlers last.
  • Dispatcher.__init__ registers self.fsm.close on dispatcher.shutdown, so with the wait at index 0 the FSM storage now closes after the wait.
  • EventObserver.trigger iterates self.handlers at shutdown time. Replacing shutdown.handlers during startup takes effect in the same cycle and does not touch a list being iterated. Startup and shutdown observers are plain EventObservers, not part of router.observers, so _inject_router does not wrap either handler.
  • Routers can be included but not detached (parent_router can only be set once), so the router holding the close stays reachable.

Limits

  • A shutdown handler added during or after startup (for example by a startup handler), or one on a router included after startup, runs after the close until the next startup. The README says so.
  • Without a startup, the FSM storage close and shutdown handlers registered before setup_di run before the wait.
  • In webhook mode, the bot session close that SimpleRequestHandler.register puts on aiohttp shutdown can still run before the wait. The README says so.

Tests

  • test_shutdown_handlers_run_after_in_flight_updates puts a shutdown handler on the dispatcher or a sub-router, registered before or after setup_di, and over two cycles checks the APP resource log is handled, hook, finalized. All four cases failed on the previous commit (hook came first).
  • test_shutdown_waits_once_per_cycle holds an update in flight with shutdown_timeout, includes routers between runs, and checks one timeout warning per shutdown, with and without startup. With the removal of the old wait_in_flight entry deleted, it fails.
  • test_shutdown_handlers_see_the_open_root puts a shutdown handler on the dispatcher, on a sub-router, or on a sub-router that has its own child, registered before or after setup_di. Over two startup/shutdown cycles it checks that the handler resolves the APP dependency before it is finalized, that the finalizer runs once per cycle, and that close_async runs once per cycle.
  • test_router_included_between_runs_sees_the_open_root and test_shutdown_without_startup_closes_the_root cover routers added between runs and the fallback.
  • The In webhook mode the root container closes before in-flight handlers finish #49 polling and webhook tests used a dispatcher.shutdown handler to signal that shutdown had started. That handler now runs after the wait, so they signal from a wrapper around dispatcher.emit_shutdown instead.

Closes #51

@lesnik512
lesnik512 merged commit be3af06 into main Oct 17, 2026
12 checks passed
@lesnik512
lesnik512 deleted the fix/close-root-after-shutdown-handlers branch October 17, 2026 13:56
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.

The root container closes before later dispatcher and sub-router shutdown handlers

1 participant