Skip to content

Commit 6149004

Browse files
jakebarnwellchayimAndrew-Chen-Wangdvora-h
authored
Implement locks for RedisCluster (#2013)
* Add support for .lock() for RedisCluster * Update changelog with lua scripting and lock() changes * Also update asyncio client .lock() doc * Add Python 3.6 back to hash verify CI (#2008) * Renaming chore as maintenance (#2015) * Add AsyncFunctionCommands (#2009) * Also update asyncio client .lock() doc Co-authored-by: Chayim <[email protected]> Co-authored-by: Andrew Chen Wang <[email protected]> Co-authored-by: dvora-h <[email protected]>
1 parent 87764e7 commit 6149004

File tree

6 files changed

+88
-15
lines changed

6 files changed

+88
-15
lines changed

CHANGES

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
* Create codeql-analysis.yml (#1988). Thanks @chayim
1+
* Create codeql-analysis.yml (#1988). Thanks @chayim
2+
* Add limited support for Lua scripting with RedisCluster
3+
* Implement `.lock()` method on RedisCluster
24
* 4.1.3 (Feb 8, 2022)
3-
* Fix flushdb and flushall (#1926)
4-
* Add redis5 and redis4 dockers (#1871)
5-
* Change json.clear test multi to be up to date with redisjson (#1922)
6-
* Fixing volume for unstable_cluster docker (#1914)
7-
* Update changes file with changes since 4.0.0-beta2 (#1915)
5+
* Fix flushdb and flushall (#1926)
6+
* Add redis5 and redis4 dockers (#1871)
7+
* Change json.clear test multi to be up to date with redisjson (#1922)
8+
* Fixing volume for unstable_cluster docker (#1914)
9+
* Update changes file with changes since 4.0.0-beta2 (#1915)
810
* 4.1.2 (Jan 27, 2022)
911
* Invalid OCSP certificates should raise ConnectionError on failed validation (#1907)
1012
* Added retry mechanism on socket timeouts when connecting to the server (#1895)
@@ -94,10 +96,10 @@
9496
* Removing command on initial connections (#1722)
9597
* Removing hiredis warning when not installed (#1721)
9698
* 4.0.0 (Nov 15, 2021)
97-
* FT.EXPLAINCLI intentionally raising NotImplementedError
99+
* FT.EXPLAINCLI intentionally raising NotImplementedError
98100
* Restoring ZRANGE desc for Redis < 6.2.0 (#1697)
99101
* Response parsing occasionally fails to parse floats (#1692)
100-
* Re-enabling read-the-docs (#1707)
102+
* Re-enabling read-the-docs (#1707)
101103
* Call HSET after FT.CREATE to avoid keyspace scan (#1706)
102104
* Unit tests fixes for compatibility (#1703)
103105
* Improve documentation about Locks (#1701)
@@ -117,7 +119,7 @@
117119
* Sleep for flaky search test (#1680)
118120
* Test function renames, to match standards (#1679)
119121
* Docstring improvements for Redis class (#1675)
120-
* Fix georadius tests (#1672)
122+
* Fix georadius tests (#1672)
121123
* Improvements to JSON coverage (#1666)
122124
* Add python_requires setuptools check for python > 3.6 (#1656)
123125
* SMISMEMBER support (#1667)

redis/asyncio/client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,10 @@ def lock(
348348
continue trying forever. ``blocking_timeout`` can be specified as a
349349
float or integer, both representing the number of seconds to wait.
350350
351-
``lock_class`` forces the specified lock implementation.
351+
``lock_class`` forces the specified lock implementation. Note that as
352+
of redis-py 3.0, the only lock class we implement is ``Lock`` (which is
353+
a Lua-based lock). So, it's unlikely you'll need this parameter, unless
354+
you have created your own custom lock class.
352355
353356
``thread_local`` indicates whether the lock token is placed in
354357
thread-local storage. By default, the token is placed in thread local

redis/client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1082,7 +1082,10 @@ def lock(
10821082
continue trying forever. ``blocking_timeout`` can be specified as a
10831083
float or integer, both representing the number of seconds to wait.
10841084
1085-
``lock_class`` forces the specified lock implementation.
1085+
``lock_class`` forces the specified lock implementation. Note that as
1086+
of redis-py 3.0, the only lock class we implement is ``Lock`` (which is
1087+
a Lua-based lock). So, it's unlikely you'll need this parameter, unless
1088+
you have created your own custom lock class.
10861089
10871090
``thread_local`` indicates whether the lock token is placed in
10881091
thread-local storage. By default, the token is placed in thread local

redis/cluster.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
TimeoutError,
2929
TryAgainError,
3030
)
31+
from redis.lock import Lock
3132
from redis.utils import (
3233
dict_merge,
3334
list_keys_to_dict,
@@ -742,6 +743,72 @@ def pipeline(self, transaction=None, shard_hint=None):
742743
reinitialize_steps=self.reinitialize_steps,
743744
)
744745

746+
def lock(
747+
self,
748+
name,
749+
timeout=None,
750+
sleep=0.1,
751+
blocking_timeout=None,
752+
lock_class=None,
753+
thread_local=True,
754+
):
755+
"""
756+
Return a new Lock object using key ``name`` that mimics
757+
the behavior of threading.Lock.
758+
759+
If specified, ``timeout`` indicates a maximum life for the lock.
760+
By default, it will remain locked until release() is called.
761+
762+
``sleep`` indicates the amount of time to sleep per loop iteration
763+
when the lock is in blocking mode and another client is currently
764+
holding the lock.
765+
766+
``blocking_timeout`` indicates the maximum amount of time in seconds to
767+
spend trying to acquire the lock. A value of ``None`` indicates
768+
continue trying forever. ``blocking_timeout`` can be specified as a
769+
float or integer, both representing the number of seconds to wait.
770+
771+
``lock_class`` forces the specified lock implementation. Note that as
772+
of redis-py 3.0, the only lock class we implement is ``Lock`` (which is
773+
a Lua-based lock). So, it's unlikely you'll need this parameter, unless
774+
you have created your own custom lock class.
775+
776+
``thread_local`` indicates whether the lock token is placed in
777+
thread-local storage. By default, the token is placed in thread local
778+
storage so that a thread only sees its token, not a token set by
779+
another thread. Consider the following timeline:
780+
781+
time: 0, thread-1 acquires `my-lock`, with a timeout of 5 seconds.
782+
thread-1 sets the token to "abc"
783+
time: 1, thread-2 blocks trying to acquire `my-lock` using the
784+
Lock instance.
785+
time: 5, thread-1 has not yet completed. redis expires the lock
786+
key.
787+
time: 5, thread-2 acquired `my-lock` now that it's available.
788+
thread-2 sets the token to "xyz"
789+
time: 6, thread-1 finishes its work and calls release(). if the
790+
token is *not* stored in thread local storage, then
791+
thread-1 would see the token value as "xyz" and would be
792+
able to successfully release the thread-2's lock.
793+
794+
In some use cases it's necessary to disable thread local storage. For
795+
example, if you have code where one thread acquires a lock and passes
796+
that lock instance to a worker thread to release later. If thread
797+
local storage isn't disabled in this case, the worker thread won't see
798+
the token set by the thread that acquired the lock. Our assumption
799+
is that these cases aren't common and as such default to using
800+
thread local storage."""
801+
if lock_class is None:
802+
lock_class = Lock
803+
return lock_class(
804+
self,
805+
name,
806+
timeout=timeout,
807+
sleep=sleep,
808+
blocking_timeout=blocking_timeout,
809+
thread_local=thread_local,
810+
)
811+
745812
def _determine_nodes(self, *args, **kwargs):
746813
command = args[0]
747814
nodes_flag = kwargs.pop("nodes_flag", None)

redis/lock.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def acquire(self, blocking=None, blocking_timeout=None, token=None):
180180
if token is None:
181181
token = uuid.uuid1().hex.encode()
182182
else:
183-
encoder = self.redis.connection_pool.get_encoder()
183+
encoder = self.redis.get_encoder()
184184
token = encoder.encode(token)
185185
if blocking is None:
186186
blocking = self.blocking
@@ -224,7 +224,7 @@ def owned(self):
224224
# need to always compare bytes to bytes
225225
# TODO: this can be simplified when the context manager is finished
226226
if stored_token and not isinstance(stored_token, bytes):
227-
encoder = self.redis.connection_pool.get_encoder()
227+
encoder = self.redis.get_encoder()
228228
stored_token = encoder.encode(stored_token)
229229
return self.local.token is not None and stored_token == self.local.token
230230

tests/test_lock.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from .conftest import _get_client
1010

1111

12-
@pytest.mark.onlynoncluster
1312
class TestLock:
1413
@pytest.fixture()
1514
def r_decoded(self, request):
@@ -223,7 +222,6 @@ def test_reacquiring_lock_no_longer_owned_raises_error(self, r):
223222
lock.reacquire()
224223

225224

226-
@pytest.mark.onlynoncluster
227225
class TestLockClassSelection:
228226
def test_lock_class_argument(self, r):
229227
class MyLock:

0 commit comments

Comments
 (0)