Thomas Ahle
Thomas Ahle

Reputation: 31604

Getting Result from Cancelled Task

I have a program, roughly like the example below. A task is gathering a number of values and returning them to a caller. Sometimes the tasks may get cancelled. In those cases, I still want to get the results the tasks have gathered so far. Hence I catch the CancelledError exception, clean up, and return the completed results.

async def f():
    results = []
    for i in range(100):
        try:
            res = await slow_call()
            results.append(res)
        except asyncio.CancelledError:
            results.append('Undecided')
    return results

def on_done(task):
    if task.cancelled():
        print('Incomplete result', task.result()
    else:
        print(task.result())

async def run():
    task = asyncio.create_task(f())
    task.add_done_callback(on_done)

The problem is that the value returned after a task is cancelled doesn't appear to be available in the task. Calling task.result() simply rethrows CancelledError. Calling task._result is just None.

Is there a way to get the return value of a cancelled task, assuming it has one?

Edit: I realize now that catching the CancelledError results in the task not being cancelled at all. This leaves me with another conundrum: How do I signal to the tasks owner that this result is only a "half" result, and the task has really been cancelled. I suppose I could add an extra return value indicating this, but that seems to go against the whole idea of the task cancellation system.

Any suggestions for a good approach here?

Upvotes: 1

Views: 621

Answers (2)

Sam Mason
Sam Mason

Reputation: 16184

I'm a long way away from understanding the use case, but the following does something sensible for me:

import asyncio

async def fn(results):
    for i in range(10):
        # your slow_call
        await asyncio.sleep(0.1)
        results.append(i)

def on_done(task, results):
    if task.cancelled():
        print('incomplete', results)
    else:
        print('complete', results)

async def run():
    results = []
    task = asyncio.create_task(fn(results))
    task.add_done_callback(lambda t: on_done(t, results))
    # give fn some time to finish, reducing this will cause the task to be cancelled
    # you'll see the incomplete message if this is < 1.1
    await asyncio.sleep(1.1)

asyncio.run(run())

it's the use of add_done_callback and sleep in run that feels very awkward and makes me think I don't understand what you're doing. maybe posting something to https://codereview.stackexchange.com containing more of the calling code would help get ideas of better ways to structure things. note that there are other libraries like trio that provide much nicer interfaces to Python coroutines than the asyncio builtin library (which was standardised prematurely IMO)

Upvotes: 1

Paolo Ardissone
Paolo Ardissone

Reputation: 847

I don't think that is possible, because in my opinion, collides with the meaning of cancellation of a task. You can implements a similar behavior inside your slow_call, by triggering the CancelledError, catching it inside your function and then returns whatever you want.

Upvotes: 0

Related Questions