Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork33.7k
gh-117536: Fix asyncio _asyncgen_finalizer_hook()#117751
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -444,6 +444,11 @@ def __init__(self): | ||
| # A weak set of all asynchronous generators that are | ||
| # being iterated by the loop. | ||
| self._asyncgens = weakref.WeakSet() | ||
| # Strong references to asynchronous generators which are being being | ||
| # closed by the loop: see _asyncgen_finalizer_hook(). | ||
| self._close_asyncgens = set() | ||
| # Set to True when `loop.shutdown_asyncgens` is called. | ||
| self._asyncgens_shutdown_called = False | ||
| # Set to True when `loop.shutdown_default_executor` is called. | ||
| @@ -555,10 +560,22 @@ def _check_default_executor(self): | ||
| if self._executor_shutdown_called: | ||
| raise RuntimeError('Executor shutdown has been called') | ||
| async def _asyncgen_close(self, agen): | ||
| await agen.aclose() | ||
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. this is just side-stepping the actual bug by delaying the creation of the async_generator_aclose object to avoid the incorrect warning | ||
| self._close_asyncgens.discard(agen) | ||
| def _asyncgen_finalizer_hook(self, agen): | ||
| if self.is_closed(): | ||
| self._asyncgens.discard(agen) | ||
| return | ||
| # gh-117536: Store a strong reference to the asynchronous generator | ||
| # to make sure that shutdown_asyncgens() can close it even if | ||
| # asyncio.run() cancels all tasks. | ||
| self._close_asyncgens.add(agen) | ||
| self._asyncgens.discard(agen) | ||
| self.call_soon_threadsafe(self.create_task,self._asyncgen_close(agen)) | ||
| def _asyncgen_firstiter_hook(self, agen): | ||
| if self._asyncgens_shutdown_called: | ||
| @@ -573,12 +590,12 @@ async def shutdown_asyncgens(self): | ||
| """Shutdown all active asynchronous generators.""" | ||
| self._asyncgens_shutdown_called = True | ||
| closing_agens = list(set(self._asyncgens) | self._close_asyncgens) | ||
| if not closing_agens: | ||
| # If Python version is <3.6 or we don't have any asynchronous | ||
| # generators alive. | ||
| return | ||
| self._asyncgens.clear() | ||
| results = await tasks.gather( | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1044,6 +1044,43 @@ def test_asyncgen_finalization_by_gc_in_other_thread(self): | ||||||||
| test_utils.run_briefly(self.loop) | ||||||||
| self.assertTrue(status['finalized']) | ||||||||
| def test_shutdown_asyncgens(self): | ||||||||
| # gh-117536: Test shutdown_asyncgens() using asyncio.run() which | ||||||||
| # may cancel the task which closes the asynchronous generator before | ||||||||
| # calling shutdown_asyncgens(). | ||||||||
| ns = {'state': 'not started'} | ||||||||
| async def agen_basic(): | ||||||||
| try: | ||||||||
| ns['state'] = 'started' | ||||||||
| yield 0 | ||||||||
| yield 1 | ||||||||
| finally: | ||||||||
| ns['state'] = 'finalized' | ||||||||
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. This should await something to prove it's getting aclosed properly Suggested change
MemberAuthor 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. Right. I added a second test. I prefer to have two tests, since having 'await' also changes the behavior in a subtle way. | ||||||||
| async def reproducer(agen): | ||||||||
| async for item in agen(): | ||||||||
| break | ||||||||
| asyncio.run(reproducer(agen_basic)) | ||||||||
| self.assertEqual(ns['state'], 'finalized') | ||||||||
| # Similar than previous test, but the generator uses 'await' in the | ||||||||
| # finally block. | ||||||||
| ns['state'] = 'not started' | ||||||||
| async def agen_await(): | ||||||||
| try: | ||||||||
| ns['state'] = 'started' | ||||||||
| yield 0 | ||||||||
| yield 1 | ||||||||
| finally: | ||||||||
| ns['state'] = 'await' | ||||||||
| await asyncio.sleep(0) | ||||||||
| ns['state'] = 'finalized' | ||||||||
| asyncio.run(reproducer(agen_await)) | ||||||||
| self.assertEqual(ns['state'], 'finalized') | ||||||||
| class MyProto(asyncio.Protocol): | ||||||||
| done = None | ||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| :mod:`asyncio`: Make the finalization of asynchronous generators more | ||
| reliable. Patch by Victor Stinner. |