Skip to content

Commit a3cfded

Browse files
committed
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.
1 parent 1214b35 commit a3cfded

File tree

3 files changed

+70
-2
lines changed

3 files changed

+70
-2
lines changed

CHANGES

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
* 3.1.0 (in development)
2+
* Added an owned method to Lock objects. owned returns a boolean
3+
indicating whether the current lock instance still owns the lock.
4+
Thanks Dave Johansen. #1112
5+
* Allow lock.acquire() to accept an optional token argument. If
6+
provided, the token argument is used as the unique value used to claim
7+
the lock. Thankd Dave Johansen. #1112
28
* Added a reacquire method to Lock objects. reaquire attempts to renew
39
the lock such that the timeout is extended to the same value that the
410
lock was initially acquired with. Thanks Ihor Kalnytskyi. #1014

redis/lock.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def __enter__(self):
149149
def __exit__(self, exc_type, exc_value, traceback):
150150
self.release()
151151

152-
def acquire(self, blocking=None, blocking_timeout=None):
152+
def acquire(self, blocking=None, blocking_timeout=None, token=None):
153153
"""
154154
Use Redis to hold a shared, distributed lock named ``name``.
155155
Returns True once the lock is acquired.
@@ -159,9 +159,18 @@ def acquire(self, blocking=None, blocking_timeout=None):
159159
160160
``blocking_timeout`` specifies the maximum number of seconds to
161161
wait trying to acquire the lock.
162+
163+
``token`` specifies the token value to be used. If provided, token
164+
must be a bytes object or a string that can be encoded to a bytes
165+
object with the default encoding. If a token isn't specified, a UUID
166+
will be generated.
162167
"""
163168
sleep = self.sleep
164-
token = uuid.uuid1().hex.encode()
169+
if token is None:
170+
token = uuid.uuid1().hex.encode()
171+
else:
172+
encoder = self.redis.connection_pool.get_encoder()
173+
token = encoder.encode(token)
165174
if blocking is None:
166175
blocking = self.blocking
167176
if blocking_timeout is None:
@@ -195,6 +204,19 @@ def locked(self):
195204
"""
196205
return self.redis.get(self.name) is not None
197206

207+
def owned(self):
208+
"""
209+
Returns True if this key is locked by this lock, otherwise False.
210+
"""
211+
stored_token = self.redis.get(self.name)
212+
# need to always compare bytes to bytes
213+
# TODO: this can be simplified when the context manager is finished
214+
if stored_token and not isinstance(stored_token, bytes):
215+
encoder = self.redis.connection_pool.get_encoder()
216+
stored_token = encoder.encode(stored_token)
217+
return self.local.token is not None and \
218+
stored_token == self.local.token
219+
198220
def release(self):
199221
"Releases the already acquired lock"
200222
expected_token = self.local.token

tests/test_lock.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@
22
import time
33

44
from redis.exceptions import LockError, LockNotOwnedError
5+
from redis.client import Redis
56
from redis.lock import Lock
7+
from .conftest import _get_client
68

79

810
class TestLock(object):
11+
@pytest.fixture()
12+
def r_decoded(self, request):
13+
return _get_client(Redis, request=request, decode_responses=True)
14+
915
def get_lock(self, redis, *args, **kwargs):
1016
kwargs['lock_class'] = Lock
1117
return redis.lock(*args, **kwargs)
@@ -18,6 +24,16 @@ def test_lock(self, r):
1824
lock.release()
1925
assert r.get('foo') is None
2026

27+
def test_lock_token(self, r):
28+
lock = self.get_lock(r, 'foo')
29+
assert lock.acquire(blocking=False, token='test')
30+
assert r.get('foo') == b'test'
31+
assert lock.local.token == b'test'
32+
assert r.ttl('foo') == -1
33+
lock.release()
34+
assert r.get('foo') is None
35+
assert lock.local.token is None
36+
2137
def test_locked(self, r):
2238
lock = self.get_lock(r, 'foo')
2339
assert lock.locked() is False
@@ -26,6 +42,30 @@ def test_locked(self, r):
2642
lock.release()
2743
assert lock.locked() is False
2844

45+
def _test_owned(self, client):
46+
lock = self.get_lock(client, 'foo')
47+
assert lock.owned() is False
48+
lock.acquire(blocking=False)
49+
assert lock.owned() is True
50+
lock.release()
51+
assert lock.owned() is False
52+
53+
lock2 = self.get_lock(client, 'foo')
54+
assert lock.owned() is False
55+
assert lock2.owned() is False
56+
lock2.acquire(blocking=False)
57+
assert lock.owned() is False
58+
assert lock2.owned() is True
59+
lock2.release()
60+
assert lock.owned() is False
61+
assert lock2.owned() is False
62+
63+
def test_owned(self, r):
64+
self._test_owned(r)
65+
66+
def test_owned_with_decoded_responses(self, r_decoded):
67+
self._test_owned(r_decoded)
68+
2969
def test_competing_locks(self, r):
3070
lock1 = self.get_lock(r, 'foo')
3171
lock2 = self.get_lock(r, 'foo')

0 commit comments

Comments
 (0)