{"record":{"id":"d05996e02bb11c7e","repo":"redis/redis-py","slug":"cannot-extend-an-unlocked-lock-d05996","errorCode":null,"errorMessage":"Cannot extend an unlocked lock","messagePattern":"Cannot extend an unlocked lock","errorType":"exception","errorClass":"LockError","httpStatus":null,"severity":"error","filePath":"redis/lock.py","lineNumber":301,"sourceCode":"                \"Cannot release a lock that's no longer owned\",\n                lock_name=self.name,\n            )\n\n    def extend(\n        self, additional_time: Number, replace_ttl: bool = False\n    ) -> 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\", lock_name=self.name)\n        if self.timeout is None:\n            raise LockError(\"Cannot extend a lock with no timeout\", lock_name=self.name)\n        return self.do_extend(additional_time, replace_ttl)\n\n    def do_extend(self, additional_time: Number, replace_ttl: bool) -> Literal[True]:\n        additional_time = int(additional_time * 1000)\n        if not bool(\n            self.lua_extend(\n                keys=[self.name],\n                args=[self.local.token, additional_time, \"1\" if replace_ttl else \"0\"],\n                client=self.redis,\n            )\n        ):\n            raise LockNotOwnedError(\n                \"Cannot extend a lock that's no longer owned\",\n                lock_name=self.name,\n            )\n        return True","sourceCodeStart":283,"sourceCodeEnd":319,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/lock.py#L283-L319","documentation":"Raised by Lock.extend() when self.local.token is None, meaning the current thread has no recorded ownership token for this lock. The token is set only on a successful acquire() and cleared on release(), so extend() refuses to run before the lock is held or after it has been released. This is a client-side precondition check (LockError) that fires before any Redis round-trip.","triggerScenarios":"Calling lock.extend(additional_time) before lock.acquire() has returned True; calling extend() after lock.release() (which sets self.local.token = None at lock.py:275); calling extend() from a worker thread different from the acquiring thread when thread_local=True (default), because the token lives in thread-local storage and is invisible to other threads.","commonSituations":"Forgetting to acquire before extending; an earlier acquire() that returned False (non-blocking) being silently ignored; extending inside an except/finally block that already released the lock; passing a Lock instance across threads without thread_local=False.","solutions":["Ensure lock.acquire() returns True before calling extend(); for non-blocking acquire, check the boolean return value.","Use the context manager form (with lock:) so acquire/release are balanced automatically.","Guard with lock.owned() before extending, and re-acquire if it returns False.","If extending from a different thread than the one that acquired, construct the Lock with thread_local=False."],"exampleFix":"# before\nlock = client.lock('mylock', timeout=10)\nlock.extend(5)\n\n# after\nlock = client.lock('mylock', timeout=10)\nif lock.acquire():\n    try:\n        lock.extend(5)\n    finally:\n        lock.release()","handlingStrategy":"validation","validationCode":"# Validate ownership before extending\nif lock.local.token is None or not lock.owned():\n    raise RuntimeError('cannot extend: lock not held by this thread')\nlock.extend(additional_time)","typeGuard":"from redis.lock import Lock\n\ndef is_held(lock: Lock) -> bool:\n    \"\"\"True only when the current thread holds the lock server-side.\"\"\"\n    return lock.local.token is not None and lock.owned()","tryCatchPattern":"from redis.exceptions import LockError\n\ntry:\n    lock.extend(additional_time)\nexcept LockError as e:\n    if 'unlocked' in str(e):\n        # not held locally — re-acquire before retrying\n        if lock.acquire(blocking=True, blocking_timeout=5):\n            lock.extend(additional_time)\n    else:\n        raise","preventionTips":["Always pair acquire()/release() — prefer the 'with lock:' context manager.","Check the boolean return of non-blocking acquire() before extend().","Keep extend() calls on the same thread that called acquire(), or set thread_local=False."],"tags":["lock","distributed-lock","precondition","thread-local"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}