{"record":{"id":"1f9501ab75247f54","repo":"Textualize/textual","slug":"rlock-release-called-too-many-times","errorCode":null,"errorMessage":"RLock.release called too many times","messagePattern":"RLock\\.release called too many times","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"src/textual/rlock.py","lineNumber":30,"sourceCode":"        self._lock = Lock()\n\n    async def acquire(self) -> None:\n        \"\"\"Wait until the lock can be acquired.\"\"\"\n        task = current_task()\n        assert task is not None\n        if self._owner is None or self._owner is not task:\n            await self._lock.acquire()\n            self._owner = task\n        self._count += 1\n\n    def release(self) -> None:\n        \"\"\"Release a previously acquired lock.\"\"\"\n        task = current_task()\n        assert task is not None\n        self._count -= 1\n        if self._count < 0:\n            # Should not occur if every acquire as a release\n            raise RuntimeError(\"RLock.release called too many times\")\n        if self._owner is task:\n            if not self._count:\n                self._owner = None\n                self._lock.release()\n\n    @property\n    def is_locked(self):\n        \"\"\"Return True if lock is acquired.\"\"\"\n        return self._lock.locked()\n\n    async def __aenter__(self) -> None:\n        \"\"\"Asynchronous context manager to acquire and release lock.\"\"\"\n        await self.acquire()\n\n    async def __aexit__(self, _type, _value, _traceback) -> None:\n        \"\"\"Exit the context manager.\"\"\"\n        self.release()\n","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/Textualize/textual/blob/06dbeef4bb70fb718236aa418ed658ef4667a126/src/textual/rlock.py#L12-L48","documentation":"The reentrant async lock detected a release() without a matching acquire() (its internal count went negative). This indicates unbalanced lock usage, typically releasing from a different task or releasing twice.","triggerScenarios":"Calling release() on an RLock more times than acquire() succeeded, or releasing in a finally block after acquire raised/timed out.","commonSituations":"Manual acquire/release in try blocks where acquire failed but release still ran, or copying release calls across code paths.","solutions":["Use 'async with lock:' instead of manual acquire/release","Ensure release() is only called on the success path of acquire()","Audit for double-release in exception handlers"],"exampleFix":"# before\nawait lock.acquire()\ntry:\n    ...\nfinally:\n    lock.release()  # may double-release\n# after\nasync with lock:\n    ...","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    lock.release()\nexcept RuntimeError:\n    pass  # already released","preventionTips":["Always prefer 'async with lock' over manual acquire/release","Track ownership when passing locks across tasks"],"tags":["locking","async","concurrency","textual"],"backgroundTag":"unbalanced-lock-release","analyzedSha":"06dbeef4bb70fb718236aa418ed658ef4667a126","analyzedAt":"2026-08-27T02:36:57.214Z","schemaVersion":2},"datasetVersion":"2026-08-27T03:17:27.898Z"}