Skip to content

feat(cluster): add scanIterator and HSCAN/SSCAN/ZSCAN iterators - #3529

Merged
nkaradzhov merged 2 commits into
redis:masterfrom
smit153:feat/1877-cluster-scan-iterator
Oct 23, 2026
Merged

nkaradzhov merged 2 commits into
redis:masterfrom
smit153:feat/1877-cluster-scan-iterator

Conversation

@smit153

@smit153 smit153 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Description

RedisCluster already supports cluster-wide SCAN, but it has no scanIterator(), so for await (const keys of cluster.scanIterator()) fails with "cluster.scanIterator is not a function". This adds it, plus the single-key iterators, as discussed in the issue.

  • scanIterator() is a thin loop over cluster.scan(), which already walks every master. It compares the cursor with cursor.toString() !== '0', so it stops under a Buffer type mapping too.
  • If the caller breaks out of the loop (or an error is thrown), the finally block calls evictScanCursor, so the cursor is freed right away and not left to the idle sweep.
  • If a call fails and the node bound to the cursor is no longer in the cluster, the iterator throws ScanIteratorInterruptedError with the original error as cause. Other errors pass through unchanged, as in the sentinel iterator. ScanIteratorInterruptedError takes an optional message so the cluster case doesn't say "Sentinel master change"; the default message is unchanged.
  • hScanIterator, sScanIterator and zScanIterator loop over the existing cluster commands, but every page runs on the slot master. Scan cursors are only valid on the node that issued them, and with useReplicas read-only commands rotate between the master and its replicas, which skips or repeats entries. I also added hScanValuesIterator and hScanNoValuesIterator so the cluster client has the same iterators as RedisClient.
  • Docs: a "Cluster" section in docs/scan-iterators.md and a link to it from docs/clustering.md.

Closes #1877.

Validation

New cases in cluster/index.spec.ts:

  • 100 keys spread over all masters (the test checks each master holds some), each one comes back
  • a Buffer type mapping (keys are Buffers and the loop ends)
  • an early break leaves no cursor behind
  • a cursor whose node has left throws ScanIteratorInterruptedError and is freed
  • a live cursor of another scan passed in as cursor is rejected and left alone
  • the hash, set and sorted set iterators
  • useReplicas: 300 members, all returned, no page sent to a replica

cluster/index.spec.ts and scan-cursor.spec.ts pass on Linux with Docker (216 passing). tsc and npm run lint are clean.


Checklist

  • Does npm test pass with this change (including linting)? Ran the cluster specs and lint, not the full suite.
  • Is the new or changed code fully tested?
  • Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?

Note

Medium Risk
New cluster iteration APIs touch scan cursor lifecycle and command routing (useReplicas / master pinning); misbehavior could miss keys or leak cursors, though behavior mirrors existing cluster.scan() and standalone iterators.

Overview
Adds for await scan iterators to RedisCluster, matching the standalone client: cluster-wide scanIterator() plus keyed hScanIterator, hScanValuesIterator, hScanNoValuesIterator, sScanIterator, and zScanIterator.

Cluster-wide iteration loops on existing cluster.scan() (all masters), compares cursors with toString() so Buffer type mappings terminate, evicts the virtual cursor when the loop ends early or errors, and throws ScanIteratorInterruptedError (with a cluster-specific message) when the node bound to the cursor leaves the cluster. Keyed *SCAN iterators use an internal _masterOnly view (_forceMaster) so pages stay on the slot master even when useReplicas is enabled.

ScanIteratorInterruptedError now accepts an optional custom message (default unchanged for Sentinel). Docs add cluster scan-iterator guidance in scan-iterators.md and clustering.md. Integration tests cover full keyspace coverage, Buffer mappings, cursor cleanup, interruption, and replica routing for sScanIterator.

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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 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 🔄 Running since 2026-10-22T10:14:32.797329Z c42ed72 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.

@smit153 smit153 mentioned this pull request Oct 22, 2026

@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: 66aa795d4c

ℹ️ 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
) {
let cursor = options?.cursor ?? '0';
do {
const reply = await this.hScan(key, cursor, options);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pin each keyed scan iterator to one node

When useReplicas: true and a hash, set, or sorted set needs multiple pages, each call here is routed independently as a read-only keyed command. _execute therefore invokes getSlotRandomNode, whose slot iterator cycles through the master and replicas, so the cursor returned by one Redis process is sent to a different process on the next page; because scan cursors are node-local, this can omit or duplicate entries. The same pattern is repeated in all five new keyed iterators, so select and pin one node client for the whole iteration (or force every page to the master).

Useful? React with 👍 / 👎.

Comment thread packages/client/lib/cluster/index.ts Outdated
} finally {
// Free the cluster-wide cursor when the caller stops early (or on an
// error) instead of leaving it to the idle sweep. No-op once done.
this._self._slots.evictScanCursor(cursor.toString());

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 cursors owned by other scan commands

When options.cursor is a live token from BLESS SCAN (or another scan-chained command), this.scan correctly rejects it because routeScan checks the cursor label, but this unconditional cleanup then removes that token from the shared scanCursors map. The unrelated scan can no longer continue, so invalid regular-SCAN input corrupts another operation's state; only evict a token after this iterator has acquired or minted a SCAN-labelled cursor.

Useful? React with 👍 / 👎.

@nkaradzhov nkaradzhov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@nkaradzhov
nkaradzhov merged commit c589896 into redis:master Oct 23, 2026
15 checks passed
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.

scanIterator on Cluster

2 participants