Skip to content

Cancelling an unpacked-once Promise drags down the nested Promise, contradicting the isolation comment in _fully_unpack_unsafe #117

Description

@teremterem

Summary

Promise._fully_unpack_unsafe() carries a comment (promising/promise.py:820-833) claiming that cancelling a Promise whose first unpacking step already produced a nested Promise cancels only the wrapper:

# Note: cancelling this Promise does NOT propagate cancellation
# into the nested Promise being awaited below - asyncio's
# task-cancellation lands on this task and unwinds upward; the
# inner Promise's own task keeps running independently.

This does not match asyncio's actual behavior. The nested Promise IS dragged down: it ends up in _CANCELLED_BEFORE_UNPACKED_ONCE instead of running to completion.

Mechanism

When the outer Promise's full-unpacking task is suspended on result = await result (promising/promise.py:834-835), the task's _fut_waiter is the inner Promise's unpacking task (awaiting a Promise ultimately suspends on its task). Task.cancel() on the outer's task therefore calls cancel() on the _fut_waiter - i.e. on the inner Promise's task - so the cancellation propagates downward into the nested Promise, not just upward through the wrapper.

Observed failure

tests/race_conditions/test_cancellation_races.py::test_cancel_after_first_unpacking_leaves_nested_promise_running pins the isolation semantics the comment claims, and currently fails:

assert not inner.done()
AssertionError: assert not True
 +  where True = done()
 +    where done = <'..._gated_inner' Promise id=...>._CANCELLED_BEFORE_UNPACKED_ONCE.done

The parts of that test covering the outer promise's post-cancellation readability (unpacked_once() stays True, intermediate_promise() returns the nested Promise instead of raising) pass today.

What needs to happen

This is the TODO [CANCELLATION] design question at promising/promise.py:824-833, and the refactoring must resolve it explicitly - either:

  1. Make the isolation real - shield the nested await (e.g. asyncio.shield-style) so cancelling the wrapper leaves the pass-through Promise's own task running independently. The TODO's own reasoning leans this way: a Promise that merely "passes through" another Promise didn't originate it, and shouldn't decide its fate. Keep the current test assertions.
  2. Embrace propagation - declare that cancelling a wrapper cancels the nested Promise it is currently awaiting, update the comment and docs to say so, and flip the test's assertions.

The decision should also spell out how this interacts with hierarchical (parent/child) cancellation, which the same TODO block flags as a separate relationship from "promise returned by another promise".

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions