djblets.protect.locks¶
Locking implementations.
New in version 6.0.
- class CacheLock(key: str | Sequence[str] = '', *, blocking: bool = True, lock_expiration_secs: int = 30, retry_secs: float = 0.25, timeout_secs: float = -1)[source]¶
Bases:
objectA distributed lock backed by a cache.
This is used to acquire a lock associated with a key, either blocking or immediately returning if a lock can’t immediately be acquired.
Locks can be used by code that needs to run only once across multiple processes or servers. They can be used directly or passed to
cache_memoize()orcache_memoize_iter()to wrap any cache updates in a lock.Locks have an expiration time in order to prevent deadlocks. Setting this higher can result in longer wait periods if the lock fails to be released. Setting it lower can result in locks automatically releasing prematurely. It defaults to an expiration of 30 seconds.
The interface is kept compatible with
threading.Lock, but with additional capabilities for updating expiration times and setting default blocking and timeout behavior during construction.Note
This lock is subject to the limitations of the cache system. The lock may be purged from cache without notice, and it’s possible for one client to overwrite another’s lock depending on timing issues or scaling setup.
For these reasons, this lock should be considered an imperfect, lossy lock. Callers can use it to help prevent multiple operations from occurring at once, but they should be tolerant of that possibility and designed accordingly.
CacheLocks are also not thread-safe. Do not reuse the same lock across threads.
New in version 6.0.
- __init__(key: str | Sequence[str] = '', *, blocking: bool = True, lock_expiration_secs: int = 30, retry_secs: float = 0.25, timeout_secs: float = -1) None[source]¶
Initialize the lock.
- Parameters:
The key to use in the cache.
This may be a sequence of strings, which will take care of serializing each component to help avoid key injection attacks.
This will be passed to
make_cache_key()to construct a full cache key.blocking (
bool, optional) – Whether this lock will block for a period of time to be acquired.lock_expiration_secs (
int, optional) –The max amount of time a lock can be claimed.
After this period, the lock will be automatically released.
retry_secs (
float, optional) –The time to sleep between checking for a lock to be released.
The caller should set this to be less than the timeout, but note that timeouts can be extended or reduced by the lock owner.
An additional jitter between 0-25% of the retry time will be added to reduce stampede issues.
timeout_secs (
float, optional) –The max time to wait for a lock to be released.
If -1, the lock will block indefinitely.
- Raises:
ValueError – A provided argument had an invalid value.
- timeout_secs: float¶
The max time to wait for a lock to be released.
If -1, the lock will wait indefinitely.
- __annotations__ = {'_lock_expires_time': 'float', 'acquired': 'bool', 'blocking': 'bool', 'full_cache_key': 'str', 'lock_expiration_secs': 'int', 'retry_secs': 'float', 'timeout_secs': 'float', 'token': 'str'}¶
- full_cache_key: str¶
The full cache key used for the lock.
This may be set lazily after the lock is constructed, but must be set prior to acquiring a lock.
- __del__() None[source]¶
Handle destruction of the cache lock.
If this lock was garbage collected without being released, an exception will be logged indicating an implementation problem with the caller’s use of the lock.
- locked() bool[source]¶
Return whether the lock is acquired.
This wraps
acquired, and provides API compatibility withthreading.Lockand other Python lock objects.- Returns:
Trueif the lock has been acquired.Falseif it has not.- Return type:
- acquire(blocking: bool | None = None, timeout: float | None = None) bool[source]¶
Acquire a lock.
If there’s already an existing lock in cache, this will either return immediately or wait for the lock to be released, depending on
blocking.If waiting, this will wait for a total time specified by
timeout_secs, checking everyretry_secs.Waiting uses the monotonic clock, so it’s not affected by changes to the system clock.
To ensure API compatibility with
threading.Lockand other Python lock objects, this method can also take arguments that override the values provided during construction.- Returns:
Trueif the lock could be acquired (even after waiting).Falseif it could not (only ifblockingisFalse).- Return type:
- Raises:
RuntimeError – An attempt was made to acquire a lock that was already acquired.
TimeoutError – The lock could not be acquired due to the wait time expiring.
ValueError – A cache key was never set for the lock.
- update_expiration(lock_expiration_secs: int | None = None) None[source]¶
Update the expiration of the lock.
This can be used to keep the lock opened a bit longer, in case there’s work in progress, or to shorten the lock.
- Parameters:
lock_expiration_secs (
int, optional) –A specific number of seconds to set for the new expiration.
If not provided, the original expiration time in seconds will be used.
- Raises:
AssertionError – The caller called this without first acquiring a lock.
- release() None[source]¶
Release a lock.
If the lock is still valid, it will be removed from the cache, allowing something else to acquire the lock.
If a lock is not acquired, this will raise an exception.
- Raises:
RuntimeError – This was called on a lock that was not acquired.
- __enter__() Self[source]¶
Enter the context manager.
This will acquire the lock, if possible, and pass the result as the context. Once the context manager is exited, the lock will be released.
This is equivalent to calling
acquire().- Context:
CacheLock– The cache lock instance.- Raises:
TimeoutError – The lock could not be acquired due to the wait time expiring.
ValueError – A cache key was never set for the lock.