Skip to content
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

Finalize async generators in the correct context #150

Merged
merged 2 commits into from
Apr 24, 2024

Conversation

oremanj
Copy link
Member

@oremanj oremanj commented Apr 23, 2024

Install custom async generator hooks that finalize each asyncgen in the same context (trio vs asyncio mode, and the correct trio-asyncio loop if asyncio mode) as it was first iterated. Fixes #92.

@oremanj oremanj force-pushed the asyncgens branch 3 times, most recently from 3efdbf1 to eabd970 Compare April 23, 2024 03:07
Copy link

@agnesnatasya agnesnatasya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont know enough to catch all the edge cases involving async gens, but the 'happy path' looks reasonable to me

tests/test_trio_asyncio.py Show resolved Hide resolved
Comment on lines 269 to 270
for _ in range(5):
gc.collect()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a significance of running this 5 times?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! it's unfortunate that we need to duplicate util functions between trio and trio-asyncio 😞


def firstiter(self, agen):
if sniffio_library.name == "asyncio":
loop = asyncio.get_running_loop()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to handle the case where there's no running loop / if the loop is no longer running?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's no running loop, then sniffio should not be reporting that we're in asyncio mode.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right, what about in finalizer? do we need to check if it's still running?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python remembers the finalizer hook that was active when an async generator was firstiter'ed, and calls it even if it is no longer the active finalizer hook. Thus, finalizer hooks have to be prepared to be called after the event loop has stopped. This can happen even without our intervention (ie with pure asyncio) so I don't think we need to do anything special. The enqueued task will run if the event loop is resumed.

oremanj@shainfra10:/usr/scratch/oremanj$ python3.12
Python 3.12.2 (main, Feb 12 2024, 10:12:13) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> async def agen(): yield
...
>>> async def main():
...   global ag
...   ag = agen()
...   await ag.asend(None)
...
>>> loop = asyncio.new_event_loop()
>>> loop.run_until_complete(main())
>>>

# now we have a stopped event loop and a half-finished async generator:
>>> loop._asyncgens
{<weakref at 0x7f7004348b80; to 'async_generator' at 0x7f700472eda0 (agen)>}
>>> loop._ready
deque([])

# if we destroy the async generator, asyncio will schedule its finalizer:
>>> del ag
>>> loop._asyncgens
set()
>>> loop._ready
deque([<Handle BaseEventLoop.create_task(<async_genera...x7f700432fc00>)>])

trio_asyncio/_loop.py Show resolved Hide resolved
tests/conftest.py Show resolved Hide resolved

def firstiter(self, agen):
if sniffio_library.name == "asyncio":
loop = asyncio.get_running_loop()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right, what about in finalizer? do we need to check if it's still running?

Comment on lines 269 to 270
for _ in range(5):
gc.collect()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! it's unfortunate that we need to duplicate util functions between trio and trio-asyncio 😞

tests/test_trio_asyncio.py Outdated Show resolved Hide resolved
async with trio_asyncio.open_loop() as loop2:
async with trio.open_nursery() as nursery:
# Make sure the iterate_one aio tasks don't get
# cancelled before they start:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would add an assertion that the dispatcher object is the same for both loops and that the refcount is 2

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree. The test is testing observable public behavior: that each async generator is finalized in the context where it was first iterated. If we later change trio-asyncio to ensure that in some other way, the test should still work.

I'll add a test that once both trio-asyncio loops are closed, the global async generator hooks go back to what they were before the loops were opened.

@tjstum tjstum self-requested a review April 24, 2024 22:11
@oremanj oremanj merged commit 898ab8d into python-trio:master Apr 24, 2024
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Arbitrate between asyncio and Trio async generator hooks
3 participants