{"id":"d27f31b0ce580e68","repo":"redis/redis-py","slug":"cannot-extend-a-lock-that-s-no-longer-owned","errorCode":null,"errorMessage":"Cannot extend a lock that's no longer owned","messagePattern":"Cannot extend a lock that's no longer owned","errorType":"exception","errorClass":"LockNotOwnedError","httpStatus":null,"severity":"warning","filePath":"redis/asyncio/lock.py","lineNumber":324,"sourceCode":"        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        \"\"\"\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            )","sourceCodeStart":306,"sourceCodeEnd":342,"githubUrl":"https://github.com/redis/redis-py/blob/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/asyncio/lock.py#L306-L342","documentation":"Raised as LockNotOwnedError (subclass of LockError) by Lock.do_extend() when the Lua extend script returns 0: the key is missing, the stored token does not match this lock's token, or the key's TTL is negative (already expired). The extend is skipped to avoid touching a lock this instance no longer owns.","triggerScenarios":"The lock TTL expired before extend() was called; another owner re-acquired after expiry; token mismatch (thread_local confusion); key deleted out-of-band. Fires at lock.py:323-324.","commonSituations":"A watchdog extending too late (after expiry); critical section overran the TTL; ownership confusion across tasks.","solutions":["Extend early and often, well before the TTL expires (e.g., extend at TTL/3 intervals).","Use a longer base timeout as a safety margin.","Catch LockNotOwnedError in the watchdog and treat it as 'lock lost' (abort the critical section).","Ensure single-owner semantics (thread_local setting matches your task model)."],"exampleFix":"// before\nlock = Lock(redis, 'k', timeout=5)\nawait lock.acquire()\nawait asyncio.sleep(6)  # TTL expired\nlock.extend(5)  # LockNotOwnedError\n// after\nlock = Lock(redis, 'k', timeout=10)\nawait lock.acquire()\nfor _ in range(work_units):\n    lock.extend(10)  # renew before expiry\n    await step()","handlingStrategy":"try-catch","validationCode":"async def safe_extend(lock, extra):\n    if not await lock.owned():\n        return False  # lock lost; do not attempt extend\n    try:\n        return await lock.extend(extra)\n    except Exception:\n        return False","typeGuard":"async def lock_still_owned(lock) -> bool:\n    return await lock.owned()","tryCatchPattern":"from redis.exceptions import LockNotOwnedError\ntry:\n    lock.extend(10)\nexcept LockNotOwnedError:\n    # TTL expired / lost; abort the critical section\n    raise CriticalSectionAborted","preventionTips":["Extend well before the TTL expires (e.g., at TTL/3).","Use a longer base timeout as a safety margin.","Catch LockNotOwnedError to detect a lost lock.","Keep single-owner semantics consistent with thread_local."],"tags":["lock","ttl","extend","ownership","asyncio"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}