diff options
author | Jake Barnwell <2320567+jakebarnwell@users.noreply.github.com> | 2022-03-01 05:22:49 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-01 12:22:49 +0200 |
commit | 61490045fc1a3d34260b293821a57424436af29e (patch) | |
tree | 410f3a694d76f987d5e9b6fe240b750a0b01638a | |
parent | 87764e7eed05d19519110d06c66a4c00ea59bcac (diff) | |
download | redis-py-61490045fc1a3d34260b293821a57424436af29e.tar.gz |
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 <chayim@users.noreply.github.com>
Co-authored-by: Andrew Chen Wang <60190294+Andrew-Chen-Wang@users.noreply.github.com>
Co-authored-by: dvora-h <dvora.heller@redis.com>
-rw-r--r-- | CHANGES | 20 | ||||
-rw-r--r-- | redis/asyncio/client.py | 5 | ||||
-rwxr-xr-x | redis/client.py | 5 | ||||
-rw-r--r-- | redis/cluster.py | 67 | ||||
-rw-r--r-- | redis/lock.py | 4 | ||||
-rw-r--r-- | tests/test_lock.py | 2 |
6 files changed, 88 insertions, 15 deletions
@@ -1,10 +1,12 @@ - * Create codeql-analysis.yml (#1988). Thanks @chayim + * Create codeql-analysis.yml (#1988). Thanks @chayim + * Add limited support for Lua scripting with RedisCluster + * Implement `.lock()` method on RedisCluster * 4.1.3 (Feb 8, 2022) - * Fix flushdb and flushall (#1926) - * Add redis5 and redis4 dockers (#1871) - * Change json.clear test multi to be up to date with redisjson (#1922) - * Fixing volume for unstable_cluster docker (#1914) - * Update changes file with changes since 4.0.0-beta2 (#1915) + * Fix flushdb and flushall (#1926) + * Add redis5 and redis4 dockers (#1871) + * Change json.clear test multi to be up to date with redisjson (#1922) + * Fixing volume for unstable_cluster docker (#1914) + * Update changes file with changes since 4.0.0-beta2 (#1915) * 4.1.2 (Jan 27, 2022) * Invalid OCSP certificates should raise ConnectionError on failed validation (#1907) * Added retry mechanism on socket timeouts when connecting to the server (#1895) @@ -94,10 +96,10 @@ * Removing command on initial connections (#1722) * Removing hiredis warning when not installed (#1721) * 4.0.0 (Nov 15, 2021) - * FT.EXPLAINCLI intentionally raising NotImplementedError + * FT.EXPLAINCLI intentionally raising NotImplementedError * Restoring ZRANGE desc for Redis < 6.2.0 (#1697) * Response parsing occasionally fails to parse floats (#1692) - * Re-enabling read-the-docs (#1707) + * Re-enabling read-the-docs (#1707) * Call HSET after FT.CREATE to avoid keyspace scan (#1706) * Unit tests fixes for compatibility (#1703) * Improve documentation about Locks (#1701) @@ -117,7 +119,7 @@ * Sleep for flaky search test (#1680) * Test function renames, to match standards (#1679) * Docstring improvements for Redis class (#1675) - * Fix georadius tests (#1672) + * Fix georadius tests (#1672) * Improvements to JSON coverage (#1666) * Add python_requires setuptools check for python > 3.6 (#1656) * SMISMEMBER support (#1667) diff --git a/redis/asyncio/client.py b/redis/asyncio/client.py index 619592e..2afad0f 100644 --- a/redis/asyncio/client.py +++ b/redis/asyncio/client.py @@ -348,7 +348,10 @@ class Redis( continue trying forever. ``blocking_timeout`` can be specified as a float or integer, both representing the number of seconds to wait. - ``lock_class`` forces the specified lock implementation. + ``lock_class`` forces the specified lock implementation. Note that as + of redis-py 3.0, the only lock class we implement is ``Lock`` (which is + a Lua-based lock). So, it's unlikely you'll need this parameter, unless + you have created your own custom lock class. ``thread_local`` indicates whether the lock token is placed in thread-local storage. By default, the token is placed in thread local diff --git a/redis/client.py b/redis/client.py index 0eade79..b12ad57 100755 --- a/redis/client.py +++ b/redis/client.py @@ -1082,7 +1082,10 @@ class Redis(AbstractRedis, RedisModuleCommands, CoreCommands, SentinelCommands): continue trying forever. ``blocking_timeout`` can be specified as a float or integer, both representing the number of seconds to wait. - ``lock_class`` forces the specified lock implementation. + ``lock_class`` forces the specified lock implementation. Note that as + of redis-py 3.0, the only lock class we implement is ``Lock`` (which is + a Lua-based lock). So, it's unlikely you'll need this parameter, unless + you have created your own custom lock class. ``thread_local`` indicates whether the lock token is placed in thread-local storage. By default, the token is placed in thread local diff --git a/redis/cluster.py b/redis/cluster.py index b8d6b19..3b30a6e 100644 --- a/redis/cluster.py +++ b/redis/cluster.py @@ -28,6 +28,7 @@ from redis.exceptions import ( TimeoutError, TryAgainError, ) +from redis.lock import Lock from redis.utils import ( dict_merge, list_keys_to_dict, @@ -742,6 +743,72 @@ class RedisCluster(RedisClusterCommands): reinitialize_steps=self.reinitialize_steps, ) + def lock( + self, + name, + timeout=None, + sleep=0.1, + blocking_timeout=None, + lock_class=None, + thread_local=True, + ): + """ + Return a new Lock object using key ``name`` that mimics + the behavior of threading.Lock. + + If specified, ``timeout`` indicates a maximum life for the lock. + By default, it will remain locked until release() is called. + + ``sleep`` indicates the amount of time to sleep per loop iteration + when the lock is in blocking mode and another client is currently + holding the lock. + + ``blocking_timeout`` indicates the maximum amount of time in seconds to + spend trying to acquire the lock. A value of ``None`` indicates + continue trying forever. ``blocking_timeout`` can be specified as a + float or integer, both representing the number of seconds to wait. + + ``lock_class`` forces the specified lock implementation. Note that as + of redis-py 3.0, the only lock class we implement is ``Lock`` (which is + a Lua-based lock). So, it's unlikely you'll need this parameter, unless + you have created your own custom lock class. + + ``thread_local`` indicates whether the lock token is placed in + thread-local storage. By default, the token is placed in thread local + storage so that a thread only sees its token, not a token set by + another thread. Consider the following timeline: + + time: 0, thread-1 acquires `my-lock`, with a timeout of 5 seconds. + thread-1 sets the token to "abc" + time: 1, thread-2 blocks trying to acquire `my-lock` using the + Lock instance. + time: 5, thread-1 has not yet completed. redis expires the lock + key. + time: 5, thread-2 acquired `my-lock` now that it's available. + thread-2 sets the token to "xyz" + time: 6, thread-1 finishes its work and calls release(). if the + token is *not* stored in thread local storage, then + thread-1 would see the token value as "xyz" and would be + able to successfully release the thread-2's lock. + + In some use cases it's necessary to disable thread local storage. For + example, if you have code where one thread acquires a lock and passes + that lock instance to a worker thread to release later. If thread + local storage isn't disabled in this case, the worker thread won't see + the token set by the thread that acquired the lock. Our assumption + is that these cases aren't common and as such default to using + thread local storage.""" + if lock_class is None: + lock_class = Lock + return lock_class( + self, + name, + timeout=timeout, + sleep=sleep, + blocking_timeout=blocking_timeout, + thread_local=thread_local, + ) + def _determine_nodes(self, *args, **kwargs): command = args[0] nodes_flag = kwargs.pop("nodes_flag", None) diff --git a/redis/lock.py b/redis/lock.py index 95bb413..74e769b 100644 --- a/redis/lock.py +++ b/redis/lock.py @@ -180,7 +180,7 @@ class Lock: if token is None: token = uuid.uuid1().hex.encode() else: - encoder = self.redis.connection_pool.get_encoder() + encoder = self.redis.get_encoder() token = encoder.encode(token) if blocking is None: blocking = self.blocking @@ -224,7 +224,7 @@ class Lock: # 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() + encoder = self.redis.get_encoder() stored_token = encoder.encode(stored_token) return self.local.token is not None and stored_token == self.local.token diff --git a/tests/test_lock.py b/tests/test_lock.py index 02cca1b..01ecb88 100644 --- a/tests/test_lock.py +++ b/tests/test_lock.py @@ -9,7 +9,6 @@ from redis.lock import Lock from .conftest import _get_client -@pytest.mark.onlynoncluster class TestLock: @pytest.fixture() def r_decoded(self, request): @@ -223,7 +222,6 @@ class TestLock: lock.reacquire() -@pytest.mark.onlynoncluster class TestLockClassSelection: def test_lock_class_argument(self, r): class MyLock: |