Repository navigation
Conversation
When get_nowait() wakes a waiting putter, the freed slot was open until the putter resumed, so a later put() or put_nowait() could take it. The woken putter then found the queue full again and went to the back of the line, putting items out of order. Count slots promised to woken putters in full(), so the slot stays with the putter that was woken for it. This keeps put_nowait() consistent with full() and stays O(1), unlike the approach in pythongh-112501. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Documentation build overview
|
|
@YvesDup here is my try to make |
| if not self._is_shutdown: | ||
| # Take the slot reserved for us. | ||
| self._reserved -= 1 | ||
| return self.put_nowait(item) |
There was a problem hiding this comment.
self.put_nowait returns only None. return statement could be removed.
There was a problem hiding this comment.
Correct, but this line is not affected by the PR. I prefer to keep it untouched here
OsmnvAslan
left a comment
There was a problem hiding this comment.
Thanks for working on this! I tested it on a checkout merged with current main: test_queues passes, and the new tests fail on main as expected. I also ran a randomized put/get/cancel script (300 seeds) and a few edge cases (a woken putter that gets cancelled or closed via GeneratorExit, shutdown() and shutdown(immediate=True) with reserved slots). _reserved stayed consistent in all of them.
One compatibility concern: subclasses that override put() but inherit get_nowait(), or the other way round, and use _putters/_wakeup_next() directly. It's private API, but the pattern is fairly common: a GitHub code search for _wakeup_next(self._putters) outside queues.py gives more than 100 hits. For example, NVIDIA NeMo-Agent-Toolkit's AsyncIOProducerConsumerQueue overrides put() with the old loop and inherits get_nowait(). With this PR, a simple producer/consumer on such a subclass (maxsize=2) deadlocks after 3 items: the queue is empty, but full() stays True because _reserved == 2 is never released. In the opposite case, a subclass that wakes putters itself via _wakeup_next(self._putters) drives _reserved negative, and the queue accepts more than maxsize items.
Probably not a blocker, but it might be worth a What's New note (Porting section) along with the full()/qsize() change. If you want to harden the second case, tracking the woken futures in a set instead of a counter keeps _reserved from going negative and makes the _is_shutdown special-casing unnecessary; I tried it locally, and only the tests that compare _reserved with an int needed adjusting.
Nit: the full() docstring in queues.py still says "Return True if there are maxsize items in the queue".
| await put_2 | ||
| self.assertEqual(q._reserved, 0) | ||
|
|
||
| async def test_put_nowait_uses_unreserved_slots(self): |
There was a problem hiding this comment.
This test makes me deeply uncomfortable. Because, IMO, for an asyncio.Queue to be fair, it should not be possible to have a successful put_nowait call with pending or/and woken putters regardless of queue's state.(full or not).
But maybe I have missed something.
When the queue is full,
get_nowait()pops a waiting putter and wakes it, but the freed slot stays open until that putter resumes. A laterput()(fast path) orput_nowait()can take it; the woken putter then finds the queue full and is re-appended to the end of_putters, so items are put out of order and a waiter can be starved.This PR reserves the freed slot for the woken putter:
get_nowait()increments_reservedwhen it wakes a putter;full()counts reserved slots (qsize() + _reserved >= maxsize).put_nowait(); if it is cancelled after being woken it passes the reservation to the next putter.shutdown()clears the counter.This is the same idea
asyncio.Semaphoreuses for fairness (taking the value on the waiter's behalf). Compared to the earlier attempt (gh-112501):full() is Falsestill guaranteesput_nowait()succeeds;get_nowait()with one waiting putter, thenput_nowait()) succeeds.Behaviour change:
full()may beTruewhileqsize() < maxsize, for as long as a woken putter has not resumed. Documented infull()/put().Getters are left as is: the issue is about the order items enter the queue, and reserving items would change the meaning of
empty().🤖 Generated with Claude Code