@@ -193,19 +193,29 @@ async def enqueue_event(self, event: Event) -> None:
193193 return
194194
195195 async def dequeue_event (self ) -> Event :
196- """Dequeues an event from the default internal sink queue."""
196+ """Pulls an event from the default internal sink queue."""
197197 if self ._default_sink is None :
198198 raise ValueError ('No default sink available.' )
199199 return await self ._default_sink .dequeue_event ()
200200
201201 def task_done (self ) -> None :
202- """Signals that a formerly enqueued task is complete via the default internal sink queue."""
202+ """Signals that a work on dequeued event is complete via the default internal sink queue."""
203203 if self ._default_sink is None :
204204 raise ValueError ('No default sink available.' )
205205 self ._default_sink .task_done ()
206206
207207 async def close (self , immediate : bool = False ) -> None :
208- """Closes the queue for future push events and also closes all child sinks."""
208+ """Closes the queue and all its child sinks.
209+
210+ It is safe to call it multiple times.
211+ If immediate is True, the queue will be closed without waiting for all events to be processed.
212+ If immediate is False, the queue will be closed after all events are processed (and confirmed with task_done() calls).
213+
214+ WARNING: Closing the parent queue with immediate=False is a deadlock risk if there are unconsumed events
215+ in any of the child sinks and the consumer has crashed without draining its queue.
216+ It is highly recommended to wrap graceful shutdowns with a timeout, e.g.,
217+ `asyncio.wait_for(queue.close(immediate=False), timeout=...)`.
218+ """
209219 logger .debug ('Closing EventQueueSource: immediate=%s' , immediate )
210220 async with self ._lock :
211221 # No more tap() allowed.
@@ -230,15 +240,24 @@ async def close(self, immediate: bool = False) -> None:
230240 )
231241
232242 def is_closed (self ) -> bool :
233- """Checks if the queue is closed."""
243+ """[DEPRECATED] Checks if the queue is closed.
244+
245+ NOTE: Relying on this for enqueue logic introduces race conditions.
246+ It is maintained primarily for backwards compatibility, workarounds for
247+ Python 3.10/3.12 async queues in consumers, and for the test suite.
248+ """
234249 return self ._is_closed
235250
236251 async def test_only_join_incoming_queue (self ) -> None :
237252 """Wait for incoming queue to be fully processed."""
238253 await self ._join_incoming_queue ()
239254
240255 async def __aenter__ (self ) -> Self :
241- """Enters the async context manager, returning the queue itself."""
256+ """Enters the async context manager, returning the queue itself.
257+
258+ WARNING: See `__aexit__` for important deadlock risks associated with
259+ exiting this context manager if unconsumed events remain.
260+ """
242261 return self
243262
244263 async def __aexit__ (
@@ -247,7 +266,13 @@ async def __aexit__(
247266 exc_val : BaseException | None ,
248267 exc_tb : TracebackType | None ,
249268 ) -> None :
250- """Exits the async context manager, ensuring close() is called."""
269+ """Exits the async context manager, ensuring close() is called.
270+
271+ WARNING: The context manager calls `close(immediate=False)` by default.
272+ If a consumer exits the `async with` block early (e.g., due to an exception
273+ or an explicit `break`) while unconsumed events remain in the queue,
274+ `__aexit__` will deadlock waiting for `task_done()` to be called on those events.
275+ """
251276 await self .close ()
252277
253278
@@ -290,26 +315,35 @@ async def enqueue_event(self, event: Event) -> None:
290315 raise RuntimeError ('Cannot enqueue to a sink-only queue' )
291316
292317 async def dequeue_event (self ) -> Event :
293- """Dequeues an event from the sink queue."""
318+ """Pulls an event from the sink queue."""
294319 logger .debug ('Attempting to dequeue event (waiting).' )
295320 event = await self ._queue .get ()
296321 logger .debug ('Dequeued event: %s' , event )
297322 return event
298323
299324 def task_done (self ) -> None :
300- """Signals that a formerly enqueued task is complete in this sink queue."""
325+ """Signals that a work on dequeued event is complete in this sink queue."""
301326 logger .debug ('Marking task as done in EventQueueSink.' )
302327 self ._queue .task_done ()
303328
304329 async def tap (
305330 self , max_queue_size : int = DEFAULT_MAX_QUEUE_SIZE
306331 ) -> 'EventQueueSink' :
307- """Taps the event queue to create a new child queue that receives future events."""
332+ """Creates a child queue that receives future events.
333+
334+ Note: The tapped queue may receive some old events if the incoming event
335+ queue is lagging behind and hasn't dispatched them yet.
336+ """
308337 # Delegate tap to the parent source so all sinks are flat under the source
309338 return await self ._parent .tap (max_queue_size = max_queue_size )
310339
311340 async def close (self , immediate : bool = False ) -> None :
312- """Closes the child sink queue."""
341+ """Closes the child sink queue.
342+
343+ It is safe to call it multiple times.
344+ If immediate is True, the queue will be closed without waiting for all events to be processed.
345+ If immediate is False, the queue will be closed after all events are processed (and confirmed with task_done() calls).
346+ """
313347 logger .debug ('Closing EventQueueSink.' )
314348 async with self ._lock :
315349 self ._is_closed = True
@@ -323,11 +357,20 @@ async def close(self, immediate: bool = False) -> None:
323357 await self ._queue .join ()
324358
325359 def is_closed (self ) -> bool :
326- """Checks if the sink queue is closed."""
360+ """[DEPRECATED] Checks if the queue is closed.
361+
362+ NOTE: Relying on this for enqueue logic introduces race conditions.
363+ It is maintained primarily for backwards compatibility, workarounds for
364+ Python 3.10/3.12 async queues in consumers, and for the test suite.
365+ """
327366 return self ._is_closed
328367
329368 async def __aenter__ (self ) -> Self :
330- """Enters the async context manager, returning the queue itself."""
369+ """Enters the async context manager, returning the queue itself.
370+
371+ WARNING: See `__aexit__` for important deadlock risks associated with
372+ exiting this context manager if unconsumed events remain.
373+ """
331374 return self
332375
333376 async def __aexit__ (
@@ -336,5 +379,11 @@ async def __aexit__(
336379 exc_val : BaseException | None ,
337380 exc_tb : TracebackType | None ,
338381 ) -> None :
339- """Exits the async context manager, ensuring close() is called."""
382+ """Exits the async context manager, ensuring close() is called.
383+
384+ WARNING: The context manager calls `close(immediate=False)` by default.
385+ If a consumer exits the `async with` block early (e.g., due to an exception
386+ or an explicit `break`) while unconsumed events remain in the queue,
387+ `__aexit__` will deadlock waiting for `task_done()` to be called on those events.
388+ """
340389 await self .close ()
0 commit comments