Skip to content

Conversation

@currantw
Copy link
Collaborator

@currantw currantw commented Oct 22, 2025

Implement Cluster Scan

Overview

Adds ScanAsync methods to the standalone and cluster clients.

Key Changes

  • Added ScanAsync to IGenericClusterCommands and implemented it in GlideClusterClient, which calls new BaseClient.ClusterScanCommand method.
  • Added ScanAsync to IGenericCommands (standalone client) and implemented it in GlideClusterClient, which calls the existing Glide SCAN command. Provides consistent scan interface for standalone and cluster clients.
  • Cluster scan is implemented with ClusterScanCursor, while both standalone and cluster scanning use ScanOptions, which supports match pattern, count, and type arguments. Refactor existing Request.GenericCommands.ScanAsync and KeysAsync method to use ScanOptions.
  • Added RequestClusterScanFfi and RemoveClusterScanCursorFfi methods to "FFI.methods.rs", as well as Add request_cluster_scan and remove_cluster_scan_cursor to "rust/src/lib.rs" that call into the existing core methods (implementation based on equivalent methods here).

Testing Coverage

  • Added new ScanTests for both standalone and cluster ScanAsync methods.
  • Updated CommandTests for change to internal Request.ScanAsync interface.

Breaking Changes

None.

@currantw currantw self-assigned this Oct 22, 2025
@currantw currantw force-pushed the currantw/cluster-scan branch from cf4bbf1 to ca014f5 Compare October 22, 2025 00:57
…n `Request.GenericCommands`.

Signed-off-by: currantw <[email protected]>
Signed-off-by: currantw <[email protected]>
@currantw currantw force-pushed the currantw/cluster-scan branch from ca014f5 to 05d8fb9 Compare October 22, 2025 01:09
.vs
.vscode
.kiro/
.amazonq/
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Unrelated addition. Supports Amazon Q memory bank.

/// * `args` and `arg_lengths` must be valid arrays of length `arg_count`
/// * `args` format: [b"MATCH", pattern, b"COUNT", count, b"TYPE", type] (all optional)
#[unsafe(no_mangle)]
pub unsafe extern "C-unwind" fn request_cluster_scan(
Copy link
Collaborator Author

@currantw currantw Oct 22, 2025

Choose a reason for hiding this comment

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

Don't have any experience with Rust. Implementation modelled on existing cluster scan methods here. Any feedback appreciated!

Comment on lines +478 to +494
let scan_state_cursor = if cursor_id == "0" {
redis::ScanStateRC::new()
} else {
match glide_core::cluster_scan_container::get_cluster_scan_cursor(cursor_id.clone()) {
Ok(existing_cursor) => existing_cursor,
Err(_error) => {
unsafe {
(core.failure_callback)(
callback_index,
format!("Invalid cursor ID: {}", cursor_id).as_ptr() as *const c_char,
RequestErrorType::Unspecified,
);
}
return;
}
}
};
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated to handle the case (raise an error) where an invalid cursor is provided, as described in this pull request.

Comment on lines +135 to +138
#region protected fields
protected Version? _serverVersion; // cached server version
protected static readonly Version DefaultServerVersion = new(8, 0, 0);
#endregion protected fields
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not new. Just moved from "private fields" region below (since these obviously are not private...)

=> Simple<bool>(RequestType.Move, [key.ToGlideString(), database.ToGlideString()]);

public static Cmd<object[], (long, ValkeyKey[])> ScanAsync(long cursor, ValkeyValue pattern = default, long pageSize = 0)
public static Cmd<object[], (string, ValkeyKey[])> ScanAsync(string cursor, ScanOptions? options = null)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated to use ScanOptions and for consistency with the cluster interface.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We can only change this API because the earlier C# release was a public preview right?


protected abstract Task<Version> GetServerVersionAsync();

protected async Task<(string cursor, ValkeyKey[] keys)> ClusterScanCommand(string cursor, string[] args)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I would have liked to put this in GlideClusterClient, since it is actually only applicable to the cluster client (obviously). But that would require changing the access modifier for _messageContainer, which I'm not sure is advisable? @jbrinkman / @alexr-bq any thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Would it be possible to give _messageContainer an internal access level? That way arbitrary user code that extends BaseClient doesn't have access to this method.

Signed-off-by: currantw <[email protected]>
Comment on lines +197 to +202
() => Assert.Equal(["SCAN", "0", "TYPE", "string"], Request.ScanAsync("0", new ScanOptions { Type = ValkeyType.String }).GetArgs()),
() => Assert.Equal(["SCAN", "5", "TYPE", "list"], Request.ScanAsync("5", new ScanOptions { Type = ValkeyType.List }).GetArgs()),
() => Assert.Equal(["SCAN", "0", "TYPE", "set"], Request.ScanAsync("0", new ScanOptions { Type = ValkeyType.Set }).GetArgs()),
() => Assert.Equal(["SCAN", "0", "TYPE", "zset"], Request.ScanAsync("0", new ScanOptions { Type = ValkeyType.SortedSet }).GetArgs()),
() => Assert.Equal(["SCAN", "0", "TYPE", "hash"], Request.ScanAsync("0", new ScanOptions { Type = ValkeyType.Hash }).GetArgs()),
() => Assert.Equal(["SCAN", "0", "TYPE", "stream"], Request.ScanAsync("0", new ScanOptions { Type = ValkeyType.Stream }).GetArgs()),
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added additional tests here for TYPE, which wasn't supported before.

/// </code>
/// </example>
/// <seealso href="https://valkey.io/commands/scan/">SCAN command</seealso>
Task<(string cursor, ValkeyKey[] keys)> ScanAsync(string cursor, ScanOptions? options = null);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Standalone ScanAsync added for consistency with other clients. There is still the existing KeysAsync, which matches the existing StackExchange.Redis interface, but that does not provide support for the filtering keys by type.

Signed-off-by: currantw <[email protected]>
@currantw currantw requested review from alexr-bq and Copilot October 23, 2025 17:23
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements cluster and standalone scan functionality for the Valkey GLIDE client. The changes introduce a consistent ScanAsync API across both client types while adding support for filtering by pattern, type, and count options.

Key Changes:

  • Added ScanAsync methods to IGenericCommands (standalone) and IGenericClusterCommands (cluster) interfaces with implementations in respective clients
  • Introduced ScanOptions class supporting match pattern, count, and type filtering, and ClusterScanCursor for cluster scan state management
  • Refactored internal Request.ScanAsync to use ScanOptions instead of individual parameters, updating cursor handling from long to string

Reviewed Changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/Valkey.Glide.UnitTests/CommandTests.cs Updated unit tests to use new ScanAsync signature with ScanOptions and string cursors
tests/Valkey.Glide.IntegrationTests/ScanTests.cs Added comprehensive integration tests for scan functionality with filtering options
sources/Valkey.Glide/abstract_APITypes/ClusterScanCursor.cs New cursor abstraction for cluster scan operations
sources/Valkey.Glide/Internals/Request.GenericCommands.cs Refactored scan command builder to accept ScanOptions and return string cursors
sources/Valkey.Glide/Internals/FFI.methods.cs Added FFI bindings for cluster scan operations
sources/Valkey.Glide/GlideClusterClient.cs Implemented ScanAsync for cluster client using new cluster scan command
sources/Valkey.Glide/GlideClient.cs Implemented ScanAsync for standalone client and updated KeysAsync to use new scan interface
sources/Valkey.Glide/Commands/Options/ScanOptions.cs New options class for scan configuration
sources/Valkey.Glide/Commands/IGenericCommands.cs Added ScanAsync method signature for standalone client
sources/Valkey.Glide/Commands/IGenericClusterCommands.cs Added ScanAsync method signature for cluster client
sources/Valkey.Glide/Commands/Constants/Constants.cs Added TYPE keyword constant
sources/Valkey.Glide/BaseClient.cs Added ClusterScanCommand implementation and relocated server version fields
rust/src/lib.rs Implemented Rust FFI functions for cluster scan operations

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@currantw currantw requested a review from jbrinkman October 23, 2025 17:23
@currantw currantw marked this pull request as ready for review October 23, 2025 17:47
@currantw currantw requested a review from a team as a code owner October 23, 2025 17:47
@yipin-chen yipin-chen requested a review from jduo October 24, 2025 20:04
do
{
(long nextCursor, ValkeyKey[] keys) = await Command(Request.ScanAsync(currentCursor, pattern, pageSize));
var options = new ScanOptions();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: let's allocate this outside the loop and reuse

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