Skip to content

aclose() doesn't stop raise StopAsyncIteration / GeneratorExit to __anext__() #78911

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

Closed
dfee mannequin opened this issue Sep 19, 2018 · 11 comments
Closed

aclose() doesn't stop raise StopAsyncIteration / GeneratorExit to __anext__() #78911

dfee mannequin opened this issue Sep 19, 2018 · 11 comments

Comments

@dfee
Copy link
Mannequin

dfee mannequin commented Sep 19, 2018

BPO 34730
Nosy @Cito, @njsmith, @asvetlov, @dimaqq, @1st1, @dfee

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = None
closed_at = None
created_at = <Date 2018-09-19.00:27:25.958>
labels = ['3.10', 'expert-asyncio']
title = "aclose() doesn't stop raise StopAsyncIteration / GeneratorExit to __anext__()"
updated_at = <Date 2020-08-21.03:12:19.223>
user = 'https://github.com/dfee'

bugs.python.org fields:

activity = <Date 2020-08-21.03:12:19.223>
actor = 'Dima.Tisnek'
assignee = 'none'
closed = False
closed_date = None
closer = None
components = ['asyncio']
creation = <Date 2018-09-19.00:27:25.958>
creator = 'dfee'
dependencies = []
files = []
hgrepos = []
issue_num = 34730
keywords = []
message_count = 8.0
messages = ['325699', '325704', '325920', '325928', '325929', '375739', '375742', '375743']
nosy_count = 6.0
nosy_names = ['cito', 'njs', 'asvetlov', 'Dima.Tisnek', 'yselivanov', 'dfee']
pr_nums = []
priority = 'normal'
resolution = None
stage = None
status = 'open'
superseder = None
type = None
url = 'https://bugs.python.org/issue34730'
versions = ['Python 3.10']

@dfee
Copy link
Mannequin Author

dfee mannequin commented Sep 19, 2018

I expected an async-generator's aclose() method to cancel it's __anext__(). Instead, the task isn't cancelled, and (it seems) manually cleanup is necessary.

@pytest.mark.asyncio
async def test_aclose_cancels():
done = False
event = asyncio.Event()
agen = None

    async def make_agen():
        try:
            await event.wait()
            raise NotImplementedError()
            yield  # pylint: disable=W0101, unreachable
        finally:
            nonlocal done
            done = True

    async def run():
        nonlocal agen
        agen = make_agen()
        await agen.__anext__()

    task = asyncio.ensure_future(run())
    await asyncio.sleep(.01)
await agen.aclose()
await asyncio.sleep(.01)

assert done
assert task.done() and task.exception()

Looking at the tests for CPython, the implementation seems to expect it, but I'm unclear if PEP-525 actually intends for this to be the case:

# Silence ResourceWarnings

Alternatively, maybe I shouldn't be calling aclose, and instead I should be cancelling the task:

@pytest.mark.asyncio
async def test_cancel_finalizes():
done = False
event = asyncio.Event()
agen = None

    async def make_agen():
        try:
            await event.wait()
            raise NotImplementedError()
            yield  # pylint: disable=W0101, unreachable
        finally:
            nonlocal done
            done = True

    async def run():
        nonlocal agen
        agen = make_agen()
        await agen.__anext__()

    task = asyncio.ensure_future(run())
    await asyncio.sleep(.01)
    task.cancel()
    await asyncio.sleep(.01)
assert done
assert task.done()

So the "bug" here is one of PEP-525 clarification.

@dfee dfee mannequin added 3.7 (EOL) end of life topic-asyncio labels Sep 19, 2018
@dfee
Copy link
Mannequin Author

dfee mannequin commented Sep 19, 2018

I've worked around this problem by doing something like this:

async def close_cancelling(agen):
    while True:
        try:
            task = asyncio.ensure_future(agen.__anext__())
            await task
            yield task.result()
        except (GeneratorExit, StopAsyncIteration):
            await agen.aclose()
            task.cancel()
            break


async def run():
    try:
        async for v in close_cancelling(agen):
            received.append(v)
    except asyncio.CancelledError:
        # handle finalization
        pass

Though, I'm still convinced that aclose() should call raise StopAsyncIteration on agen.ag_await

@1st1
Copy link
Member

1st1 commented Sep 20, 2018

Interesting.

Nathaniel, do you have this problem in Trio (aclose() not cancelling an anext()) or cancel scopes solve this problem somehow?

@njsmith
Copy link
Contributor

njsmith commented Sep 20, 2018

Part of the issue here is the one discussed in bpo-30773 / bpo-32526: async generators allow themselves to be re-entered while another asend/athrow/aclose call is in progress, and it causes weird and confusing results. So at a minimum, trying to call aclose while another task is calling asend should make aclose raise an error saying that the generator is already busy.

Of course the OP wants to go further and have aclose actually trigger a cancellation of any outstanding asend/athrow. I see the intuition here, but unfortunately I don't think this can work. Doing a cancellation requires some intimate knowledge of the coroutine runner. You can't just throw in an exception; you also have to unwind the task state. (Eg in the example with 'event.wait', you need to somehow tell the event object that this task is no longer waiting for it, so it shouldn't be notified when the event occurrs.)

So I think we just need to fix ag_running and then recommend people find other ways to interrupt running async generators.

@1st1
Copy link
Member

1st1 commented Sep 20, 2018

So I think we just need to fix ag_running and then recommend people find other ways to interrupt running async generators.

Agree. Speaking of fixing ag_running, could you please review my PR: #7468 I can then commit it and maybe get this fixed in 3.7.1.

@dimaqq
Copy link
Mannequin

dimaqq mannequin commented Aug 21, 2020

#7468 (prohibit asend/etc. reentrance) was merged ~a year ago.

Perhaps it's time to restart the original discussion, whether aclose() should cancel pending anext.

@dimaqq dimaqq mannequin added 3.10 only security fixes and removed 3.7 (EOL) end of life labels Aug 21, 2020
@njsmith
Copy link
Contributor

njsmith commented Aug 21, 2020

Perhaps it's time to restart the original discussion, whether aclose() should cancel pending anext.

I'm still not aware of any way to make this work, technically. So it's a moot point unless someone has a proposal.

@dimaqq
Copy link
Mannequin

dimaqq mannequin commented Aug 21, 2020

Then perhaps the issue should be closed 🤔

@ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
@ezio-melotti ezio-melotti moved this to Todo in asyncio Jul 17, 2022
@kumaraditya303 kumaraditya303 removed the 3.10 only security fixes label Oct 16, 2022
@kumaraditya303
Copy link
Contributor

IMO we should not change anything here but recommend the users to not close async generators concurrently with docs. Doing any change here is going to be both complex probably backwards incompatible as users might be relying on the current behavior.

@gvanrossum
Copy link
Member

Sounds good. I agree that the behavior by now is ingrained and will be hard to change without breaking working user code, and not really worth it. So let's document it.

@kumaraditya303
Copy link
Contributor

Documentation will be taken care of in #100108

@kumaraditya303 kumaraditya303 closed this as not planned Won't fix, can't repro, duplicate, stale Apr 20, 2023
@github-project-automation github-project-automation bot moved this from Todo to Done in asyncio Apr 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Done
Development

No branches or pull requests

4 participants