{"id":"4be726eefc62b94b","repo":"redis/redis-py","slug":"cannot-reacquire-an-unlocked-lock","errorCode":null,"errorMessage":"Cannot reacquire an unlocked lock","messagePattern":"Cannot reacquire an unlocked lock","errorType":"exception","errorClass":"LockError","httpStatus":null,"severity":"warning","filePath":"redis/asyncio/lock.py","lineNumber":332,"sourceCode":"\n    async def do_extend(self, additional_time, replace_ttl) -> Literal[True]:\n        additional_time = int(additional_time * 1000)\n        if not bool(\n            await self.lua_extend(\n                keys=[self.name],\n                args=[self.local.token, additional_time, replace_ttl and \"1\" or \"0\"],\n                client=self.redis,\n            )\n        ):\n            raise LockNotOwnedError(\"Cannot extend a lock that's no longer owned\")\n        return True\n\n    def reacquire(self) -> Awaitable[Literal[True]]:\n        \"\"\"\n        Resets a TTL of an already acquired lock back to a timeout value.\n        \"\"\"\n        if self.local.token is None:\n            raise LockError(\"Cannot reacquire an unlocked lock\")\n        if self.timeout is None:\n            raise LockError(\"Cannot reacquire a lock with no timeout\")\n        return self.do_reacquire()\n\n    async def do_reacquire(self) -> Literal[True]:\n        timeout = int(self.timeout * 1000)\n        if not bool(\n            await self.lua_reacquire(\n                keys=[self.name], args=[self.local.token, timeout], client=self.redis\n            )\n        ):\n            raise LockNotOwnedError(\"Cannot reacquire a lock that's no longer owned\")\n        return True\n","sourceCodeStart":314,"sourceCodeEnd":346,"githubUrl":"https://github.com/redis/redis-py/blob/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/asyncio/lock.py#L314-L346","documentation":"Raised as LockError by Lock.reacquire() when self.local.token is None: reacquire() (reset TTL to the original timeout) requires the lock to currently be owned by this instance. Without a token there is nothing to reacquire.","triggerScenarios":"Calling lock.reacquire() before acquire() or after release(). Fires at lock.py:331-332.","commonSituations":"A keepalive loop calling reacquire() after the lock was released; reacquiring from a different task under thread_local=True; logic error calling reacquire without a prior successful acquire.","solutions":["Call reacquire() only after a successful acquire() and before release(), in the same task (or use thread_local=False).","Stop keepalive loops when the token is cleared.","Guard with owned() if the ownership state is uncertain."],"exampleFix":"// before\nlock = Lock(redis, 'k', timeout=30)\nlock.reacquire()  # before acquire -> LockError\n// after\nawait lock.acquire()\nlock.reacquire()","handlingStrategy":"validation","validationCode":"async def reacquire_if_owned(lock):\n    if lock.local.token is None:\n        return False\n    if lock.timeout is None:\n        return False\n    return await lock.reacquire()","typeGuard":"def lock_holds_token(lock) -> bool:\n    return getattr(getattr(lock, 'local', None), 'token', None) is not None","tryCatchPattern":"from redis.exceptions import LockError\ntry:\n    lock.reacquire()\nexcept LockError as e:\n    if 'unlocked' in str(e):\n        # reacquire after a fresh acquire() instead\n        ...","preventionTips":["Call reacquire() only after acquire() and before release().","Stop keepalive loops once the token is cleared.","Use owned() before reacquire if state is uncertain."],"tags":["lock","ownership","reacquire","asyncio"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}