{"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":"warning","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/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/asyncio/lock.py#L292-L328","documentation":"Raised as LockError by Lock.extend() when self.local.token is None: there is no ownership token to extend. extend() only makes sense on a lock that has been acquired; without a token the Lua extend script cannot run.","triggerScenarios":"Calling lock.extend(...) before acquire() succeeded, or after release() has already cleared the token. Fires at lock.py:309-310.","commonSituations":"Extending in a watchdog/keepalive loop that starts before acquisition completes; extending after an early release; extending from a different task under thread_local=True.","solutions":["Only call extend() between a successful acquire() and release(), within the same task (or use thread_local=False).","Guard the watchdog loop to stop extending once the token is cleared/released.","Use owned() to check ownership before extending if unsure."],"exampleFix":"// before\nlock = Lock(redis, 'k', timeout=10)\nlock.extend(10)  # before acquire -> LockError\n// after\nawait lock.acquire()\nlock.extend(10)\n...","handlingStrategy":"validation","validationCode":"def can_extend(lock) -> bool:\n    return lock.local.token is not None and lock.timeout is not None\n\nif can_extend(lock):\n    lock.extend(10)","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.extend(10)\nexcept LockError as e:\n    if 'unlocked' in str(e):\n        # not acquired (or already released); skip extend\n        ...","preventionTips":["Only extend() between acquire() and release() in the same task.","Stop watchdog loops when the token is cleared.","Use owned() before extending if state is uncertain."],"tags":["lock","ownership","extend","asyncio"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}