Can't cancel an awaiting task?

I’m trying to cancel a task that is awaiting on the completion of another future, however this cause the code to abort.

Looking a bit into the code, it seems that AsyncTaskChain::do_remove() (which is called by cancel()) does not remove a task in S_awaiting state.

The cancel()method checks if the task is done after calling remove, and, if not, an assert is triggered.

from panda3d.core import AsyncFuture

future = AsyncFuture()

async def waiting_task():
    await future

async def cancel_task(t):
    print(t.get_state())
    t.cancel()

import direct.directbase.DirectStart

t = taskMgr.add(waiting_task())
taskMgr.add(cancel_task(t))
base.run()

And the output is :

6
Assertion failed: done() at line 465 of panda/src/event/asyncTask.cxx
:task(error): PythonTask cancel_task exception was never retrieved:
Traceback (most recent call last):
  File "test/test_async3.py", line 10, in cancel_task
    t.cancel()
AssertionError: done() at line 465 of panda/src/event/asyncTask.cxx

This is in fact implemented, but only on the master branch, not in 1.10. In 1.10, cancel() is an alias for remove(). On the master branch, it reschedules the task and throws a CancelledException into it.

Thanks for the info! Perhaps it´s time for me to switch to the master branch.

I want to add that I’m not sure whether we yet implemented the behaviour of asyncio, which is (I believe) that the future you’re awaiting inside the cancelled task also gets cancelled (eg. if it’s a cancellable request like a tex.prepare() call, an interval, or a ModelLoadRequest). I actually feel a little ambivalent about whether that should be the right behaviour, but we should probably match what asyncio does.

With asyncio, if you’re awaiting on a future in a task and the task gets cancelled the future is also cancelled (I believe using a call to the cancel() method if I understand correctly asyncio.tasks.Task).

Thanks for confirming. Could you please file a feature request for 1.11 that futures being awaited by a coroutine task should be cancelled if the task is cancelled?

The feature request is Futures being awaited by a coroutine task should be cancelled if the task is cancelled · Issue #1136 · panda3d/panda3d · GitHub

2 Likes

I modified my code to use the new feature, it works fine (thank you for your great work btw !) except with gather().

If I cancel a task awaiting on a gather(), the code aborts with either :

AssertionError: !AtomicAdjust::dec(_num_pending) at line 422 of panda/src/event/asyncFuture.cxx

or

AssertionError: unexpected task state at line 353 of panda/src/event/asyncFuture.cxx

Here is a piece of code that trigger the issue :

from direct.task.Task import gather
from panda3d.core import AsyncFuture

f1 = AsyncFuture()
f2 = AsyncFuture()

async def t1():
    await f1

async def t2():
    await f2

async def test():
    await gather(t1(), t2())
    #await t1()

def cancel(to_cancel):
    to_cancel.cancel()
    return to_cancel.done

def printf(task):
    print(f1)
    print(f2)
    return task.done

def add_task():
    t = taskMgr.add(test(), 'test')
    if False:
        taskMgr.step()
        t.cancel()
    else:
        taskMgr.add(cancel, 'cancel', extraArgs=[t])
    taskMgr.add(printf, 'printf')

import direct.directbase.DirectStart

add_task()
base.run()

Note that with this code I can only get the !AtomicAdjust::dec(_num_pending) assertion error.

It’s because the AsyncGatheringFuture::cancel() code assumes that if cancel() on a future returns true, it is really cancelled right then and there; but this is no longer true because calling cancel() on a task only schedules it to be cancelled in the future (and then it may still resist the cancellation).

The solution is to change gather().cancel() to make fewer assumptions.

As for “unexpected task state”, this is a rare corner case: cancelling the task is also causing the gather() to be cancelled, which would be fine if it were taking effect right away, but this happens delayed since it needs to wait for the contained task to (possibly) handle the cancellation. So, the task gets reactivated but then when the gather() finishes being cancelled it tries to also reactivate the task, and gets confused because it is already active. I think I can just handle this with a no-op if the task is already active.

Checked in fixes for both issues to the master branch.

Thanks for the fixes and the explanation ! I made a new build with these fixes and so far I don’t get any errors any more. I will continue stressing the app to see if everything is stable.

Bw, I’m using the coroutine tasks inside a common thread but also across threads and it works as expected for both cases :slight_smile:

1 Like