This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

Author Dmitrii Ivaniushin
Recipients Dmitrii Ivaniushin, asvetlov, yselivanov
Date 2019-07-29.08:50:08
SpamBayes Score -1.0
Marked as misclassified Yes
Message-id <1564390209.24.0.854284418154.issue37703@roundup.psfhosted.org>
In-reply-to
Content
I found some issue that I suppose is a bug.

Let us have long running coroutines. We use them in gather, and one of them raises an error. Since then we cannot cancel the gather anymore, thus remaining children are not cancelable and executed until complete or raise an exception themselves.

===

import asyncio


async def coro_with_error():
    # Coro raises en error with 1 sec delay
    await asyncio.sleep(1)
    raise Exception('Error in coro')


async def cancellator(coro):
    # We use this to cancel gather with delay 1 sec
    await asyncio.sleep(1)
    coro.cancel()


async def success_long_coro():
    # Long running coro, 2 sec
    try:
        await asyncio.sleep(2)
        print("I'm ok!")
        return 42
    except asyncio.CancelledError:
        # Track that this coro is really cancelled
        print('I was cancelled')
        raise


async def collector_with_error():
    gather = asyncio.gather(coro_with_error(), success_long_coro())
    try:
        await gather
    except Exception:
        print(f"WHOAGH ERROR, gather done={gather.done()}")
        print(f'EXC={type(gather.exception()).__name__}')
        # We want to cancel still running success_long_coro()
        gather.cancel()


async def collector_with_cancel():
    # Gather result from success_long_coro()
    gather = asyncio.gather(success_long_coro())
    # schedule cancel in 1 sec
    asyncio.create_task(cancellator(gather))
    try:
        await gather
    except Exception:
        print(f"WHOAGH ERROR, gather done={gather.done()}")
        print(f'EXC={type(gather.exception()).__name__}')
        # We want to cancel still running success_long_coro()
        gather.cancel()
        return

# First case, cancel gather when children are running
print('First case')
loop = asyncio.get_event_loop()
loop.create_task(collector_with_cancel())
# Ensure test coros we fully run
loop.run_until_complete(asyncio.sleep(3))
print('Done')

# Second case, cancel gather when child raise error
print('Second case')
loop = asyncio.get_event_loop()
loop.create_task(collector_with_error())
# Ensure test coros we fully run
loop.run_until_complete(asyncio.sleep(3))
print('Done')

===

Actual output:

    First case
    I was cancelled
    WHOAGH ERROR, gather done=True
    EXC=CancelledError
    Done
    Second case
    WHOAGH ERROR, gather done=True
    EXC=Exception
    I'm ok!
    Done

Expected output:

    First case
    I was cancelled
    WHOAGH ERROR, gather done=True
    EXC=CancelledError
    Done
    Second case
    I was cancelled
    WHOAGH ERROR, gather done=True
    EXC=Exception
    Done

Documentations says:
> If gather() is cancelled, all submitted awaitables (that have not completed yet) are also cancelled.
But it mentions no cases on child coros' exceptions.

From doc:
> If return_exceptions is False (default), the first raised exception is immediately propagated to the task that awaits on gather(). Other awaitables in the aws sequence won’t be cancelled and will continue to run.
Which is true, exception is propagated, the gather has an exception set, marked as done() so its children are not cancelled.

I believe asyncio should allow cancellation in that case.
History
Date User Action Args
2019-07-29 08:50:09Dmitrii Ivaniushinsetrecipients: + Dmitrii Ivaniushin, asvetlov, yselivanov
2019-07-29 08:50:09Dmitrii Ivaniushinsetmessageid: <1564390209.24.0.854284418154.issue37703@roundup.psfhosted.org>
2019-07-29 08:50:09Dmitrii Ivaniushinlinkissue37703 messages
2019-07-29 08:50:08Dmitrii Ivaniushincreate