This was added in version 0.7.0 and the API is new. This means its open to breaking changes in future versions until the API is considered stable.
The implementations provided are NOT intented for consistency/synchronization purposes. If you need a locking mechanism focused on consistency, consider implementing your mechanism based on more serious tools like https://zookeeper.apache.org/.
There are a couple of locking implementations than can help you to protect against different scenarios:
RedLock(client: aiocache.base.BaseCache[~CacheKeyType][CacheKeyType], key: str, lease: Union[int, float])[source]¶
Implementation of Redlock with a single instance because aiocache is focused on single instance cache.
This locking has some limitations and shouldn’t be used in situations where consistency is critical. Those locks are aimed for performance reasons where failing on locking from time to time is acceptable. TLDR: do NOT use this if you need real resource exclusion.
Couple of considerations with the implementation:
- If the lease expires and there are calls waiting, all of them will pass (blocking just happens for the first time).
- When a new call arrives, it will wait always at most lease time. This means that the call could end up blocked longer than needed in case the lease from the blocker expires.
Backend specific implementation:
- Redis implements correctly the redlock algorithm. It sets the key if it doesn’t exist. To release, it checks the value is the same as the instance trying to release and if it is, it removes the lock. If not it will do nothing
- Memcached follows the same approach with a difference. Due to memcached lacking a way to execute the operation get and delete commands atomically, any client is able to release the lock. This is a limitation that can’t be fixed without introducing race conditions.
- Memory implementation is not distributed, it will only apply to the process running. Say you have 4 processes running APIs with aiocache, the locking will apply only per process (still useful to reduce load per process).
from aiocache import Cache from aiocache.lock import RedLock cache = Cache(Cache.REDIS) async with RedLock(cache, 'key', lease=1): # Calls will wait here result = await cache.get('key') if result is not None: return result result = await super_expensive_function() await cache.set('key', result)
In the example, first call will start computing the
super_expensive_functionwhile consecutive calls will block at most 1 second. If the blocking lasts for more than 1 second, the calls will proceed to also calculate the result of
OptimisticLock(client: aiocache.base.BaseCache[~CacheKeyType][CacheKeyType], key: str)[source]¶
Implementation of optimistic lock
Optimistic locking assumes multiple transactions can happen at the same time and they will only fail if before finish, conflicting modifications with other transactions are found, producing a roll back.
Finding a conflict will end up raising an aiocache.lock.OptimisticLockError exception. A conflict happens when the value at the storage is different from the one we retrieved when the lock started.
cache = Cache(Cache.REDIS) # The value stored in 'key' will be checked here async with OptimisticLock(cache, 'key') as lock: result = await super_expensive_call() await lock.cas(result)
If any other call sets the value of
lock.casis called, an
aiocache.lock.OptimisticLockErrorwill be raised. A way to make the same call crash would be to change the value inside the lock like:
cache = Cache(Cache.REDIS) # The value stored in 'key' will be checked here async with OptimisticLock(cache, 'key') as lock: result = await super_expensive_call() await cache.set('random_value') # This will make the `lock.cas` call fail await lock.cas(result)
If the lock is created with an unexisting key, there will never be conflicts.