@@ -640,7 +640,7 @@ async def runner(
640640
641641 :param queue: queue with prefetched data.
642642 """
643- cancelled = False
643+ cancellation : asyncio . CancelledError | None = None
644644 try :
645645 while True :
646646 queued_message = await queue .get ()
@@ -650,13 +650,25 @@ async def runner(
650650 raise TypeError ("Receiver queue contains an invalid message." )
651651 await self ._start_callback (queued_message )
652652
653- except asyncio .CancelledError :
654- cancelled = True
655- raise
653+ except asyncio .CancelledError as exc :
654+ cancellation = exc
656655 finally :
657- await self ._drain_active_tasks (cancel_immediately = cancelled )
656+ with anyio .CancelScope (shield = True ):
657+ cleanup_cancellation = await self ._drain_active_tasks (
658+ cancel_immediately = cancellation is not None ,
659+ )
660+ if cancellation is None :
661+ cancellation = cleanup_cancellation
662+
663+ if cancellation is not None :
664+ raise cancellation
658665 logger .info ("The runner is stopped." )
659666
667+ async def _run_owned_callback (self , message : bytes | AckableMessage ) -> None :
668+ """Run a callback outside repeated listener-scope cancellation."""
669+ with anyio .CancelScope (shield = True ):
670+ await self .callback (message = message , raise_err = False )
671+
660672 async def _start_callback (self , message : _PrefetchedMessage ) -> None :
661673 """Transfer one prefetched delivery into an owned callback task."""
662674 owns_prefetch_slot = message .owns_prefetch_slot
@@ -671,7 +683,7 @@ async def _start_callback(self, message: _PrefetchedMessage) -> None:
671683 self .sem_prefetch .release ()
672684 owns_prefetch_slot = False
673685 task = asyncio .create_task (
674- self .callback (message = message .data , raise_err = False ),
686+ self ._run_owned_callback (message .data ),
675687 )
676688 except BaseException :
677689 if owns_prefetch_slot :
@@ -704,11 +716,15 @@ def _on_callback_done(self, task: "asyncio.Task[Any]") -> None:
704716 ),
705717 )
706718
707- async def _drain_active_tasks (self , * , cancel_immediately : bool ) -> None :
719+ async def _drain_active_tasks (
720+ self ,
721+ * ,
722+ cancel_immediately : bool ,
723+ ) -> asyncio .CancelledError | None :
708724 """Wait for owned callbacks and cancel them after the graceful boundary."""
709725 tasks = set (self ._active_tasks )
710726 if not tasks :
711- return
727+ return None
712728
713729 logger .info ("Waiting for %d running tasks to complete..." , len (tasks ))
714730 cancellation : asyncio .CancelledError | None = None
@@ -733,25 +749,24 @@ async def _drain_active_tasks(self, *, cancel_immediately: bool) -> None:
733749 if cancellation is None :
734750 cancellation = cleanup_cancellation
735751 logger .info ("No more tasks to wait for. Shutting down." )
736- if cancellation is not None :
737- raise cancellation
752+ return cancellation
738753
739754 @staticmethod
740755 async def _cancel_callback_tasks (
741756 tasks : set [asyncio .Task [Any ]],
742757 ) -> asyncio .CancelledError | None :
743- """Cancel callback tasks and await cleanup despite one outer cancellation."""
758+ """Cancel callbacks once and await cleanup despite outer cancellation."""
744759 for task in tasks :
745760 task .cancel ()
746- try :
747- await asyncio .gather ( * tasks , return_exceptions = True )
748- except asyncio . CancelledError as exc :
749- for task in tasks :
750- if not task . done ():
751- task . cancel ()
752- await asyncio . gather ( * tasks , return_exceptions = True )
753- return exc
754- return None
761+ waiter = asyncio . gather ( * tasks , return_exceptions = True )
762+ cancellation : asyncio .CancelledError | None = None
763+ while not waiter . done () :
764+ try :
765+ await asyncio . shield ( waiter )
766+ except asyncio . CancelledError as exc :
767+ if cancellation is None :
768+ cancellation = exc
769+ return cancellation
755770
756771 def _record_listen_error (self , error : BaseException ) -> None :
757772 """Preserve the first transport error while allowing iterator cleanup."""
0 commit comments