Skip to content

fix: enable Redis Cluster clients with a single seed - #337

Merged
iFurySt merged 3 commits into
AmoyLab:mainfrom
qiankunli:fix/redis-client-mode
Oct 20, 2026
Merged

iFurySt merged 3 commits into
AmoyLab:mainfrom
qiankunli:fix/redis-client-mode

Conversation

@qiankunli

@qiankunli qiankunli commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

When cluster_type=cluster is configured with only one seed address, Session, Notifier, and OAuth storage currently create a standalone Redis client. PING may succeed, but commands for keys owned by another node fail with MOVED.

Set UniversalOptions.IsClusterMode from the existing cluster_type value in each of the three constructors. The existing go-redis v9.9.0 dependency already supports this option.

The production change is three added lines. Three focused miniredis tests assert that each constructor returns a ClusterClient for a single seed. Existing configuration parsing, validation, client lifecycle, and other deployment modes remain unchanged.

Validation on the final implementation:

  • make lint passed.
  • Full make test with TEST_FLAGS='-v -race' passed.

The committed tests verify client selection; they do not claim full Redis Cluster compatibility for every storage operation.

Summary by Sourcery

Enable Redis cluster client selection consistently across session, notifier, and OAuth storage constructors.

Bug Fixes:

  • Ensure Session, Notifier, and OAuth Redis storage use cluster clients when Redis cluster mode is configured with a single seed address.

Enhancements:

  • Document that token revocation by client ID may be incomplete in Redis Cluster mode because SCAN does not cover all primary nodes.

Tests:

  • Add focused tests verifying that all three Redis constructors return cluster clients for single-seed cluster configurations.

With cluster_type=cluster and a single seed, Session, Notifier, and OAuth storage currently construct a standalone client. PING can succeed while key operations return MOVED.

Use one internal Redis client constructor that selects standalone, Sentinel failover, or Cluster explicitly from cluster_type. Preserve authentication, existing configuration keys, and the existing Cluster behavior of ignoring db. Close clients when startup connection checks fail and report the component and selected client type.

The constructor rejects unknown modes, missing addresses, multiple standalone addresses, and Sentinel without master_name. An omitted mode defaults to single; deployments relying on multiple addresses to implicitly select Cluster must explicitly configure cluster_type=cluster.

Validation:
- make lint passed (the existing target runs gofmt/goimports).
- Full make test with TEST_FLAGS='-v -race' passed, with the opt-in Cluster test enabled.
- A disposable three-primary Redis 7.2.5 Cluster passed the single-seed cross-shard test: a plain seed client receives MOVED, while Session registration/read/event delivery, Notifier delivery, and OAuth client CRUD succeed. Discovery also succeeds with an unreachable seed in the configured list.
- go build passed for cmd/mcp-gateway and cmd/apiserver.

The regular suite covers mode selection and all three single-seed constructors with miniredis. The real Cluster test is opt-in via UNLA_TEST_REDIS_CLUSTER_ADDR; execution instructions and compatibility behavior are in docs/redis-client-modes.md. Live Sentinel failover and OAuth all-shard token scans are outside this validation scope. No dependency upgrade or Redis key migration is required.
@sourcery-ai

sourcery-ai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

The PR fixes single-seed Redis Cluster handling by introducing one validated, explicit mode-aware client constructor and migrating Session, Notifier, and OAuth storage to it. It preserves existing credentials and configuration keys, closes clients on failed startup checks, adds targeted unit and optional real-Cluster coverage, and documents the resulting compatibility contract.

Sequence diagram for single-seed Redis Cluster initialization

sequenceDiagram
    participant Storage as Storage consumer
    participant Factory as redisclient.New
    participant Cluster as Redis Cluster client
    participant Redis as Redis Cluster

    Storage->>Factory: New(cluster, UniversalOptions)
    Factory->>Cluster: NewClusterClient(options.Cluster())
    Factory-->>Storage: UniversalClient
    Storage->>Cluster: Ping()
    Cluster->>Redis: Discover cluster topology
    Redis-->>Cluster: Slot ownership
    Cluster-->>Storage: Ping result
    Storage->>Cluster: Close() on failed Ping
Loading

Flow diagram for Redis mode validation

flowchart TD
    Start[redisclient.New] --> Parse[Parse and normalize addresses]
    Parse --> Valid{Mode and addresses valid?}
    Valid -->|No| Error[Return configuration error]
    Valid -->|Yes| Mode{Selected mode}
    Mode -->|single| Standalone[Create standalone client]
    Mode -->|sentinel| Failover[Create Sentinel failover client]
    Mode -->|cluster| Cluster[Create Cluster client]
    Failover --> Return[Return client to owner]
    Standalone --> Return
    Cluster --> Return
Loading

File-Level Changes

Change Details Files
Centralize Redis deployment-mode selection in a validated constructor that honors explicit configuration.
  • Default omitted mode to standalone and reject unknown or malformed configurations.
  • Parse comma/semicolon-separated addresses with whitespace filtering.
  • Construct standalone, Sentinel failover, or Cluster clients explicitly from the selected mode.
  • Preserve authentication and DB behavior while ignoring DB for Cluster mode.
internal/redisclient/client.go
internal/redisclient/client_test.go
docs/redis-client-modes.md
Route OAuth, session, and configuration-notifier storage through the shared Redis client constructor.
  • Replace per-component UniversalClient construction with shared mode-aware construction.
  • Ensure failed startup PING checks close newly created clients.
  • Add component-specific initialization errors and client-type logging where applicable.
internal/auth/storage/redis.go
internal/mcp/session/redis.go
internal/mcp/storage/notifier/redis.go
Add coverage for single-seed Cluster construction and real cross-shard behavior.
  • Verify all three consumers create ClusterClient instances when cluster mode has one seed.
  • Exercise session, notifier, and OAuth operations across Redis Cluster shards.
  • Validate MOVED behavior, topology discovery, and recovery when an initial seed is unreachable.
internal/auth/storage/redis_test.go
internal/mcp/session/redis_test.go
internal/mcp/storage/notifier/redis_test.go
internal/redisclient/cluster_integration_test.go
Document the explicit Redis client-mode contract and validation expectations.
  • Clarify that address count no longer infers deployment mode.
  • Document authentication, DB, compatibility, and opt-in integration-test behavior.
  • Provide commands for running the disposable multi-node Cluster verification.
docs/redis-client-modes.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 4 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="internal/auth/storage/redis.go" line_range="22" />
<code_context>
-	if clusterType != cnst.RedisClusterTypeCluster {
-		// can not set db in cluster mode
-		redisOptions.DB = db
+	client, err := redisclient.New(clusterType, redis.UniversalOptions{
+		Addrs:            []string{addr},
+		MasterName:       masterName,
</code_context>
<issue_to_address>
**Revoked tokens remain usable**

When OAuth storage is configured for Cluster mode and a client's tokens are stored on shards the scan does not visit, `DeleteTokensByClientID` scans for matching keys without covering every cluster primary, then reports successful cleanup while tokens on unscanned shards remain stored and usable.

Make token cleanup scan every cluster primary before reporting success.

Also at `internal/redisclient/client.go:55`.
</issue_to_address>

### Comment 2
<location path="internal/redisclient/client.go" line_range="20" />
<code_context>
+// +link=component://docs/redis-client-modes.md
+func New(mode string, options redis.UniversalOptions) (redis.UniversalClient, error) {
+	if mode == "" {
+		mode = cnst.RedisClusterTypeSingle
+	}
+	switch mode {
</code_context>
<issue_to_address>
**Helm chart cannot select Cluster**

When a Helm chart user configures Redis Cluster through the chart, `New` defaults an omitted mode to `single`, so Helm-generated Redis configs select standalone for a single Cluster seed and reject multiple seeds instead of using Cluster; shard operations fail or initialization stops.

Add `cluster_type` configuration and value/environment-variable plumbing for the Redis sections generated by the Helm chart.
</issue_to_address>

### Comment 3
<location path="internal/redisclient/client_test.go" line_range="104" />
<code_context>
+				require.Equal(t, tt.options.Username, c.Options().Username)
+				require.Equal(t, tt.options.Password, c.Options().Password)
+			} else {
+				c, ok := client.(*redis.Client)
+				require.True(t, ok, "single and sentinel modes must not create ClusterClient")
+				require.Equal(t, tt.db, c.Options().DB)
</code_context>
<issue_to_address>
**Sentinel failover regression passes**

When the Sentinel branch returns a standalone client instead of a failover client, `TestNewDeploymentMode` only requires a non-Cluster `*redis.Client` and checks fields a standalone client also has, so returning `redis.NewClient` for Sentinel mode still passes both Sentinel cases and leaves failover untested.

Assert the Sentinel failover configuration on the returned client, including the configured master name and Sentinel addresses.
</issue_to_address>

### Comment 4
<location path="internal/auth/storage/redis.go" line_range="22" />
<code_context>
-	if clusterType != cnst.RedisClusterTypeCluster {
-		// can not set db in cluster mode
-		redisOptions.DB = db
+	client, err := redisclient.New(clusterType, redis.UniversalOptions{
+		Addrs:            []string{addr},
+		MasterName:       masterName,
</code_context>
<issue_to_address>
**Selected client type is not logged**

When OAuth Redis storage initializes successfully, `NewRedisStorage` returns without logging the selected Redis client type, so operators cannot verify whether OAuth storage selected standalone, Sentinel, or Cluster at startup.

Log the selected Redis client type when initializing OAuth storage.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread internal/auth/storage/redis.go Outdated
Comment thread internal/redisclient/client.go Outdated
Comment thread internal/redisclient/client_test.go Outdated
Comment thread internal/auth/storage/redis.go Outdated
@qiankunli qiankunli changed the title fix: honor configured Redis deployment mode fix: enable Redis Cluster clients with a single seed Oct 20, 2026

@iFurySt iFurySt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Thanks!

@iFurySt
iFurySt merged commit de8ab0b into AmoyLab:main Oct 20, 2026
7 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.

2 participants