classification
Title: If a task is canceled at the right moment, the cancellation is ignored
Type: behavior Stage: resolved
Components: asyncio Versions: Python 3.6, Python 3.5
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: yselivanov Nosy List: abacabadabacaba, inada.naoki, yselivanov
Priority: normal Keywords:

Created on 2017-04-12 00:11 by abacabadabacaba, last changed 2017-05-12 05:37 by inada.naoki. This issue is now closed.

Pull Requests
URL Status Linked Edit
PR 1097 merged inada.naoki, 2017-04-13 01:26
PR 1546 merged inada.naoki, 2017-05-11 12:24
PR 1547 merged inada.naoki, 2017-05-11 12:37
Messages (11)
msg291522 - (view) Author: Evgeny Kapun (abacabadabacaba) Date: 2017-04-12 00:11
If I run this code:

    import asyncio as a

    @a.coroutine
    def coro1():
        yield from a.ensure_future(coro2())
        print("Still here")
        yield from a.sleep(1)
        print("Still here 2")

    @a.coroutine
    def coro2():
        yield from a.sleep(1)
        res = task.cancel()
        print("Canceled task:", res)

    loop = a.get_event_loop()
    task = a.ensure_future(coro1())
    loop.run_until_complete(task)

I expect the task to stop shortly after a call to cancel(). It should surely stop when I try to sleep(). But it doesn't. On my machine this prints:

    Canceled task: True
    Still here
    Still here 2

So, cancel() returns True, but the task doesn't seem to be canceled.
msg291523 - (view) Author: Yury Selivanov (yselivanov) * (Python committer) Date: 2017-04-12 00:47
Interesting. It doesn't work for both C and Python versions of the Task. I'll take a look in detail when I return from vacation.
msg291528 - (view) Author: Inada Naoki (inada.naoki) * (Python committer) Date: 2017-04-12 08:18
When task.cancel() called, CancelledError is thrown to coro2.
But coro2 doesn't call `yield from` after task.cancel().
So the exception is never raised.

If you add `yield from` after `task.cancel()`, the script runs expected.
---
$ cat at.py
import asyncio as a

@a.coroutine
def coro1():
    yield from a.ensure_future(coro2())
    print("Still here")
    yield from a.sleep(.1)
    print("Still here 2")

@a.coroutine
def coro2():
    yield from a.sleep(.1)
    res = task.cancel()
    print("Canceled task:", res)
    yield from a.sleep(.1)  # !!! added this line

loop = a.get_event_loop()
task = a.ensure_future(coro1())
loop.run_until_complete(task)

$ ./python.exe at.py
Canceled task: True
Traceback (most recent call last):
  File "at.py", line 19, in <module>
    loop.run_until_complete(task)
  File "/Users/inada-n/work/python/cpython/Lib/asyncio/base_events.py", line 465, in run_until_complete
    return future.result()
concurrent.futures._base.CancelledError
msg291531 - (view) Author: Inada Naoki (inada.naoki) * (Python committer) Date: 2017-04-12 08:46
This behavior is documented as:

https://docs.python.org/3.6/library/asyncio-task.html#asyncio.Task.cancel

> Unlike Future.cancel(), this does not guarantee that the task will be cancelled: the exception might be caught and acted upon, delaying cancellation of the task or preventing cancellation completely. The task may also return a value or raise a different exception.
>
> Immediately after this method is called, cancelled() will not return True (unless the task was already cancelled). A task will be marked as cancelled when the wrapped coroutine terminates with a CancelledError exception (even if cancel() was not called).

I agree that this behavior is somewhat surprising.
But I don't know how can I fix the behavior.
msg291577 - (view) Author: Evgeny Kapun (abacabadabacaba) Date: 2017-04-12 23:11
The problem is that the task doesn't catch CancelledError, yet it disappears.
msg291578 - (view) Author: Inada Naoki (inada.naoki) * (Python committer) Date: 2017-04-13 00:19
> The problem is that the task doesn't catch CancelledError, yet it disappears.

The problem is CancelledError is not raised, even it's thrown.
Task can't catch exception not raised.  See below example which demonstrates how task works.

---
from asyncio import CancelledError

cancelled = False

def coro():
    global cancelled
    print(1)
    yield (1,)
    print("cancel")
    cancelled = True
    #print(2)
    #yield (2,)  # uncomment this line makes cancel success.

c = coro()

while True:
    try:
        if cancelled:
            r = c.throw(CancelledError)
        else:
            r = c.send(None)
    except StopIteration:
        print("end")
        break
    except CancelledError:
        print("cancelled")
        break
msg291579 - (view) Author: Yury Selivanov (yselivanov) * (Python committer) Date: 2017-04-13 00:44
In Evgeny's example the 'task' is 'coro1' (not 'coro2'). It has plenty of yield points after being cancelled.
msg291580 - (view) Author: Inada Naoki (inada.naoki) * (Python committer) Date: 2017-04-13 01:07
> In Evgeny's example the 'task' is 'coro1' (not 'coro2'). It has plenty of yield points after being cancelled.

Since coro1 waiting coro2 when cancelling, Task(coro1).cancel() redirect to Task(coro2).cancel().

But I was wrong about "CancelledError is thrown to coro2."
Task(coro2) is cancelled in last step.  CancelledError is not thrown to coro2 actually.
So there is chance to detect it.
msg293492 - (view) Author: Inada Naoki (inada.naoki) * (Python committer) Date: 2017-05-11 12:18
New changeset 991adca012f5e106c2d4040ce619c696ba6f9c46 by INADA Naoki in branch 'master':
bpo-30048: asyncio: fix Task.cancel() was ignored. (GH-1097)
https://github.com/python/cpython/commit/991adca012f5e106c2d4040ce619c696ba6f9c46
msg293493 - (view) Author: Inada Naoki (inada.naoki) * (Python committer) Date: 2017-05-11 12:56
New changeset 3dc7c52a9f4fb83be3e26e31e2c7cd9dc1cb41a2 by INADA Naoki in branch '3.6':
bpo-30048: asyncio: fix Task.cancel() was ignored. (GH-1546)
https://github.com/python/cpython/commit/3dc7c52a9f4fb83be3e26e31e2c7cd9dc1cb41a2
msg293524 - (view) Author: Inada Naoki (inada.naoki) * (Python committer) Date: 2017-05-12 05:34
New changeset 5e94dedcddf5e09164bf20f18a3c701eeb96c71e by INADA Naoki in branch '3.5':
bpo-30048: asyncio: fix Task.cancel() was ignored. (GH-1547)
https://github.com/python/cpython/commit/5e94dedcddf5e09164bf20f18a3c701eeb96c71e
History
Date User Action Args
2017-05-12 05:37:59inada.naokisetstatus: open -> closed
resolution: fixed
stage: resolved
2017-05-12 05:34:42inada.naokisetmessages: + msg293524
2017-05-11 12:56:44inada.naokisetmessages: + msg293493
2017-05-11 12:37:18inada.naokisetpull_requests: + pull_request1646
2017-05-11 12:24:58inada.naokisetpull_requests: + pull_request1645
2017-05-11 12:18:41inada.naokisetmessages: + msg293492
2017-04-13 01:26:41inada.naokisetpull_requests: + pull_request1240
2017-04-13 01:07:37inada.naokisetmessages: + msg291580
2017-04-13 00:44:56yselivanovsetmessages: + msg291579
2017-04-13 00:19:13inada.naokisetmessages: + msg291578
2017-04-12 23:11:14abacabadabacabasetmessages: + msg291577
2017-04-12 08:46:31inada.naokisetmessages: + msg291531
2017-04-12 08:18:02inada.naokisetmessages: + msg291528
2017-04-12 00:47:18yselivanovsetassignee: yselivanov

messages: + msg291523
nosy: + inada.naoki
2017-04-12 00:11:24abacabadabacabacreate