Source code for trol.lock
import redis
import threading
[docs]class Lock:
"""Lock provides a Redis-backed distributed lock on a Model or Database instance.
Lock should be a class member of a Model class, and provides a unique lock to each instance of
that model. It is essentially a factory class for `redis-py's Lock objects`_
.. _redis-py's Lock objects: https://redis-py.readthedocs.io/en/latest/#redis.Redis.lock
Attributes:
timeout (float or None): Maximum time the lock can be held before it expires, releasing it automatically. When set to None, the lock never expired.
sleep (float): Time, in seconds, to sleep between each attempt to acquire the lock.
blocking_timeout (float or None): Maximum time to spend acquiring the lock.
lock_class (type or None): Class used to construct the lock. See `redis.Lock`_ for the canonical version.
thread_local (bool): Whether to use threadlocal storage when to store the reservation token.
.. _redis.Lock: https://github.com/andymccurdy/redis-py/blob/master/redis/lock.py
.. TODO:: Lock currently only works when bound to an object, and not directly from the Database
class. Lock should be refactored as a subclass of redis.lock.Lock to allow direct operations
(instead of being built during the ``__get__`` access) and/or Database should be refactored to no
longer rely on janky "class-binding".
>>> import trol
>>> import time
>>> from threading import Thread
>>> from redis import Redis
...
>>> class Sleepy(trol.Model):
... redis = Redis()
... sleepy_lock = trol.Lock()
... sleep = time.sleep
...
... def __init__(self, id):
... self.id = id
...
... def print(self, t, msg):
... with self.sleepy_lock:
... self.sleep(t)
... print(msg)
...
>>> sleepy = Sleepy('foo')
>>> Thread(target=sleepy.print, args=(3, "ok, go ahead")).start()
>>> Thread(target=sleepy.print, args=(1, "my turn!")).start()
>>> time.sleep(5)
ok, go ahead
my turn!
Just like with Property, the name of the lock is used to form it's Redis key and can either by
specified explicitly in the constructor or inferred from the attribute name in a Model.
>>> with sleepy.sleepy_lock:
... print(sleepy.redis.keys())
[b'Sleepy:foo:sleepy_lock']
>>> print(sleepy.redis.keys())
[]
"""
[docs] @staticmethod
def mangle(name):
"""Creates a mangled version of the given name.
Mangling produces a name unlikely to collide with other attribute names.
Args:
name (str): The name which should be mangled
Returns:
str: Mangled name
"""
return "_trol_lock_{}".format(name)
def __init__(self, name=None, timeout=None, sleep=0.1, blocking_timeout=None, lock_class=None, thread_local=True):
self.name = name
self.timeout = timeout
self.sleep = sleep
self.blocking_timeout = blocking_timeout
self.lock_class = lock_class
self.thread_local = thread_local
self._inception_lock = threading.Lock()
def __get__(self, obj, cls=None):
if obj is None:
return self
lock = getattr(obj, self.mangle(self.name), None)
# If the lock instance doesn't exist, build it.
if lock is None:
with self._inception_lock:
lock = getattr(obj, self.mangle(self.name), None)
if lock is None:
lock = self.build(obj)
return lock
@property
def name(self):
"""``str``: The name for this property, which will be used to determine the key when bound to a Model"""
return self._name
@name.setter
def name(self, value):
self._name = value
[docs] def key(self, obj):
"""Gets the key where this lock exists in Redis
The key for this lock is the key of it's holder, with ``:<name>`` appended
Args:
obj (object): Model instance for which locking is provided.
Returns:
str: The key which can be used to synchronize this lock data in Redis
"""
if obj.key is not None:
return "{}:{}".format(obj.key, self.name)
else:
return self.name
[docs] def build(self, obj):
"""Builds a lock instance and assigns it to a field in obj for later retrieval.
Args:
obj (object): Model instance for which locking is provided.
Returns:
object: Lock of type specified as lock_class. Probably redis.Lock.
"""
lock = obj.redis.lock(
name=self.key(obj),
timeout=self.timeout,
sleep=self.sleep,
blocking_timeout=self.blocking_timeout,
lock_class=self.lock_class,
thread_local=self.thread_local
)
setattr(obj, self.mangle(self.name), lock)
return lock