{"record":{"id":"7e9305955ba91a10","repo":"redis/redis-py","slug":"cannot-release-a-lock-that-s-not-owned-or-is-alrea","errorCode":null,"errorMessage":"Cannot release a lock that's not owned or is already unlocked.","messagePattern":"Cannot release a lock that's not owned or is already unlocked\\.","errorType":"exception","errorClass":"LockError","httpStatus":null,"severity":"error","filePath":"redis/asyncio/lock.py","lineNumber":275,"sourceCode":"        if stored_token and not isinstance(stored_token, bytes):\n            try:\n                encoder = self.redis.connection_pool.get_encoder()\n            except AttributeError:\n                # Cluster\n                encoder = self.redis.get_encoder()\n            stored_token = encoder.encode(stored_token)\n        return self.local.token is not None and stored_token == self.local.token\n\n    async def release(self) -> None:\n        \"\"\"Releases the already acquired lock.\n\n        The token is only cleared after the Redis release operation completes\n        successfully. This ensures that if the release is cancelled mid-operation,\n        the lock state remains consistent and can be retried.\n        \"\"\"\n        expected_token = self.local.token\n        if expected_token is None:\n            raise LockError(\n                \"Cannot release a lock that's not owned or is already unlocked.\",\n                lock_name=self.name,\n            )\n        try:\n            await self.do_release(expected_token)\n        except LockNotOwnedError:\n            # Lock doesn't exist in Redis, safe to clear token\n            self.local.token = None\n            raise\n        # Only clear token after successful release\n        self.local.token = None\n\n    async def do_release(self, expected_token: bytes) -> None:\n        if not bool(\n            await self.lua_release(\n                keys=[self.name], args=[expected_token], client=self.redis\n            )\n        ):","sourceCodeStart":257,"sourceCodeEnd":293,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/asyncio/lock.py#L257-L293","documentation":"release() reads the local token; if it is None the lock was never acquired on this instance (or was already released in this task), so the client refuses to run the Lua release and raises LockError. With thread_local=True (default) the token is stored per-thread, so releasing from a different thread/task than the one that acquired looks like 'not owned'.","triggerScenarios":"Calling release() before acquire(); calling release() twice; releasing from a different thread/task than acquired (thread_local=True); context-manager exit after a failed/contended acquisition.","commonSituations":"Cross-thread or cross-task lock handoff without thread_local=False; double release in finally blocks; reusing a Lock object and releasing before acquiring.","solutions":["Only release after a successful acquire; guard with 'if await lock.owned()'.","For cross-thread/task handoff, construct the Lock with thread_local=False.","In context-manager usage set raise_on_release_error=False to suppress release errors on exit.","Avoid calling release() twice."],"exampleFix":"# before\nlock = Lock(r, 'resource')  # thread_local=True default\n# acquired in task A, released in task B -> LockError\n# after\nlock = Lock(r, 'resource', thread_local=False)\n# token visible across tasks","handlingStrategy":"try-catch","validationCode":"if await lock.owned():\n    await lock.release()","typeGuard":null,"tryCatchPattern":"from redis.exceptions import LockError\ntry:\n    await lock.release()\nexcept LockError:\n    # never owned on this task: safe to ignore or log\n    ...","preventionTips":["Use thread_local=False for locks handed off across threads/tasks.","Guard release/extend/reacquire with 'await lock.owned()'.","Set raise_on_release_error=False in context-manager usage where appropriate."],"tags":["lock","distributed-lock","ownership","async"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}