@@ -41,10 +41,11 @@ def __init__(self, max_queue_size: int = DEFAULT_MAX_QUEUE_SIZE) -> None:
4141 self ._children : list [EventQueue ] = []
4242 self ._is_closed = False
4343 self ._lock = asyncio .Lock ()
44+ self ._bg_tasks : set [asyncio .Task [None ]] = set ()
4445 logger .debug ('EventQueue initialized.' )
4546
4647 async def enqueue_event (self , event : Event ) -> None :
47- """Enqueues an event to this queue and all its children .
48+ """Enqueues an event to this queue and propagates it to all child queues .
4849
4950 Args:
5051 event: The event object to enqueue.
@@ -59,7 +60,12 @@ async def enqueue_event(self, event: Event) -> None:
5960 # Make sure to use put instead of put_nowait to avoid blocking the event loop.
6061 await self .queue .put (event )
6162 for child in self ._children :
62- await child .enqueue_event (event )
63+ # We use a background task to enqueue to children to avoid blocking
64+ # the parent queue if a child queue is full (e.g. slow consumer).
65+ # This prevents deadlocks where a slow consumer blocks the producer.
66+ task = asyncio .create_task (child .enqueue_event (event ))
67+ self ._bg_tasks .add (task )
68+ task .add_done_callback (self ._bg_tasks .discard )
6369
6470 async def dequeue_event (self , no_wait : bool = False ) -> Event :
6571 """Dequeues an event from the queue.
@@ -132,6 +138,17 @@ def tap(self) -> 'EventQueue':
132138 self ._children .append (queue )
133139 return queue
134140
141+ async def flush (self ) -> None :
142+ """Waits for all pending background propagation tasks to complete recursively."""
143+ while self ._bg_tasks :
144+ # Copy the set to avoid "Set changed size during iteration"
145+ tasks = list (self ._bg_tasks )
146+ if tasks :
147+ await asyncio .gather (* tasks , return_exceptions = True )
148+
149+ if self ._children :
150+ await asyncio .gather (* (child .flush () for child in self ._children ))
151+
135152 async def close (self , immediate : bool = False ) -> None :
136153 """Closes the queue for future push events and also closes all child queues.
137154
@@ -161,6 +178,12 @@ async def close(self, immediate: bool = False) -> None:
161178 return
162179 if not self ._is_closed :
163180 self ._is_closed = True
181+
182+ if immediate :
183+ # Cancel all pending background propagation tasks
184+ for task in self ._bg_tasks :
185+ task .cancel ()
186+
164187 # If using python 3.13 or higher, use shutdown but match <3.13 semantics
165188 if sys .version_info >= (3 , 13 ):
166189 if immediate :
@@ -170,10 +193,12 @@ async def close(self, immediate: bool = False) -> None:
170193 for child in self ._children :
171194 await child .close (True )
172195 return
173- # Graceful: prevent further gets/puts via shutdown, then wait for drain and children
196+ # Graceful: prevent further gets/puts via shutdown, then wait for drain, propagation and children
174197 self .queue .shutdown (False )
175198 await asyncio .gather (
176- self .queue .join (), * (child .close () for child in self ._children )
199+ self .queue .join (),
200+ self .flush (),
201+ * (child .close () for child in self ._children ),
177202 )
178203 # Otherwise, join the queue
179204 else :
@@ -182,8 +207,11 @@ async def close(self, immediate: bool = False) -> None:
182207 for child in self ._children :
183208 await child .close (immediate )
184209 return
210+ # Graceful: wait for drain, propagation and children
185211 await asyncio .gather (
186- self .queue .join (), * (child .close () for child in self ._children )
212+ self .queue .join (),
213+ self .flush (),
214+ * (child .close () for child in self ._children ),
187215 )
188216
189217 def is_closed (self ) -> bool :
0 commit comments