Bug report
Bug description:
On Python 3.14, asyncio.wait(..., return_when=FIRST_COMPLETED) permanently retains every
completed waiting task when one of the awaited futures is long-lived (never resolves). Each call
registers the waiting task on the long-lived future's await-graph (_asyncio_awaited_by) but never
discards it once the wait returns, so the task is pinned until that future finally resolves — which,
for a "signal" future used as a cancellation/close marker, can mean the lifetime of the process.
This is the same omission already fixed for asyncio.staggered.staggered_race in
gh-129195 (add to awaited_by → must discard), but asyncio.wait still has it.
import asyncio, gc
async def main():
loop = asyncio.get_running_loop()
immortal = loop.create_future() # never resolves (e.g. a connection-close signal)
def done_tasks():
gc.collect()
return sum(1 for o in gc.get_objects() if isinstance(o, asyncio.Task) and o.done())
async def race_once():
await asyncio.wait(
{asyncio.ensure_future(asyncio.sleep(0)), immortal},
return_when=asyncio.FIRST_COMPLETED,
)
base = done_tasks()
for _ in range(1000):
await asyncio.create_task(race_once())
print("retained done tasks:", done_tasks() - base)
immortal.cancel()
asyncio.run(main())
3.13: retained done tasks: 1
3.14: retained done tasks: 1000 # grows linearly with calls; freed only when `immortal` resolves
Related
CPython versions tested on:
CPython main branch, 3.14
Operating systems tested on:
macOS
Linked PRs
Bug report
Bug description:
On Python 3.14,
asyncio.wait(..., return_when=FIRST_COMPLETED)permanently retains everycompleted waiting task when one of the awaited futures is long-lived (never resolves). Each call
registers the waiting task on the long-lived future's await-graph (
_asyncio_awaited_by) but neverdiscards it once the wait returns, so the task is pinned until that future finally resolves — which,
for a "signal" future used as a cancellation/close marker, can mean the lifetime of the process.
This is the same omission already fixed for
asyncio.staggered.staggered_raceingh-129195 (add to awaited_by → must discard), but
asyncio.waitstill has it.Related
staggered_racemissing discard)CPython versions tested on:
CPython main branch, 3.14
Operating systems tested on:
macOS
Linked PRs