{"record":{"id":"bf1ad8843f9471e2","repo":"redis/redis-py","slug":"cannot-extend-an-unlocked-lock","errorCode":null,"errorMessage":"Cannot extend an unlocked lock","messagePattern":"Cannot extend an unlocked lock","errorType":"exception","errorClass":"LockError","httpStatus":null,"severity":"error","filePath":"redis/asyncio/lock.py","lineNumber":310,"sourceCode":"            )\n        ):\n            raise LockNotOwnedError(\"Cannot release a lock that's no longer owned\")\n\n    def extend(\n        self, additional_time: Number, replace_ttl: bool = False\n    ) -> Awaitable[Literal[True]]:\n        \"\"\"\n        Adds more time to an already acquired lock.\n\n        ``additional_time`` can be specified as an integer or a float, both\n        representing the number of seconds to add.\n\n        ``replace_ttl`` if False (the default), add `additional_time` to\n        the lock's existing ttl. If True, replace the lock's ttl with\n        `additional_time`.\n        \"\"\"\n        if self.local.token is None:\n            raise LockError(\"Cannot extend an unlocked lock\")\n        if self.timeout is None:\n            raise LockError(\"Cannot extend a lock with no timeout\")\n        return self.do_extend(additional_time, replace_ttl)\n\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        \"\"\"","sourceCodeStart":292,"sourceCodeEnd":328,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/asyncio/lock.py#L292-L328","documentation":"extend() first checks the local token; if it is None the lock was never acquired (or already released) on this instance, so the client refuses to run the Lua extend and raises LockError before touching Redis.","triggerScenarios":"Calling extend(...) before acquire(), after release(), or from a different thread/task than the acquirer when thread_local=True.","commonSituations":"A renewal task started before acquisition; extending after release; cross-task token invisibility with the default thread_local=True.","solutions":["Acquire before extend; guard with 'if await lock.owned()'.","Use thread_local=False for cross-task renewal so the token is shared.","Do not extend after release."],"exampleFix":"# before\nawait lock.extend(10)  # before any acquire -> LockError\n# after\nif await lock.owned():\n    await lock.extend(10)","handlingStrategy":"validation","validationCode":"if await lock.owned():\n    await lock.extend(additional_time)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only extend after a successful acquire on the same task.","Use thread_local=False when a separate task renews the lock."],"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"}