33import warnings
44from abc import ABC , abstractmethod
55from collections import defaultdict
6- from collections .abc import AsyncGenerator , Awaitable , Callable
7- from functools import wraps
6+ from collections .abc import AsyncGenerator , Awaitable , Callable , Iterator
7+ from contextlib import contextmanager
8+ from functools import partial , wraps
89from logging import getLogger
910from typing import (
1011 TYPE_CHECKING ,
4748_ReturnType = TypeVar ("_ReturnType" )
4849
4950EventHandler : TypeAlias = Callable [[TaskiqState ], Awaitable [None ] | None ]
51+ ShutdownHook : TypeAlias = Callable [[], Awaitable [None ] | None ]
5052
5153logger = getLogger ("taskiq" )
5254
@@ -118,6 +120,7 @@ def __init__(
118120 self .is_worker_process = False
119121 # True only if broker runs in scheduler process.
120122 self .is_scheduler_process = False
123+ self ._shutdown_resource_index = 0
121124
122125 def find_task (self , task_name : str ) -> AsyncTaskiqDecoratedTask [Any , Any ] | None :
123126 """
@@ -209,35 +212,37 @@ async def shutdown(self) -> None:
209212 This method is called,
210213 when broker is closing.
211214 """
212- shutdown_error : BaseException | None = None
215+ shutdown_errors : list [BaseException ] = []
216+ await self ._shutdown_resources (shutdown_errors )
213217
218+ if shutdown_errors :
219+ raise shutdown_errors [0 ]
220+
221+ async def _shutdown_resources (
222+ self ,
223+ shutdown_errors : list [BaseException ],
224+ ) -> None :
225+ """Close every registered resource and record failures in order."""
226+ for index , shutdown_hook in enumerate (self ._iter_shutdown_hooks ()):
227+ if index < self ._shutdown_resource_index :
228+ continue
229+ try :
230+ await maybe_awaitable (shutdown_hook ())
231+ except Exception as exc :
232+ self ._record_shutdown_error (shutdown_errors , exc )
233+ self ._shutdown_resource_index = index + 1
234+
235+ def _iter_shutdown_hooks (self ) -> Iterator [ShutdownHook ]:
236+ """Yield lifecycle hooks in their shutdown order."""
214237 for event in self ._get_shutdown_events ():
215238 for handler in self .event_handlers [event ]:
216- try :
217- await maybe_awaitable (handler (self .state ))
218- except BaseException as exc :
219- shutdown_error = self ._remember_shutdown_error (
220- shutdown_error ,
221- exc ,
222- )
239+ yield partial (handler , self .state )
223240
224241 for middleware in self .middlewares :
225242 if middleware .__class__ .shutdown != TaskiqMiddleware .shutdown :
226- try :
227- await maybe_awaitable (middleware .shutdown ())
228- except BaseException as exc :
229- shutdown_error = self ._remember_shutdown_error (
230- shutdown_error ,
231- exc ,
232- )
233-
234- try :
235- await self .result_backend .shutdown ()
236- except BaseException as exc :
237- shutdown_error = self ._remember_shutdown_error (shutdown_error , exc )
243+ yield middleware .shutdown
238244
239- if shutdown_error is not None :
240- raise shutdown_error
245+ yield self .result_backend .shutdown
241246
242247 def _get_shutdown_events (self ) -> tuple [TaskiqEvents , ...]:
243248 """Return event phases owned by this broker shutdown."""
@@ -259,6 +264,23 @@ def _remember_shutdown_error(
259264 )
260265 return first_error
261266
267+ @classmethod
268+ def _record_shutdown_error (
269+ cls ,
270+ shutdown_errors : list [BaseException ],
271+ current_error : BaseException ,
272+ ) -> None :
273+ """Record the first failure when it occurs and log later failures."""
274+ first_error = shutdown_errors [0 ] if shutdown_errors else None
275+ remembered_error = cls ._remember_shutdown_error (first_error , current_error )
276+ if first_error is None :
277+ shutdown_errors .append (remembered_error )
278+
279+ @contextmanager
280+ def _send_lifecycle (self ) -> Iterator [None ]:
281+ """Own package-internal client send work through broker handoff."""
282+ yield
283+
262284 async def _kick_with_post_send (
263285 self ,
264286 message : BrokerMessage ,
0 commit comments