-
-
Notifications
You must be signed in to change notification settings - Fork 34.5k
gh-96471: Add asyncio queue shutdown #104228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
440a702
Add asyncio queue shutdown
EpicWink fb458db
Fix queue shutdown
YvesDup e5951ac
ππ€ Added by blurb_it.
blurb-it[bot] a72aedd
Merge remote-tracking branch 'upstream/main' into asyncio-queue-shutdown
EpicWink d5e925d
Add references in docs and news entry
EpicWink f3517fb
Merge remote-tracking branch 'upstream/main' into asyncio-queue-shutdown
EpicWink bd2a7c3
Improve docs
EpicWink e9ac8de
Consume queue on immediate shutdown
EpicWink 1e7813a
Fix links in what's-new
EpicWink 1275bb6
Merge remote-tracking branch 'upstream/main' into asyncio-queue-shutdown
EpicWink eec29bb
Fix formatting in news entry
EpicWink 2c6156f
Merge remote-tracking branch 'upstream/main' into asyncio-queue-shutdown
EpicWink 17f1f32
Merge remote-tracking branch 'upstream/main' into asyncio-queue-shutdown
EpicWink a233830
Improve tests
EpicWink 420a247
Improve tests even more
EpicWink 25ad2ac
Merge remote-tracking branch 'upstream/main' into asyncio-queue-shutdown
EpicWink f3321b4
Merge remote-tracking branch 'upstream/main' into asyncio-queue-shutdown
EpicWink 6d9edd6
Document tests
EpicWink 1135d85
Merge remote-tracking branch 'upstream/main' into asyncio-queue-shutdown
EpicWink ddc6ad6
Always allow getters to re-check queue empty
EpicWink 2fa1bd9
Merge branch 'main' into asyncio-queue-shutdown
gvanrossum aef4063
Simplify shutdown-check in put and get
EpicWink d49c6dd
Format shutdown docstring
EpicWink 5a435a6
Check for 0 unfinised tasks in shutdown
EpicWink c8db40e
Use asyncio.sleep to run other tasks
EpicWink ca01ee1
Use public method to shut down queue in format test
EpicWink b02c4dd
Only start queue join after shutdown in test
EpicWink 8deca77
Test join before failing task-done
EpicWink File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Fix queue shutdown
* Queue state enum members are capitalised * Termination state in str/repr * Include raised exception in docstrings * Factor out queue-state checks and updates to methods * Logic fixes in get_nowait and shutdown * Handle queue shutdown in task_done and join * Updated tests * Document feature added in 3.13
- Loading branch information
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| ) | ||
|
|
||
| import collections | ||
| import enum | ||
| import heapq | ||
| from types import GenericAlias | ||
|
|
||
|
|
@@ -30,9 +31,10 @@ class QueueShutDown(Exception): | |
| pass | ||
|
|
||
|
|
||
| _queue_alive = "alive" | ||
| _queue_shutdown = "shutdown" | ||
| _queue_shutdown_immediate = "shutdown-immediate" | ||
| class _QueueState(enum.Enum): | ||
| ALIVE = "alive" | ||
| SHUTDOWN = "shutdown" | ||
| SHUTDOWN_IMMEDIATE = "shutdown-immediate" | ||
|
|
||
|
|
||
| class Queue(mixins._LoopBoundMixin): | ||
|
|
@@ -58,7 +60,7 @@ def __init__(self, maxsize=0): | |
| self._finished = locks.Event() | ||
| self._finished.set() | ||
| self._init(maxsize) | ||
| self.shutdown_state = _queue_alive | ||
| self._shutdown_state = _QueueState.ALIVE | ||
|
|
||
| # These three are overridable in subclasses. | ||
|
|
||
|
|
@@ -99,6 +101,8 @@ def _format(self): | |
| result += f' _putters[{len(self._putters)}]' | ||
| if self._unfinished_tasks: | ||
| result += f' tasks={self._unfinished_tasks}' | ||
| if not self._is_alive(): | ||
| result += f' state={self._shutdown_state.value}' | ||
| return result | ||
|
|
||
| def qsize(self): | ||
|
|
@@ -130,8 +134,10 @@ async def put(self, item): | |
|
|
||
| Put an item into the queue. If the queue is full, wait until a free | ||
| slot is available before adding item. | ||
|
|
||
| Raises QueueShutDown if the queue has been shut down. | ||
| """ | ||
| if self.shutdown_state != _queue_alive: | ||
| if not self._is_alive(): | ||
| raise QueueShutDown | ||
| while self.full(): | ||
| putter = self._get_loop().create_future() | ||
|
|
@@ -145,23 +151,25 @@ async def put(self, item): | |
| self._putters.remove(putter) | ||
| except ValueError: | ||
| # The putter could be removed from self._putters by a | ||
| # previous get_nowait call. | ||
| # previous get_nowait call or a shutdown call. | ||
| pass | ||
| if not self.full() and not putter.cancelled(): | ||
| # We were woken up by get_nowait(), but can't take | ||
| # the call. Wake up the next in line. | ||
| self._wakeup_next(self._putters) | ||
| raise | ||
| if self.shutdown_state != _queue_alive: | ||
| if not self._is_alive(): | ||
| raise QueueShutDown | ||
| return self.put_nowait(item) | ||
|
|
||
| def put_nowait(self, item): | ||
| """Put an item into the queue without blocking. | ||
|
|
||
| If no free slot is immediately available, raise QueueFull. | ||
|
|
||
| Raises QueueShutDown if the queue has been shut down. | ||
| """ | ||
| if self.shutdown_state != _queue_alive: | ||
| if not self._is_alive(): | ||
| raise QueueShutDown | ||
| if self.full(): | ||
| raise QueueFull | ||
|
|
@@ -174,11 +182,14 @@ async def get(self): | |
| """Remove and return an item from the queue. | ||
|
|
||
| If queue is empty, wait until an item is available. | ||
|
|
||
| Raises QueueShutDown if the queue has been shut down and is empty, or | ||
| if the queue has been shut down immediately. | ||
| """ | ||
| if self.shutdown_state == _queue_shutdown_immediate: | ||
| if self._is_shutdown_immediate(): | ||
| raise QueueShutDown | ||
| while self.empty(): | ||
| if self.shutdown_state != _queue_alive: | ||
| if self._is_shutdown(): | ||
| raise QueueShutDown | ||
| getter = self._get_loop().create_future() | ||
| self._getters.append(getter) | ||
|
|
@@ -191,28 +202,32 @@ async def get(self): | |
| self._getters.remove(getter) | ||
| except ValueError: | ||
| # The getter could be removed from self._getters by a | ||
| # previous put_nowait call. | ||
| # previous put_nowait call, | ||
| # or a shutdown call. | ||
| pass | ||
| if not self.empty() and not getter.cancelled(): | ||
| # We were woken up by put_nowait(), but can't take | ||
| # the call. Wake up the next in line. | ||
| self._wakeup_next(self._getters) | ||
| raise | ||
| if self.shutdown_state == _queue_shutdown_immediate: | ||
| if self._is_shutdown_immediate(): | ||
| raise QueueShutDown | ||
| return self.get_nowait() | ||
|
|
||
| def get_nowait(self): | ||
| """Remove and return an item from the queue. | ||
|
|
||
| Return an item if one is immediately available, else raise QueueEmpty. | ||
|
|
||
| Raises QueueShutDown if the queue has been shut down and is empty, or | ||
| if the queue has been shut down immediately. | ||
| """ | ||
| if self._is_shutdown_immediate(): | ||
| raise QueueShutDown | ||
| if self.empty(): | ||
|
EpicWink marked this conversation as resolved.
|
||
| if self.shutdown_state != _queue_alive: | ||
| if self._is_shutdown(): | ||
| raise QueueShutDown | ||
| raise QueueEmpty | ||
| elif self.shutdown_state == _queue_shutdown_immediate: | ||
| raise QueueShutDown | ||
| item = self._get() | ||
| self._wakeup_next(self._putters) | ||
| return item | ||
|
|
@@ -230,7 +245,11 @@ def task_done(self): | |
|
|
||
| Raises ValueError if called more times than there were items placed in | ||
| the queue. | ||
|
|
||
| Raises QueueShutDown if the queue has been shut down immediately. | ||
| """ | ||
| if self._is_shutdown_immediate(): | ||
| raise QueueShutDown | ||
| if self._unfinished_tasks <= 0: | ||
| raise ValueError('task_done() called too many times') | ||
| self._unfinished_tasks -= 1 | ||
|
|
@@ -244,9 +263,15 @@ async def join(self): | |
| queue. The count goes down whenever a consumer calls task_done() to | ||
| indicate that the item was retrieved and all work on it is complete. | ||
| When the count of unfinished tasks drops to zero, join() unblocks. | ||
|
|
||
| Raises QueueShutDown if the queue has been shut down immediately. | ||
| """ | ||
| if self._is_shutdown_immediate(): | ||
| raise QueueShutDown | ||
| if self._unfinished_tasks > 0: | ||
| await self._finished.wait() | ||
| if self._is_shutdown_immediate(): | ||
| raise QueueShutDown | ||
|
|
||
| def shutdown(self, immediate=False): | ||
| """Shut-down the queue, making queue gets and puts raise. | ||
|
|
@@ -257,19 +282,40 @@ def shutdown(self, immediate=False): | |
| All blocked callers of put() will be unblocked, and also get() | ||
| and join() if 'immediate'. The QueueShutDown exception is raised. | ||
| """ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Docstring to modify depending of agree/disagree on my first remark about `blocked callers'. |
||
| if self._is_shutdown_immediate(): | ||
| return | ||
| # here _shutdown_state is ALIVE or SHUTDOWN | ||
| if immediate: | ||
| self.shutdown_state = _queue_shutdown_immediate | ||
| self._set_shutdown_immediate() | ||
| while self._getters: | ||
| getter = self._getters.popleft() | ||
| if not getter.done(): | ||
| getter.set_result(None) | ||
| # Release all 'blocked' tasks/coros in `join()` | ||
| self._finished.set() | ||
| else: | ||
| self.shutdown_state = _queue_shutdown | ||
| self._set_shutdown() | ||
| while self._putters: | ||
| putter = self._putters.popleft() | ||
| if not putter.done(): | ||
| putter.set_result(None) | ||
|
|
||
| def _is_alive(self): | ||
| return self._shutdown_state is _QueueState.ALIVE | ||
|
|
||
| def _is_shutdown(self): | ||
| return self._shutdown_state is _QueueState.SHUTDOWN | ||
|
|
||
| def _is_shutdown_immediate(self): | ||
| return self._shutdown_state is _QueueState.SHUTDOWN_IMMEDIATE | ||
|
|
||
| def _set_shutdown(self): | ||
| self._shutdown_state = _QueueState.SHUTDOWN | ||
|
|
||
| def _set_shutdown_immediate(self): | ||
| self._shutdown_state = _QueueState.SHUTDOWN_IMMEDIATE | ||
|
|
||
|
|
||
| class PriorityQueue(Queue): | ||
| """A subclass of Queue; retrieves entries in priority order (lowest first). | ||
|
|
||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry but I have a doubt, shouldn't this documentation block be rather:
In event of change, the docstring of the
shutdownmethod must be updated.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
joincallers aren't necessarily even unblocked anyway, if consumers are processing any items. I should probably say that a task is marked as done for each item in the queue if immediate shutdown.Also, I think the threading queue docs are the same.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's very precise, better.
Yes, I commented here so as not to forget (see #117532 (comment)).
English is your native language, I think it'is best if you update documentations and docstrings.
Update: but I can create the follow-up PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made a PR: #117621