Skip to content

feat: add try_scan/try_for_each to the scan cursors - #482

Draft
memark wants to merge 1 commit into
masterfrom
memark-try-scan
Draft

feat: add try_scan/try_for_each to the scan cursors#482
memark wants to merge 1 commit into
masterfrom
memark-try-scan

Conversation

@memark

@memark memark commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Both KeysCursor and ScanKeyCursor could only scan to completion: the callback had no way to signal "stop". That gap is most visible on ScanKeyCursor, where a listpack-encoded value is walked entirely within a single RedisModule_ScanKey call, so the existing while cursor.scan(..) loop never gets a chance to break.

Add try_scan and try_for_each to both cursors. The callback returns ControlFlow<B>, and try_for_each is the analogue of Iterator::try_for_each -- it stops issuing scan calls as soon as the callback breaks and propagates the break value. KeysCursor also gains the for_each that ScanKeyCursor already had.

RedisModuleScanCB and RedisModuleScanKeyCB both return void, so Redis cannot be aborted mid-batch: it finishes the batch and advances the cursor past all of it. Breaking therefore stops the user callback, not Redis's iteration, and the rest of the current batch is skipped for good. Both try_scan doc comments spell this out, including how much gets skipped per encoding.

ScanKeyCursor::scan now delegates to try_scan, which removes its duplicate extern "C" callback along with the &f as *const F as *mut c_void cast it needed. KeysCursor::scan cannot do the same: it only holds a &F, and hoisting the &F -> &mut F cast out of the callback trips the deny-by-default invalid_reference_casting lint, so it keeps its own callback unchanged.

Covered by three new example commands and three integration tests, including one that returns exactly the first two pairs of a three-field listpack hash.

Claude-Session: https://claude.ai/code/session_01Rykj2WP9fzGjnseyrnVgBf

Both `KeysCursor` and `ScanKeyCursor` could only scan to completion: the
callback had no way to signal "stop". That gap is most visible on
`ScanKeyCursor`, where a listpack-encoded value is walked entirely within a
single `RedisModule_ScanKey` call, so the existing `while cursor.scan(..)`
loop never gets a chance to break.

Add `try_scan` and `try_for_each` to both cursors. The callback returns
`ControlFlow<B>`, and `try_for_each` is the analogue of
`Iterator::try_for_each` -- it stops issuing scan calls as soon as the
callback breaks and propagates the break value. `KeysCursor` also gains the
`for_each` that `ScanKeyCursor` already had.

`RedisModuleScanCB` and `RedisModuleScanKeyCB` both return `void`, so Redis
cannot be aborted mid-batch: it finishes the batch and advances the cursor
past all of it. Breaking therefore stops the user callback, not Redis's
iteration, and the rest of the current batch is skipped for good. Both
`try_scan` doc comments spell this out, including how much gets skipped per
encoding.

`ScanKeyCursor::scan` now delegates to `try_scan`, which removes its
duplicate `extern "C"` callback along with the `&f as *const F as *mut
c_void` cast it needed. `KeysCursor::scan` cannot do the same: it only holds
a `&F`, and hoisting the `&F -> &mut F` cast out of the callback trips the
deny-by-default `invalid_reference_casting` lint, so it keeps its own
callback unchanged.

Covered by three new example commands and three integration tests, including
one that returns exactly the first two pairs of a three-field listpack hash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rykj2WP9fzGjnseyrnVgBf
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.

1 participant