Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/lua_scripting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,10 @@ The following commands are not supported:
- ``EVAL_RO``
- ``EVALSHA_RO``

Using scripting within pipelines in cluster mode is **not supported**.
``EVALSHA`` can be used inside a ``ClusterPipeline``. Keys must still map to
the same hash slot (or ``numkeys`` may be ``0``, in which case the command is
routed to a random primary). The script must already be loaded on the target
node, for example via ``SCRIPT LOAD`` on the cluster client, which loads the
script on all primaries. Other scripting helpers such as ``EVAL``,
``load_scripts``, and ``script_load_for_pipeline`` remain unsupported on
cluster pipelines.
12 changes: 7 additions & 5 deletions redis/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -3641,7 +3641,6 @@ def inner(*args, **kwargs):
"CONFIG",
"DBSIZE",
"ECHO",
"EVALSHA",
"FLUSHALL",
Comment thread
ahmed5145 marked this conversation as resolved.
"FLUSHDB",
"INFO",
Expand Down Expand Up @@ -4112,12 +4111,15 @@ def _send_cluster_commands(
# in a list of pre-defined request policies
command_flag = self.command_flags.get(command)
if not command_flag:
# Fallback to default policy
# Fallback to default policy.
# Use determine_slot (not _get_command_keys) so EVAL /
# EVALSHA with numkeys=0 work on Redis <7, matching
# the async ClusterPipeline path.
if not self._pipe.get_default_node():
keys = None
slot = None
else:
keys = self._pipe._get_command_keys(*c.args)
if not keys or len(keys) == 0:
slot = self._pipe.determine_slot(*c.args)
Comment thread
ahmed5145 marked this conversation as resolved.
Outdated
if slot is None:
command_policies = CommandPolicies()
else:
command_policies = CommandPolicies(
Expand Down
74 changes: 74 additions & 0 deletions tests/test_cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
)
from redis.cluster import (
PRIMARY,
PIPELINE_BLOCKED_COMMANDS,
REDIS_CLUSTER_HASH_SLOTS,
REPLICA,
ClusterNode,
Expand Down Expand Up @@ -3835,6 +3836,56 @@ def test_disconnect_calls_disconnect_on_existing_connections(self, r):
mock_node_conn.disconnect.assert_called_once()


def test_evalsha_not_in_pipeline_blocked_commands():
"""EVALSHA must be usable in ClusterPipeline (see #2914)."""
assert "EVALSHA" not in PIPELINE_BLOCKED_COMMANDS
Comment thread
ahmed5145 marked this conversation as resolved.


def test_evalsha_can_be_queued_on_cluster_pipeline():
"""
Queuing EVALSHA on a ClusterPipeline must not raise the historical
'blocked when running redis in cluster mode' error. Slot selection for
EVALSHA is already handled by RedisCluster.determine_slot().
"""
r = get_mocked_redis_client(host=default_host, port=default_port)
try:
pipe = r.pipeline()
sha = "a" * 40
returned = pipe.evalsha(sha, 1, "foo", "bar")
assert returned is pipe

queue = pipe._execution_strategy.command_queue
assert len(queue) == 1
assert queue[0].args == ("EVALSHA", sha, 1, "foo", "bar")

assert r.determine_slot("EVALSHA", sha, 1, "foo", "bar") == key_slot(b"foo")
finally:
r.close()


def test_evalsha_zero_keys_pipeline_execute_skips_get_command_keys():
"""
Sync ClusterPipeline must route EVALSHA via determine_slot, not
COMMAND GETKEYS. Redis <7 fails on GETKEYS for EVALSHA with numkeys=0.
"""
r = get_mocked_redis_client(host=default_host, port=default_port)
try:
mock_all_nodes_resp(r, 42)
sha = "a" * 40
with r.pipeline() as pipe:
pipe.evalsha(sha, 0)
with patch.object(
pipe,
"_get_command_keys",
side_effect=AssertionError(
"COMMAND GETKEYS must not be used for EVALSHA"
),
):
assert pipe.execute() == [42]
finally:
r.close()


@pytest.mark.onlycluster
class TestClusterPipeline:
"""
Expand Down Expand Up @@ -3870,6 +3921,29 @@ def test_blocked_arguments(self, r):
str(ex.value).startswith("shard_hint is deprecated in cluster mode") is True
)

def test_evalsha_in_pipeline(self, r):
"""
EVALSHA is allowed in ClusterPipeline when keys map to one slot
and the script is already loaded on cluster primaries (#2914).
"""
multiply = "return redis.call('GET', KEYS[1]) * ARGV[1]"
sha = r.script_load(multiply)
# hash tag keeps the key on a known slot for the pipeline
r.set("{user}a", 2)
with r.pipeline() as pipe:
pipe.evalsha(sha, 1, "{user}a", 3)
assert pipe.execute() == [6]

def test_evalsha_zero_keys_in_pipeline(self, r):
"""
EVALSHA with numkeys=0 must execute in ClusterPipeline without
calling COMMAND GETKEYS (broken on Redis <7 for this case).
"""
sha = r.script_load("return 42")
with r.pipeline() as pipe:
pipe.evalsha(sha, 0)
Comment thread
ahmed5145 marked this conversation as resolved.
assert pipe.execute() == [42]

def test_redis_cluster_pipeline(self, r):
"""
Test that we can use a pipeline with the RedisCluster class
Expand Down