python / python/cpython

asyncio.Barrier: cancelling a waiting task mid-round can cause a later arrival to reuse its index

Open
#155,233 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

stdlib topic-asyncio type-bug
Dominant language
Python
Stars
77.2k
Forks
35.9k
PR merge metrics
PR metrics pending

Description

Bug description:

asyncio.Barrier.wait() is documented to return "a unique and individual index number from 0 to 'parties-1'" for each task that passes the barrier together. If a task's wait() call is cancelled while the barrier is still filling (i.e. before enough parties have arrived), a task that arrives afterward can be assigned the same index as another task that is already waiting in that same round — so two tasks in the same successful release get the same index, and another index in the range is never handed out at all.

Reproducer
import asyncio

async def main():
    b = asyncio.Barrier(3)
    results = []

    async def party(name):
        try:
            idx = await b.wait()
            results.append((name, idx))
        except asyncio.CancelledError:
            results.append((name, "cancelled"))
            raise

    t1 = asyncio.create_task(party("A"))
    t2 = asyncio.create_task(party("B"))
    await asyncio.sleep(0)
    await asyncio.sleep(0)

    t1.cancel()          # A leaves while the barrier is still filling
    try:
        await t1
    except asyncio.CancelledError:
        pass
    await asyncio.sleep(0)

    t3 = asyncio.create_task(party("C"))
    t4 = asyncio.create_task(party("D"))   # completes the round of 3
    await asyncio.gather(t2, t3, t4)

    print(results)

asyncio.run(main())

Output on current main:

[('A', 'cancelled'), ('D', 2), ('B', 1), ('C', 1)]

B and C both get index 1; index 0 is never handed out to any of the three tasks that actually pass the barrier together (B, C, D).

Root cause

In Lib/asyncio/locks.py, Barrier.wait() assigns index = self._count eagerly, at arrival, before the round's final membership is known:

index = self._count
self._count += 1
if index + 1 == self._parties:
    await self._release()
else:
    await self._wait()
return index

self._count is decremented again when a party leaves early (cancellation), in the finally block. Since index is derived from a counter that can both increase (new arrivals) and decrease (early departures) within the same round, a later arrival can end up with the exact self._count value an earlier, still-waiting task already captured as its own index.

This is specific to the asyncio implementation — threading.Barrier doesn't need to handle a party "un-arriving" mid-wait the way a cancellable asyncio.Task can, so the sync version doesn't have an analogous bug.

I don't see an existing report for this (checked issues/PRs mentioning asyncio.Barrier).

A working fix

Indices can't be assigned correctly at arrival time when membership can still change; they need to be finalized once the round's exact release cohort is known (at release time), assigned once from the current arrival order. I have a fix + regression test ready and will open a PR against this issue.

CPython versions tested on:

3.16 (main)

Operating systems tested on:

Linux (WSL), should be platform-independent (no OS-specific code involved)

Linked PRs
  • gh-155235

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with Barrier.wait in Lib/asyncio/locks.py and run the reproducer described in the issue to observe the duplicate index after cancellation. Review the linked PR gh-155235 and its regression test. Done means the cancellation scenario releases a round with unique indices for every task that passes.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.