Skip to content

asyncio.shield() leaks cancelled waiters #155854

Description

@deadlovelll

Bug report

Bug description:

asyncio.shield() never releases a waiter that was cancelled, the cancelled task stays referenced by the shielded task forever. As a result, printing call graph raises an exception

Repro:

import asyncio

async def main():
    inner = asyncio.create_task(asyncio.sleep(3600), name="INNER")

    async def waiter():
        await asyncio.shield(inner)

    alive= asyncio.create_task(waiter(), name="ALIVE")
    for i in range(2):
        t = asyncio.create_task(waiter(), name=f"CANCELLED-{i}")
        await asyncio.sleep(0)
        t.cancel()
    await asyncio.sleep(0)

    asyncio.print_call_graph(inner)
    inner.cancel()

asyncio.run(main())

Expected:

* Task(name='INNER', id=0x35f290b0410)
  + Call stack:
  |   File '/Users/timofeiivankov/cpython/Lib/asyncio/tasks.py', line 705, in async sleep()
  + Awaited by:
    * Task(name='ALIVE', id=0x35f290b0610)
      + Call stack:
      |   File '/Users/timofeiivankov/cpython/repro.py', line 7, in async main.<locals>.waiter()

Actually:

Traceback (most recent call last):
  File "/Users/timofeiivankov/cpython/repro.py", line 19, in <module>
    asyncio.run(main())
    ~~~~~~~~~~~^^^^^^^^
  File "/Users/timofeiivankov/cpython/Lib/asyncio/runners.py", line 205, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "/Users/timofeiivankov/cpython/Lib/asyncio/runners.py", line 128, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/Users/timofeiivankov/cpython/Lib/asyncio/base_events.py", line 725, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "/Users/timofeiivankov/cpython/repro.py", line 16, in main
    asyncio.print_call_graph(inner)
    ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
  File "/Users/timofeiivankov/cpython/Lib/asyncio/graph.py", line 276, in print_call_graph
    print(format_call_graph(future, depth=depth, limit=limit), file=file)
          ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/timofeiivankov/cpython/Lib/asyncio/graph.py", line 260, in format_call_graph
    render_level(graph, buf, 0)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^
  File "/Users/timofeiivankov/cpython/Lib/asyncio/graph.py", line 252, in render_level
    render_level(fut, buf, level + 1)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
  File "/Users/timofeiivankov/cpython/Lib/asyncio/graph.py", line 217, in render_level
    if f.f_generator is None:
       ^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'f_generator'

Proposed fix: add cleanup in _outer_done_callback of asyncio.shield()

    def _outer_done_callback(outer):
        if not inner.done():
            inner.remove_done_callback(_inner_done_callback)
            if cur_task is not None:
                inner.remove_done_callback(_clear_awaited_by_callback)
                futures.future_discard_from_awaited_by(inner, cur_task)
            # Keep only one callback to log on cancel
            inner.remove_done_callback(_log_on_exception)
            inner.add_done_callback(_log_on_exception)

I have a fix ready

CPython versions tested on:

CPython main branch

Operating systems tested on:

macOS

Linked PRs

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

    stdlibStandard Library Python modules in the Lib/ directorytopic-asynciotype-bugAn unexpected behavior, bug, or error

    Projects

    • Status
      Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions