Skip to content

Request response policies - #2996

Merged
nkaradzhov merged 17 commits into
redis:masterfrom
nkaradzhov:request-response-policies
Jul 31, 2026
Merged

Request response policies#2996
nkaradzhov merged 17 commits into
redis:masterfrom
nkaradzhov:request-response-policies

Conversation

@nkaradzhov

@nkaradzhov nkaradzhov commented Jun 11, 2025

Copy link
Copy Markdown
Collaborator

Description

Describe your pull request here


Checklist

  • Does npm test pass with this change (including linting)?
  • Is the new or changed code fully tested?
  • Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?

Note

High Risk
Large behavioral change to cluster command routing (cross-slot MGET, fan-out admin commands, cursor state); incorrect policy or aggregation logic could misroute writes or return wrong merged results.

Overview
Adds cluster-wide command routing driven by server command metadata: resolve request/response policies, fan out or split commands across nodes, aggregate replies, and fall back to key-based routing when metadata is missing.

Execution path: Cluster commands and sendCommand go through _executeWithPolicies (policy resolution → router → per-node _execute with MOVED/ASK → reducer → optional post-hooks). Single-key default-keyed traffic keeps a fast path. Multi-shard commands (e.g. MGET, MSET, DEL) are split by hash slot using COMMAND key specs and replies are reassembled in caller key order. All-shards / all-nodes policies fan out with reducers (sum, min, merge, one_succeeded, etc.) and numeric aggregates re-apply type mappings after aggregation.

Special cursors: Virtual tokens and bindings on RedisClusterSlots implement cluster-wide SCAN (walk masters sequentially) and sticky FT.CURSOR / FT.AGGREGATE routing so keyless cursor commands hit the correct node.

Parser / metadata: CommandParser gains commandIdentifier and markRoutingKey for raw cluster sends; client-side cache eligibility uses command-metadata + isCacheable when CSC is enabled. Bloom module commands drop per-command IS_READ_ONLY (replica safety comes from metadata). Cluster multi pipelines use isReplicaSafe the same way.

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

@nkaradzhov
nkaradzhov requested a review from bobymicroby June 11, 2025 07:49
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch 2 times, most recently from e74bb9a to 2ea355f Compare June 17, 2025 08:14
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch from fd526fa to e857c1b Compare June 9, 2026 08:18
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch 3 times, most recently from a85904d to 25716d1 Compare June 30, 2026 09:54
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch 2 times, most recently from 55e1732 to fb6d15e Compare July 2, 2026 17:37
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch 2 times, most recently from c413da5 to 005db4a Compare July 14, 2026 15:21
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch from a745ca1 to b3371b2 Compare July 16, 2026 16:26
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch from 1947819 to 689af4d Compare July 24, 2026 08:05
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch from 8f70362 to 8dff5a0 Compare July 29, 2026 08:12
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch from 177e8e9 to 313219c Compare July 30, 2026 09:20
@nkaradzhov
nkaradzhov marked this pull request as ready for review July 30, 2026 10:52
@nkaradzhov
nkaradzhov requested a review from PavelPashov July 30, 2026 10:52
Comment thread packages/bloom/lib/commands/bloom/EXISTS.ts
nkaradzhov and others added 6 commits July 30, 2026 14:14
Regenerate the static command-metadata table from a live server COMMAND
dump and add the generator + overrides tooling that produces it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drive each command's routing and client-side-cache eligibility from the
regenerated server COMMAND metadata instead of per-file hand annotations,
updating the command definitions across all packages accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parse the COMMAND key-specifications into CommandReply, add the
request/response policy constants, and the static + dynamic policy
resolvers that back cluster routing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split a multi_shard command's keys per hash slot using the server key
specs as the reconstruction recipe (numkeys rewrite, options-suffix copy,
single-slot fast path), recording group indices for reply reassembly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add _executeWithPolicies: resolve a command's policy, route it to an
execution plan (pass-through, fan-out, or multi_shard split), and reduce
the replies via the response policy. Includes the default-policy fallback
for policy-less commands and the raw sendCommand split path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement the client-side special policies: cluster-wide SCAN behind a
virtual cursor token, RANDOMKEY fan-out with a non-nil reduction, and
sticky FT.CURSOR routing bound to the node that served the aggregate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch from 6983991 to 8cc07fd Compare July 30, 2026 11:14

@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: 6983991f2f

ℹ️ 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 packages/client/lib/cluster/request-response-policies/dispatch.ts Outdated
Comment thread packages/client/lib/commands/generic-transformers.ts
Comment thread packages/client/lib/commands/TOUCH.ts
Comment thread packages/client/lib/command-metadata/predicates.ts
Comment thread packages/client/lib/cluster/index.ts
Comment thread packages/client/lib/cluster/index.ts
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch from 8cc07fd to 0ce49cb Compare July 30, 2026 12:25
Comment thread packages/client/lib/cluster/index.ts
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch from 0ce49cb to 090eea5 Compare July 30, 2026 12:41

@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: 090eea51da

ℹ️ 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 packages/client/lib/commands/GET.ts
Comment thread packages/client/lib/sentinel/utils.ts
Comment thread packages/client/lib/command-metadata/command-metadata-data.ts
Comment thread packages/client/lib/sentinel/utils.ts
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch from 090eea5 to 959dacb Compare July 30, 2026 13:16
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch from 959dacb to 7eef2a9 Compare July 30, 2026 13:37
Comment thread packages/client/lib/cluster/index.ts Outdated
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch from 7eef2a9 to f7d7e38 Compare July 30, 2026 18:29
Unit-test the splitter, resolvers, aggregators and cursor routers, and add
cluster-docker integration coverage with one assertion per reachable
request/response policy type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nkaradzhov
nkaradzhov force-pushed the request-response-policies branch from f7d7e38 to 91ea94d Compare July 30, 2026 18:53
nkaradzhov and others added 3 commits July 30, 2026 22:45
WAITAOF is tagged all_shards/agg_min but its reply is an array
([numlocal, numreplicas]); the scalar aggregateMin threw "All replies
must be numbers", breaking every raw cluster fan-out. Fold array replies
per-position — matching the server's AGG_MIN/AGG_MAX semantics and the
existing element-wise logical AND/OR aggregators — via a shared helper;
scalar aggregation is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The server flags TOUCH `[readonly, fast]` (no `write`), so the derived
isReplicaSafe would route it to a replica. TOUCH only bumps LRU/LFU access
metadata that drives eviction on the master, so it must stay master-only.
Restore the explicit IS_READ_ONLY: false override (dropped when overrides
were replaced by metadata derivation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The metadata-derived isReplicaSafe returns false for every keyless command
(it cannot tell a keyless read from keyless admin like SHUTDOWN), so keyless
reads that master routed to replicas now go master-only in sentinel. Restore
the explicit IS_READ_ONLY: true override on the keyless data reads — DBSIZE,
KEYS, SCAN, RANDOMKEY, the RediSearch reads (FT.SEARCH/SPELLCHECK/EXPLAIN/
EXPLAINCLI/INFO/DICTDUMP/TAGVALS/SYNDUMP/PROFILE) and the time-series reads
(TS.MGET/MRANGE/MREVRANGE/QUERYINDEX) — matching what master declared. Each is
a non-mutating read over replicated data. FT.AGGREGATE is deliberately left
master-only (WITHCURSOR mints server-side cursor state), as on master.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread packages/client/lib/cluster/index.ts
The cluster and sentinel MULTI/pipeline builders passed command.IS_READ_ONLY
raw, bypassing the metadata resolver the standalone path uses. After the
overrides were dropped in favor of derivation, a read-only command in a
pipeline (e.g. GET) passed `undefined` and routed to the master even though
the same command standalone reaches a replica. Route the std and module
builders through isReplicaSafe(lookup(id), command.IS_READ_ONLY) — override
first, else server metadata — memoized per command method like the standalone
path. Function/script builders keep their explicit IS_READ_ONLY, unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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: aa9fa83fa2

ℹ️ 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 packages/client/lib/cluster/index.ts Outdated
nkaradzhov and others added 2 commits July 31, 2026 15:34
`&&`/`||` return the operand, not a boolean, so a future agg_logical_and/or
command returning non-binary numbers would leak values (1 && 2 === 2). Clamp
each element to 0/1. Correct today for SCRIPT EXISTS (0/1 inputs); this
future-proofs the registered reducers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread packages/client/lib/cluster/multi-command.ts
The FT.CURSOR cluster tests retry the initial aggregate until the freshly
created index has propagated to the coordinator, but the retry predicate only
matched the 7.x "No such index" wording. On 8.x the server returns
"SEARCH_INDEX_NOT_FOUND Index not found", which slipped past the predicate and
rethrew on the first racy attempt (intermittent CI failures). Broaden the
predicate to match both wordings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@PavelPashov PavelPashov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we document these clustering changes and add release notes before merging? Cross-slot mGet/mSet/del now succeed, cluster.scan() uses instance-specific virtual cursors, keys()/dbSize()/flushAll() fan out, and raw sendCommand(['DBSIZE']) has new reply semantics. This is a breaking change for raw-path users that docs/clustering.md doesn’t cover.

Comment thread packages/client/lib/cluster/request-response-policies/dispatch.ts
Comment thread packages/client/lib/command-metadata/command-metadata-data.ts
…ail the route

routeAllShards/routeAllNodes awaited getAllMasterClients()/getAllClients(),
which Promise.all over lazy node connects — a single failed connect rejected
the entire route before any reducer ran, so a one_succeeded command like
SCRIPT KILL failed even when another shard was reachable. Return the fan-out
target nodes without connecting (getAllMasterNodes/getAllNodes) and defer each
node's connect into its own per-entry promise via a getClient thunk, resolved
inside the plan's per-entry execution. A connect failure now rejects only that
entry, so the reducer (Promise.any for one_succeeded, Promise.all for the
all-must-succeed policies) sees the reachable shards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@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 default effort and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 86128d2. Configure here.

Comment thread packages/client/lib/cluster/cluster-slots.ts
nkaradzhov and others added 2 commits July 31, 2026 22:32
A blocking read (XREAD ... BLOCK) can return null on timeout; cached, that
reply would be replayed instead of blocking for new data. Eligibility is
per-command so the non-blocking form can't be carved out — add a `blocking`
flag exclusion to isCacheable. Verified against a live COMMAND reply (447
commands, 130 cacheable): XREAD is the only command this flips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nkaradzhov
nkaradzhov merged commit f2a432f into redis:master Jul 31, 2026
14 of 15 checks passed
@nkaradzhov
nkaradzhov deleted the request-response-policies branch July 31, 2026 19:44

@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: a28d8ec06d

ℹ️ 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".

publish(CHANNELS.COMMAND_REPLY, () => ({ args: sanitizeArgs(parser.redisArgs), reply: finalReply, clientId: this._self._clientId }));
// Override-first: a defined `Command.CACHEABLE` wins; otherwise CSC
// eligibility derives from the server flags/tips (see `isCacheable`).
const cacheable = isCacheable(defaultCommandMetadata.lookup(parser.commandIdentifier), command.CACHEABLE);

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 Keep reviver-dependent JSON reads out of CSC

When client-side caching is enabled, deriving cacheability from metadata makes JSON.GET/JSON.MGET cacheable even though their reviver lives in parser.preserve and is not part of redisArgs. handleCache keys only on the wire args and returns the already transformed value on a hit, so json.get('k', { reviver: a }) followed by the same JSON.GET with reviver b will skip b and return a clone of a's transformed result until the key is invalidated; keep these commands explicitly non-cacheable or include the preserve/transform context in the cache entry.

Useful? React with 👍 / 👎.

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