On 14-05-31 09:09 AM, Andrew Svetlov wrote:
> I wrote simple script to check Twisted behavior:
> [...]
> It shows (if I understand correctly) that cancelling of inner deferred
> doesn't cancel outer one, but outer gets CanceledError exception.
> Cancelling of outer deferred doesn't affects inner one.

Thanks for the example Andrew. I read the output the same way.

So both cases (current behaviour or what we have been considering)
differs from Twisted.


> Maybe writing a document with comprehensive description of future/task
> cancellation can help to make good decision?

Cancellation is tricky and I agree it needs to be carefully documented. 
I'm happy to document whatever decision we come up with here, but I
hesitate to go through the effort of documenting something that might be
throwaway.


I think the crux of the behavioural questions are:

 1. If an outer Future is cancelled, what should happen to the inner Future?
 2. If an inner Future is cancelled, what should happen to the outer
    Future(s)?


I feel like #1 isn't really in dispute, even though it differs from
Twisted.  In Tulip, the inner future is implicitly cancelled unless
explicitly shielded.

This seems intuitive when you think about a task as being the sum of all
its parts.  That is, a task might yield a bunch of subtasks to do what
it needs to do.  If I cancel a task, it's quite likely that those
subtasks aren't needed anymore.  In those cases where they are, they can
be shielded.

But in terms of #2, is it intuitive for the parent task to be cancelled
too?  The parent task is obligated to catch exceptions raise by tasks it
yields from, but if it doesn't, should we considered it cancelled, or
just done with exception?

To argue the case against the parent task being considered cancelled,
I'll point out what seems like an inconsistency if we do that.

Imagine we have a task that fetches all the assets for a web page.  The
get_web_page() coroutine calls get_asset() for each asset on the page. 
We want to have a timeout on the retrieval, so we wrap get_asset() in
asyncio.wait_for().

Today, if the timeout happens, the get_asset() task (whether explicitly
passed or implicitly created by wait_for()) is cancelled.  But even if
get_web_page() doesn't catch the exception raised by wait_for(), it's
itself not considered cancelled.  Why would an explicit cancellation of
the get_asset() task cause get_web_page() to be cancelled?  In both
cases get_asset() is cancelled, it's just an implicit cancellation due
to timeout rather than an explicit cancellation.  Is that difference
sufficient to justify the change?  IMO in both cases get_web_page()
should not be cancelled.


import asyncio

EXPLICIT_CANCELLATION = False

@asyncio.coroutine
def get_asset(n):
    # Pretend we're fetching something.
    print('get asset:', n)
    yield from asyncio.sleep(n)

@asyncio.coroutine
def get_web_page(assets):
    for asset in assets:
        task = asyncio.Task(get_asset(asset))
        if EXPLICIT_CANCELLATION:
            loop.call_later(1.5, task.cancel)
            yield from task
        else:
            yield from asyncio.wait_for(task, 1.5)

def show(task):
    print('{} is cancelled: {}'.format(task, task.cancelled()))

loop = asyncio.get_event_loop()
page_task = asyncio.Task(get_web_page(range(3)))
page_task.add_done_callback(show)
loop.run_forever()


Does that help to make the case?  Hopefully I'm not alone in thinking
that page_task.cancelled() shouldn't be different between the implicit
and explicit cancellation cases?

Thanks,
Jason.

Reply via email to