Skip to content

Route SCRIPT KILL and FUNCTION KILL to every primary in cluster mode - #4568

Closed
LindseyZ1205 wants to merge 4 commits into
redis:masterfrom
LindseyZ1205:fix/cluster-script-kill
Closed

LindseyZ1205 wants to merge 4 commits into
redis:masterfrom
LindseyZ1205:fix/cluster-script-kill

Conversation

@LindseyZ1205

@LindseyZ1205 LindseyZ1205 commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Description of change

script_kill() and function_kill() can't be used with RedisCluster:

from redis.cluster import RedisCluster

rc = RedisCluster(host="localhost", port=30001)
rc.script_kill()
# RedisClusterException: No way to dispatch this command to Redis Cluster. Missing key.

Neither command is in COMMAND_FLAGS or in the static command metadata, and they take no key, so the client has nothing to route them by. execute_command("SCRIPT KILL", target_nodes=RedisCluster.PRIMARIES) fails the same way, because a nodes flag still goes through that lookup.

The server tips both commands request_policy:all_shards and response_policy:one_succeeded: the script or function runs on one shard, and the others answer NOTBUSY. This adds both to the static metadata with those policies, and makes the cluster clients handle ONE_SUCCEEDED the way the tip describes: the command is sent to every target and the first reply that isn't an error is returned, whatever the other targets answer, since retrying a kill that succeeded would only get NOTBUSY. An error is raised only if no target succeeds, so NOTBUSY still comes back when nothing is running, as with a standalone client. An error that isn't a reply or that the retry handling applies to, such as a ConnectionError or CLUSTERDOWN, then comes first, as the script may run on that node, and it goes through the retry handling as before. Next comes an error reply such as UNKILLABLE, from a node whose script can't be stopped, and NOTBUSY comes last.

Until now ONE_SUCCEEDED could only come from a server-derived resolver, and it didn't work there either. With a DynamicPolicyResolver, the sync client sent SCRIPT KILL to the first primary only and raised its NOTBUSY when the script ran elsewhere. When the script ran on the first primary, it was killed, but _process_result() then raised KeyError: <ResponsePolicy.ONE_SUCCEEDED: 'one_succeeded'>, because the response-policy callbacks only knew the two defaults. The asyncio client had no ONE_SUCCEEDED handling: it sent the command to every primary and raised the first NOTBUSY, even when the script was killed.

FUNCTION KILL isn't in PIPELINE_BLOCKED_COMMANDS, so in a cluster pipeline it now raises "Too many targets", like other commands that go to every primary such as FUNCTION FLUSH, instead of going to a single node. With a single node in target_nodes, it still goes to that node, and a response callback set for it still applies: the sync cluster pipeline, which has its own response policy callbacks, knows ONE_SUCCEEDED too.

#2599, which was closed for inactivity, listed both commands among the ones that should be sent to all primaries.

With a DynamicPolicyResolver, commands tipped with the other aggregate response policies still raise KeyError in _process_result() the same way. I checked DBSIZE (agg_sum), PING, CONFIG SET, CLIENT SETNAME and SCRIPT FLUSH (all_succeeded), SCRIPT EXISTS (agg_logical_and), and RANDOMKEY and FUNCTION STATS (special). I left those out of this change.

Tests

  • test_kill_reaches_every_primary_and_returns_the_first_success and test_kill_raises_when_every_primary_replies_with_an_error in TestStaticMetadataRouting, for both clients and both commands, fail without this change. I updated the docstrings of test_explicit_target_nodes_do_not_apply_the_commands_aggregation, which described the old ONE_SUCCEEDED handling.
  • test_pipeline_function_kill_on_one_node_reaches_its_response_callback, for both clients, runs FUNCTION KILL on one node in a pipeline with a response callback set. Without the sync pipeline's ONE_SUCCEEDED entry, the sync one fails with KeyError.
  • test_kill_succeeds_when_another_primary_cannot_be_reached and test_kill_raises_the_error_of_an_unreachable_primary_first, for both clients, cover a primary that can't be reached, before and after the others.
  • test_kill_raises_another_error_rather_than_notbusy, for both clients, checks that UNKILLABLE or CLUSTERDOWN from one primary is raised rather than the NOTBUSY of the others. Against a local Redis 8.2.1 cluster, with a script or function that has written running on the last primary, script_kill() and function_kill() now raise UNKILLABLE with both clients, where they raised NOTBUSY.
  • Against a local Redis 8.2.1 cluster with three primaries and busy-reply-threshold 100, with a script or a function running on the last primary, script_kill() and function_kill() stop it with both clients: by default, with target_nodes=RedisCluster.PRIMARIES, and with a DynamicPolicyResolver. With nothing running they raise NOTBUSY.
  • tests/test_cluster.py, tests/test_cluster_transaction.py, tests/test_pubsub.py and their asyncio versions give the same results with and without this change, against a local Redis 8.2.1 cluster (3 primaries, 3 replicas) with --protocol 2 and 3, and in a fixed_client run. So do tests/test_command_metadata.py and its asyncio version. The only failures are test_geosearchstore_dist and test_cluster_georadius_store_dist, whose distances differ in the last digits with my Redis build, the asyncio TestSSL tests, which need certificates I don't have locally, and the metadata tests that need the module server on localhost:6479.
  • The two records match the COMMAND INFO reply of Redis 8.2.1 (flags noscript and allow_busy, and the two tips). I don't have the 8.10 server that the table's provenance note mentions, so I couldn't check them against it.
  • ruff check, ruff format --check (0.9.6) and vulture pass.

Note

Medium Risk
Changes core multi-node command dispatch and error aggregation for a new response policy, affecting SCRIPT/FUNCTION KILL and any metadata using ONE_SUCCEEDED, though behavior is scoped and heavily tested.

Overview
SCRIPT KILL and FUNCTION KILL now work on cluster clients by registering them in static metadata with request_policy:all_shards and response_policy:one_succeeded, matching Redis server tips.

Cluster execution for ONE_SUCCEEDED is implemented in sync, async, and pipeline paths: the command is sent to every resolved target, the first non-error reply is returned (others may answer NOTBUSY), and if none succeed _one_succeeded_error picks the raised error (connectivity/retryable errors before UNKILLABLE-style replies, with NOTBUSY last). The sync client no longer stops after the first primary; the async client no longer fails on the first NOTBUSY while a later shard succeeded.

Tests cover all-primaries routing, all-NOTBUSY, unreachable nodes, preferring serious errors over NOTBUSY, and pipeline FUNCTION KILL with response callbacks; docstrings for explicit target_nodes vs cluster aggregation were updated.

Reviewed by Cursor Bugbot for commit 33dc0ca. Bugbot is set up for automated code reviews on this repo. Configure here.

Neither command was in the static metadata or in COMMAND_FLAGS, and
both are keyless, so the cluster clients raised "No way to dispatch".
Record the request_policy:all_shards and response_policy:one_succeeded
tips the server reports for them, and handle ONE_SUCCEEDED as the tip
says: send the command to every target and return the first reply that
is not an error, raising one only if every target replies with one.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-04T00:48:06.735265Z 33dc0ca New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3e7bb1d212

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread redis/cluster.py
RequestPolicy.SPECIAL: self.get_special_nodes,
ResponsePolicy.DEFAULT_KEYLESS: lambda res: res,
ResponsePolicy.DEFAULT_KEYED: lambda res: res,
ResponsePolicy.ONE_SUCCEEDED: lambda res: res,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add ONE_SUCCEEDED to the sync pipeline policy map

When a sync ClusterPipeline queues execute_command("FUNCTION KILL", target_nodes=node) and a response callback is registered for that command, the explicit target avoids the multi-target rejection but the newly resolved ONE_SUCCEEDED response policy is retained. PipelineStrategy._send_cluster_commands() then indexes the pipeline's own _policies_callback_mapping, which still contains only the two default response policies, so this previously supported callback path now raises KeyError instead of returning the callback result; mirror this entry in ClusterPipeline.__init__ as well (the async pipeline reuses the client map).

AGENTS.md reference: AGENTS.md:L146-L149

Useful? React with 👍 / 👎.

A pipeline sends FUNCTION KILL to a single target and keeps its
ONE_SUCCEEDED response policy, so with a response callback registered
for the command, the pipeline's own policy callback map raised
KeyError.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cf32034d6f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

"exists": _CACHEABLE_KEYED,
"expiretime": _CACHEABLE_KEYED,
"fcall_ro": _SCRIPT_RUNNER_MOVABLE_KEYS,
"function kill": _WRITE_KEYLESS_ALL_SHARDS_ONE_SUCCEEDED,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve explicit pipeline targets during kill retries

When FUNCTION KILL is queued in a cluster pipeline with a single explicit node, a read/write ConnectionError triggers the pipeline retry after the sync strategy has already popped target_nodes at redis/cluster.py:5825; it then calls parent_execute_command at line 6037 without that target. This new ALL_SHARDS policy consequently broadcasts the retry to every primary, so a transient failure can terminate a function on a shard the caller did not select; the async strategy likewise pops the kwarg at redis/asyncio/cluster.py:3795 but retries into “Too many targets,” so the stacks diverge. Preserve the explicit target or disable retries for explicitly targeted pipeline commands.

AGENTS.md reference: AGENTS.md:L146-L149

Useful? React with 👍 / 👎.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread redis/asyncio/cluster.py Resolved
A ConnectionError or TimeoutError from one target was raised even after
another target had answered, so a SCRIPT KILL that had stopped the
script was retried, and the retry got NOTBUSY from every shard. The sync
client also stopped at that target, without trying the others. Now an
error that isn't a reply only matters when no target succeeds, and it is
then raised before the error replies, as the script may run on that
node.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c1127b3c4a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread redis/cluster.py Outdated
Comment on lines +2007 to +2008
if first_error is None:
first_error = e

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the busy shard's UNKILLABLE response

When a running script or function has already performed a write, its shard replies UNKILLABLE while every other primary replies NOTBUSY. Keeping the first ResponseError (and the async fallback to replies[0] at redis/asyncio/cluster.py:1644-1650) means that whenever a nonbusy primary is ordered first, script_kill()/function_kill() report NOTBUSY even though the execution is still running and cannot be killed. Prefer a non-NOTBUSY reply when no shard succeeds, using NOTBUSY only when all shards return it.

Useful? React with 👍 / 👎.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit c1127b3. Configure here.

Comment thread redis/cluster.py Resolved
When no target succeeded, the first error reply was raised, so the
NOTBUSY of a node that runs no script could hide the UNKILLABLE of the
node whose script can't be stopped. CLUSTERDOWN, a ResponseError that
the retry handling applies to, was also taken as a reply, so an earlier
NOTBUSY was raised and the command wasn't retried. Now an error that
isn't a reply or that the retry handling applies to comes first, then
the other error replies, and NOTBUSY last.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 33dc0ca859

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

# the script or function, while the others answer NOTBUSY.
_WRITE_KEYLESS_ALL_SHARDS_ONE_SUCCEEDED = replace(
_WRITE_KEYLESS,
request_policy=RequestPolicy.ALL_SHARDS,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include replicas when broadcasting kill commands

When a long-running EVAL_RO/EVALSHA_RO or FCALL_RO is routed to a replica (for example with read_from_replicas or a replicas-only load-balancing strategy), this policy expands via get_primaries() at redis/cluster.py:1208, so script_kill()/function_kill() only collect NOTBUSY from primaries and leave the replica's execution running. Those read-only runners are explicitly replica-safe in the static metadata and get_nodes_from_slot() selects replicas for them, so the kill broadcast must include replicas (or all nodes), not only shards' primaries.

AGENTS.md reference: AGENTS.md:L159-L163

Useful? React with 👍 / 👎.

@petyaslavova

Copy link
Copy Markdown
Collaborator

Hey @LindseyZ1205, thank you for your contribution!

The problem is real: script_kill() and function_kill() can't be dispatched on RedisCluster today, and ONE_SUCCEEDED has no working handling on either client. #4569 routes the same two commands with the same all_shards / one_succeeded tips. It also handles the other aggregate response policies (agg_sum, all_succeeded, agg_logical_and, special) that still raise KeyError here. To keep one implementation path, I'm going to close this PR in favor of #4569.

There is one difference worth noting. This PR keeps a success when another shard can't be reached. #4569 fails the command so it can be retried. If you have a real scenario where that retry hides a successful kill, please add it on #4569.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants