Skip to content

Commit 9937904

Browse files
feat(cluster): support for transactions on cluster-aware client
Adds support for transactions based on multi/watch/exec on clusters. Transactions in this mode are limited to a single hash slot. Contributed-by: Scopely <[email protected]>
1 parent 950a462 commit 9937904

10 files changed

+1081
-98
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ vagrant/.vagrant
99
.cache
1010
.eggs
1111
.idea
12+
.vscode
1213
.coverage
1314
env
1415
venv

CHANGES

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
* Support transactions in ClusterPipeline (originally developed by Scopely and contributed under the MIT License)
12
* Removing support for RedisGraph module. RedisGraph support is deprecated since Redis Stack 7.2 (https://redis.com/blog/redisgraph-eol/)
23
* Fix lock.extend() typedef to accept float TTL extension
34
* Update URL in the readme linking to Redis University

docs/advanced_features.rst

+42-14
Original file line numberDiff line numberDiff line change
@@ -177,20 +177,48 @@ the server.
177177
... pipe.set('foo1', 'bar1').get('foo1').execute()
178178
[True, b'bar1']
179179
180-
Please note: - RedisCluster pipelines currently only support key-based
181-
commands. - The pipeline gets its ‘read_from_replicas’ value from the
182-
cluster’s parameter. Thus, if read from replications is enabled in the
183-
cluster instance, the pipeline will also direct read commands to
184-
replicas. - The ‘transaction’ option is NOT supported in cluster-mode.
185-
In non-cluster mode, the ‘transaction’ option is available when
186-
executing pipelines. This wraps the pipeline commands with MULTI/EXEC
187-
commands, and effectively turns the pipeline commands into a single
188-
transaction block. This means that all commands are executed
189-
sequentially without any interruptions from other clients. However, in
190-
cluster-mode this is not possible, because commands are partitioned
191-
according to their respective destination nodes. This means that we can
192-
not turn the pipeline commands into one transaction block, because in
193-
most cases they are split up into several smaller pipelines.
180+
Please note:
181+
182+
- RedisCluster pipelines currently only support key-based commands.
183+
- The pipeline gets its ‘read_from_replicas’ value from the
184+
cluster’s parameter. Thus, if read from replications is enabled in
185+
the cluster instance, the pipeline will also direct read commands to
186+
replicas.
187+
188+
189+
Transactions in clusters
190+
~~~~~~~~~~~~~~~~~~~~~~~~
191+
192+
Transactions are supported in cluster-mode with one caveat: all keys of
193+
all commands issued on a transaction pipeline must reside on the
194+
same slot. This is similar to the limitation of multikey commands in
195+
cluster. The reason behind this is that the Redis engine does not offer
196+
a mechanism to block or exchange key data across nodes on the fly. A
197+
client may add some logic to abstract engine limitations when running
198+
on a cluster, such as the pipeline behavior explained on the previous
199+
block, but there is no simple way that a client can enforce atomicity
200+
across nodes on a distributed system.
201+
202+
The compromise of limiting the transaction pipeline to same-slot keys
203+
is exactly that: a compromise. While this behavior is differnet from
204+
non-transactional cluster pipelines, it simplifies migration of clients
205+
from standalone to cluster under some circumstances. Note that application
206+
code that issues multi/exec commands on a standalone client without
207+
embedding them within a pipeline would eventually get ‘AttributeError’s.
208+
With this approach, if the application uses ‘client.pipeline(transaction=True)’,
209+
then switching the client with a cluster-aware instance would simplify
210+
code changes (to some extent). This may be true for application code that
211+
makes use of hash keys, since its transactions may are already be
212+
mapping all commands to the same slot.
213+
214+
An alternative is some kind of two-step commit solution, where a slot
215+
validation is run before the actual commands are run. This could work
216+
with controlled node maintenance but does not cover single node failures.
217+
218+
Cluster transaction support (pipeline/multi/exec) was originally developed by
219+
Scopely and contributed to redis-py under the MIT License.
220+
221+
194222

195223
Publish / Subscribe
196224
-------------------

redis/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616
BusyLoadingError,
1717
ChildDeadlockedError,
1818
ConnectionError,
19+
CrossSlotTransactionError,
1920
DataError,
21+
InvalidPipelineStack,
2022
InvalidResponse,
2123
OutOfMemoryError,
2224
PubSubError,
2325
ReadOnlyError,
26+
RedisClusterException,
2427
RedisError,
2528
ResponseError,
2629
TimeoutError,
@@ -56,15 +59,18 @@ def int_or_str(value):
5659
"ConnectionError",
5760
"ConnectionPool",
5861
"CredentialProvider",
62+
"CrossSlotTransactionError",
5963
"DataError",
6064
"from_url",
6165
"default_backoff",
66+
"InvalidPipelineStack",
6267
"InvalidResponse",
6368
"OutOfMemoryError",
6469
"PubSubError",
6570
"ReadOnlyError",
6671
"Redis",
6772
"RedisCluster",
73+
"RedisClusterException",
6874
"RedisError",
6975
"ResponseError",
7076
"Sentinel",

redis/client.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
list_or_args,
3131
)
3232
from redis.connection import (
33+
Connection,
3334
AbstractConnection,
3435
ConnectionPool,
3536
SSLConnection,
@@ -1279,9 +1280,15 @@ class Pipeline(Redis):
12791280

12801281
UNWATCH_COMMANDS = {"DISCARD", "EXEC", "UNWATCH"}
12811282

1282-
def __init__(self, connection_pool, response_callbacks, transaction, shard_hint):
1283+
def __init__(
1284+
self,
1285+
connection_pool: ConnectionPool,
1286+
response_callbacks,
1287+
transaction,
1288+
shard_hint,
1289+
):
12831290
self.connection_pool = connection_pool
1284-
self.connection = None
1291+
self.connection: Optional[Connection] = None
12851292
self.response_callbacks = response_callbacks
12861293
self.transaction = transaction
12871294
self.shard_hint = shard_hint
@@ -1414,7 +1421,7 @@ def pipeline_execute_command(self, *args, **options) -> "Pipeline":
14141421
self.command_stack.append((args, options))
14151422
return self
14161423

1417-
def _execute_transaction(self, connection, commands, raise_on_error) -> List:
1424+
def _execute_transaction(self, connection: Connection, commands, raise_on_error) -> List:
14181425
cmds = chain([(("MULTI",), {})], commands, [(("EXEC",), {})])
14191426
all_cmds = connection.pack_commands(
14201427
[args for args, options in cmds if EMPTY_RESPONSE not in options]

0 commit comments

Comments
 (0)