diff options
author | Andy McCurdy <andy@andymccurdy.com> | 2019-01-02 14:13:44 -0800 |
---|---|---|
committer | Andy McCurdy <andy@andymccurdy.com> | 2019-01-02 14:13:44 -0800 |
commit | a3cfded93afa2a65908f05ac251b18d77fa84dd2 (patch) | |
tree | 893d14fa7a6e39a24c446110c17dabf36b0be9d3 | |
parent | 1214b357badb2cfa2a85a462d8e35749785d0744 (diff) | |
download | redis-py-a3cfded93afa2a65908f05ac251b18d77fa84dd2.tar.gz |
Lock objects now support specifying token values and ownership checking
Lock.acquire() can now be provided a token. If provided, this value will be
used as the value stored in Redis to hold the lock.
Lock.owned() returns a boolean indicating whether the lock is owned by the
current instance.
-rw-r--r-- | CHANGES | 6 | ||||
-rw-r--r-- | redis/lock.py | 26 | ||||
-rw-r--r-- | tests/test_lock.py | 40 |
3 files changed, 70 insertions, 2 deletions
@@ -1,4 +1,10 @@ * 3.1.0 (in development) + * Added an owned method to Lock objects. owned returns a boolean + indicating whether the current lock instance still owns the lock. + Thanks Dave Johansen. #1112 + * Allow lock.acquire() to accept an optional token argument. If + provided, the token argument is used as the unique value used to claim + the lock. Thankd Dave Johansen. #1112 * Added a reacquire method to Lock objects. reaquire attempts to renew the lock such that the timeout is extended to the same value that the lock was initially acquired with. Thanks Ihor Kalnytskyi. #1014 diff --git a/redis/lock.py b/redis/lock.py index 2bb7794..8d481d7 100644 --- a/redis/lock.py +++ b/redis/lock.py @@ -149,7 +149,7 @@ class Lock(object): def __exit__(self, exc_type, exc_value, traceback): self.release() - def acquire(self, blocking=None, blocking_timeout=None): + def acquire(self, blocking=None, blocking_timeout=None, token=None): """ Use Redis to hold a shared, distributed lock named ``name``. Returns True once the lock is acquired. @@ -159,9 +159,18 @@ class Lock(object): ``blocking_timeout`` specifies the maximum number of seconds to wait trying to acquire the lock. + + ``token`` specifies the token value to be used. If provided, token + must be a bytes object or a string that can be encoded to a bytes + object with the default encoding. If a token isn't specified, a UUID + will be generated. """ sleep = self.sleep - token = uuid.uuid1().hex.encode() + if token is None: + token = uuid.uuid1().hex.encode() + else: + encoder = self.redis.connection_pool.get_encoder() + token = encoder.encode(token) if blocking is None: blocking = self.blocking if blocking_timeout is None: @@ -195,6 +204,19 @@ class Lock(object): """ return self.redis.get(self.name) is not None + def owned(self): + """ + Returns True if this key is locked by this lock, otherwise False. + """ + stored_token = self.redis.get(self.name) + # need to always compare bytes to bytes + # TODO: this can be simplified when the context manager is finished + if stored_token and not isinstance(stored_token, bytes): + encoder = self.redis.connection_pool.get_encoder() + stored_token = encoder.encode(stored_token) + return self.local.token is not None and \ + stored_token == self.local.token + def release(self): "Releases the already acquired lock" expected_token = self.local.token diff --git a/tests/test_lock.py b/tests/test_lock.py index 945fe2f..f92afec 100644 --- a/tests/test_lock.py +++ b/tests/test_lock.py @@ -2,10 +2,16 @@ import pytest import time from redis.exceptions import LockError, LockNotOwnedError +from redis.client import Redis from redis.lock import Lock +from .conftest import _get_client class TestLock(object): + @pytest.fixture() + def r_decoded(self, request): + return _get_client(Redis, request=request, decode_responses=True) + def get_lock(self, redis, *args, **kwargs): kwargs['lock_class'] = Lock return redis.lock(*args, **kwargs) @@ -18,6 +24,16 @@ class TestLock(object): lock.release() assert r.get('foo') is None + def test_lock_token(self, r): + lock = self.get_lock(r, 'foo') + assert lock.acquire(blocking=False, token='test') + assert r.get('foo') == b'test' + assert lock.local.token == b'test' + assert r.ttl('foo') == -1 + lock.release() + assert r.get('foo') is None + assert lock.local.token is None + def test_locked(self, r): lock = self.get_lock(r, 'foo') assert lock.locked() is False @@ -26,6 +42,30 @@ class TestLock(object): lock.release() assert lock.locked() is False + def _test_owned(self, client): + lock = self.get_lock(client, 'foo') + assert lock.owned() is False + lock.acquire(blocking=False) + assert lock.owned() is True + lock.release() + assert lock.owned() is False + + lock2 = self.get_lock(client, 'foo') + assert lock.owned() is False + assert lock2.owned() is False + lock2.acquire(blocking=False) + assert lock.owned() is False + assert lock2.owned() is True + lock2.release() + assert lock.owned() is False + assert lock2.owned() is False + + def test_owned(self, r): + self._test_owned(r) + + def test_owned_with_decoded_responses(self, r_decoded): + self._test_owned(r_decoded) + def test_competing_locks(self, r): lock1 = self.get_lock(r, 'foo') lock2 = self.get_lock(r, 'foo') |