{"record":{"id":"0e2cb2d5f6c97786","repo":"python/cpython","slug":"boundedsemaphore-released-too-many-times","errorCode":null,"errorMessage":"BoundedSemaphore released too many times","messagePattern":"BoundedSemaphore released too many times","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"Lib/asyncio/locks.py","lineNumber":461,"sourceCode":"                # `fut` is now `done()` and not `cancelled()`.\n                return True\n        return False\n\n\nclass BoundedSemaphore(Semaphore):\n    \"\"\"A bounded semaphore implementation.\n\n    This raises ValueError in release() if it would increase the value\n    above the initial value.\n    \"\"\"\n\n    def __init__(self, value=1):\n        self._bound_value = value\n        super().__init__(value)\n\n    def release(self):\n        if self._value >= self._bound_value:\n            raise ValueError('BoundedSemaphore released too many times')\n        super().release()\n\n\n\nclass _BarrierState(enum.Enum):\n    FILLING = 'filling'\n    DRAINING = 'draining'\n    RESETTING = 'resetting'\n    BROKEN = 'broken'\n\n\nclass Barrier(mixins._LoopBoundMixin):\n    \"\"\"Asyncio equivalent to threading.Barrier\n\n    Implements a Barrier primitive.\n    Useful for synchronizing a fixed number of tasks at known synchronization\n    points. Tasks block on 'wait()' and are simultaneously awoken once they\n    have all made their call.","sourceCodeStart":443,"sourceCodeEnd":479,"githubUrl":"https://github.com/python/cpython/blob/bc6749cc3b5ae4a5e88a6cc2d5b3bebbe354eae6/Lib/asyncio/locks.py#L443-L479","documentation":"asyncio.BoundedSemaphore exists precisely to catch over-release: if release() would push the internal counter above the initial bound, it raises ValueError instead of silently growing. This surfaces unbalanced acquire/release logic that a plain Semaphore would hide.","triggerScenarios":"Calling release() more times than acquire() succeeded; releasing in a finally block of a code path that never acquired; mixing manual release with 'async with sem' (which releases automatically).","commonSituations":"'async with sem:' plus a manual sem.release() in cleanup; exception paths that release on tasks which timed out before acquiring; refactors that moved acquire() behind a condition but kept the unconditional release.","solutions":["Remove duplicate releases; prefer 'async with sem:' which releases exactly once","Release only on the success path of acquire: pair acquire/release in try/finally entered after acquire completes","Audit with BoundedSemaphore during development to find the imbalance, even if plain Semaphore is used in production"],"exampleFix":"# before\nasync with sem:\n    ...\nfinally:\n    sem.release()  # second release -> ValueError\n\n# after\nasync with sem:  # releases exactly once\n    ...","handlingStrategy":"validation","validationCode":"# BoundedSemaphore exposes _bound_value; check headroom before release\nif sem._value >= sem._bound_value:\n    # release() would raise; skip or fix imbalance","typeGuard":null,"tryCatchPattern":"try:\n    sem.release()\nexcept ValueError:\n    # over-release bug surfaced; log and fix acquire/release pairing","preventionTips":["Use 'async with sem:' so release happens exactly once","Never pair the context manager with a manual release()","Develop against BoundedSemaphore to catch imbalances early"],"tags":["asyncio","semaphore","synchronization","resource-leak"],"backgroundTag":null,"analyzedSha":"bc6749cc3b5ae4a5e88a6cc2d5b3bebbe354eae6","analyzedAt":"2026-08-14T22:01:13.976Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}