agronholm / anyio

High level asynchronous concurrency and networking framework that works on top of either trio or asyncio
MIT License
1.74k stars 134 forks source link

Different cancel scope behaviour on asyncio vs Trio #698

Open arthur-tacca opened 5 months ago

arthur-tacca commented 5 months ago

Things to check first

AnyIO version

4.3.0

Python version

3.10.13; 3.12.1

What happened?

Run a cancel scope within another cancel scope. Cancel the inner one then await something so that a cancellation exception is raised, but during handling also cancel the outer one (but do not call any more unshielded async functions before the end of the inner block). With the asyncio backend, the inner block will swallow the cancellation exception, allowing the outer block to continue running (until the next unshielded await in it), while the Trio backend will propagate the exception right out to the end of the outer block

By the way, the Trio docs give the impression that it is meant to work the same way as the asyncio backend does. (See discussion on Trio Discourse: Do Cancelled exceptions “know” which block they belong to?)

How can we reproduce the bug?

import anyio

async def cancel_check():
    with anyio.CancelScope() as outer_scope:
        with anyio.CancelScope() as inner_scope:
            await anyio.sleep(0.1)
            inner_scope.cancel()
            print("inner cancelled")
            try:
                await anyio.sleep(0.1)
            finally:
                outer_scope.cancel()
                print("outer cancelled")
            print("should not be here inner")
        print("should not be here outer")
    print("all done")

anyio.run(cancel_check, backend="trio")

Output with Trio backend:

inner cancelled
outer cancelled
all done

Output with asyncio backend:

inner cancelled
outer cancelled
should not be here outer
all done
arthur-tacca commented 5 months ago

As my second comment at the linked post says, Trio changed to the current behaviour in version 0.11.0 (2019-02-09).

agronholm commented 4 months ago

I tried fixing this by raising a new CancelledError from CancelScope.__exit__(), but that interacted poorly with task groups, as they're not prepared to handle exceptions falling out of that method (timeouts in taskgroup.cancel_scope are not a thing).