API Reference for lock()¶
Overview¶
The lock module provides a mechanism for managing locking within synchronous and asynchronous contexts.
The main class, lock, combines both synchronous and asynchronous locking operations and relies on an underlying cache client that supports atomic "set-if-not-exists" (nx) semantics for correct distributed locking behavior.
There are two main ways to use locking with py-cachify:
- Via the global
lockfactory exported frompy_cachify, which relies on a globally initialized client. - Via instance-based locking obtained from a
Cachifyobject created byinit_cachify(is_global=False).
Class: lock¶
Description¶
The lock class manages locks using a specified key, with options for waiting and expiration.
It can be used in both synchronous and asynchronous contexts.
Parameters¶
| Parameter | Type | Description |
|---|---|---|
key |
str |
The key used to identify the lock. |
nowait |
bool, optional |
If True, do not wait for the lock to be released and raise immediately. Defaults to True. |
timeout |
Union[int, float], optional |
Time in seconds to wait for the lock if nowait is False. Defaults to None. |
exp |
Union[int, None], optional |
Expiration time for the lock. Defaults to UNSET and falls back to the global setting in cachify. |
Methods¶
-
__enter__() -> Self- Acquire a lock for the specified key as a context manager, synchronous.
-
release() -> None- Release the lock that is currently being held, synchronous.
-
is_locked() -> bool- Check if the lock is currently held, synchronous.
-
__aenter__() -> Self- Async version of
__enter__to acquire a lock as an async context manager.
- Async version of
-
arelease() -> None- Release the lock that is currently held, asynchronously.
-
is_alocked() -> bool- Check if the lock is currently held asynchronously.
-
as a
decorator- Decorator to acquire a lock for the wrapped function on call, for both synchronous and asynchronous functions.
- Attaches the following methods to the wrapped function:
is_locked(*args, **kwargs): Check if the function is currently locked.release(*args, **kwargs): Release the lock associated with the function.
Error Handling¶
CachifyLockError: Raised when an operation on a lock is invalid or a lock cannot be acquired.
Backend Requirements and nx Semantics¶
The correctness of lock (and decorators built on top of it) depends on the underlying cache client providing an atomic "set-if-not-exists" operation via an nx flag:
- When
nx=False, asetcall should behave like a normal upsert and overwrite existing values. - When
nx=True, asetcall must atomically set the value only if the key does not already exist, and return a truthy indication on success and a falsy indication otherwise.
Built-in clients implement this behavior and use it to acquire and release locks safely. Custom clients should follow the same contract as documented in the initialization reference to ensure that locks behave correctly in concurrent and distributed scenarios.
Lock Polling and nowait=False¶
When you create a lock with nowait=False, the library uses a polling mechanism to repeatedly attempt lock acquisition until it succeeds or the timeout is reached:
- The lock checks availability at intervals specified by
lock_poll_interval(configured ininit_cachify()) - Default interval:
0.1seconds (100ms) - Between attempts, the lock sleeps to avoid busy-waiting and reduce load on the cache backend
- Once the lock is acquired or the timeout expires, polling stops
You can adjust lock_poll_interval when initializing to trade off between responsiveness and backend load:
from py_cachify import init_cachify, lock
# Use a longer polling interval to reduce Redis load
init_cachify(lock_poll_interval=0.5)
# This lock will check every 500ms when waiting
@lock(key='heavy-operation', nowait=False, timeout=30)
def process_large_dataset():
...
For more details, see the initialization reference.
Usage Example¶
from py_cachify import lock
@lock('my_lock_key-{arg}', nowait=True)
def my_function(arg: str) -> None:
# Critical section of code goes here
pass
with lock('my_lock_key'):
# Critical section of code goes here
pass
async with lock('my_async_lock_key'):
# Critical section of async code goes here
pass
By using the lock class, you'll ensure that your function calls are properly synchronized, preventing race conditions in shared resources.
Instance-based usage¶
If you need multiple, independent locking backends (for example, per module or subsystem), you can create dedicated Cachify instances via init_cachify(is_global=False) and use their lock method instead of the global factory:
from py_cachify import init_cachify
# Create a dedicated instance that does not affect the global client
local_cachify = init_cachify(is_global=False, prefix='LOCAL-')
local_lock = local_cachify.lock(key='local-lock-{name}')
with local_lock:
# Critical section protected by the local instance
...
- Global
lock(...)uses the client configured by a globalinit_cachify()call. local_cachify.lock(...)uses a client that is completely independent from the global one.
Releasing the Lock or checking whether it's locked or not¶
my_function.is_locked(arg='arg-value') # returns bool
my_function.release(arg='arg-value') # forcefully releases the lock
Note¶
- If py-cachify is not initialized through
init_cachifywithis_global=True, using the globallockfactory or decorators will raise aCachifyInitError. Cachifyinstances created withis_global=Falsedo not depend on global initialization and can be used independently.
Type Hints Remark (Decorator only application)¶
Currently, Python's type hints have limitations in fully capturing a function's
original signature when transitioning to a protocol-based callable in a decorator,
particularly for methods (i.e., those that include self).
ParamSpec can effectively handle argument and keyword types for functions
but doesn't translate well to methods within protocols like WrappedFunctionLock.
I'm staying updated on this issue and recommend checking the following resources
for more insights into ongoing discussions and proposed solutions:
- Typeshed Pull Request #11662
- Mypy Pull Request #17123
- Python Discussion on Allowing Self-Binding for Generic ParamSpec
Once any developments occur, I will quickly update the source code to incorporate the changes.