From 03e08bec32eae0df544857fefea7ead0221f6bd9 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 12 Jun 2026 03:42:12 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(redis):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=20Sentinel=20=E5=88=87=E6=8D=A2=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=B8=A2=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 切换 Redis DB 时复用完整 Connect 逻辑,保留 Sentinel、TLS、SSH 等连接参数 - 补充 Sentinel 切 DB 与 Redis RPC 配置字段回归测试 --- .../src/utils/connectionRpcConfig.test.ts | 24 +++++++ internal/redis/redis_impl.go | 51 ++++---------- internal/redis/redis_impl_test.go | 66 +++++++++++++++++++ 3 files changed, 102 insertions(+), 39 deletions(-) diff --git a/frontend/src/utils/connectionRpcConfig.test.ts b/frontend/src/utils/connectionRpcConfig.test.ts index bbf69ad..1da959e 100644 --- a/frontend/src/utils/connectionRpcConfig.test.ts +++ b/frontend/src/utils/connectionRpcConfig.test.ts @@ -199,6 +199,30 @@ describe('buildRpcConnectionConfig', () => { }); }); + it('preserves Redis cluster and Sentinel topology fields for RPC calls', () => { + const result = buildRpcConnectionConfig({ + id: 'conn-redis-sentinel', + type: 'redis', + host: 'sentinel-a.local', + port: '26379' as unknown as number, + hosts: ['sentinel-b.local:26379', 'sentinel-c.local:26379'], + topology: 'sentinel', + user: 'default', + password: 'redis-secret', + redisSentinelMaster: 'mymaster', + redisSentinelUser: 'sentinel-user', + redisSentinelPassword: 'sentinel-secret', + redisDB: '3' as unknown as number, + } as any); + + expect(result.topology).toBe('sentinel'); + expect(result.hosts).toEqual(['sentinel-b.local:26379', 'sentinel-c.local:26379']); + expect(result.redisSentinelMaster).toBe('mymaster'); + expect(result.redisSentinelUser).toBe('sentinel-user'); + expect(result.redisSentinelPassword).toBe('sentinel-secret'); + expect(result.redisDB).toBe(3); + }); + it('returns a Wails connection model instance for RPC compatibility', () => { const result = buildRpcConnectionConfig({ id: 'conn-model', diff --git a/internal/redis/redis_impl.go b/internal/redis/redis_impl.go index b34d065..3d25f91 100644 --- a/internal/redis/redis_impl.go +++ b/internal/redis/redis_impl.go @@ -49,6 +49,10 @@ const ( redisSearchMaxDuration = 3 * time.Second ) +var redisDBSwitchConnect = func(client *RedisClientImpl, config connection.ConnectionConfig) error { + return client.Connect(config) +} + // NewRedisClient creates a new Redis client instance func NewRedisClient() RedisClient { return &RedisClientImpl{} @@ -1589,49 +1593,18 @@ func (r *RedisClientImpl) SelectDB(index int) error { return fmt.Errorf("数据库索引必须在 0-15 之间") } - // Create new client with different DB - addr := "" - if len(r.seedAddrs) > 0 { - addr = r.seedAddrs[0] - } - if r.forwarder != nil { - addr = r.forwarder.LocalAddr - } - if addr == "" { - addr = fmt.Sprintf("%s:%d", r.config.Host, r.config.Port) - } - - timeout := normalizeRedisTimeout(r.config.Timeout) - - opts := &redis.Options{ - Addr: addr, - Username: strings.TrimSpace(r.config.User), - Password: r.config.Password, - DB: index, - DialTimeout: timeout, - ReadTimeout: timeout, - WriteTimeout: timeout, - } - - newClient := redis.NewClient(opts) - - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - if err := newClient.Ping(ctx).Err(); err != nil { - newClient.Close() + nextConfig := r.config + nextConfig.RedisDB = index + nextClient := &RedisClientImpl{} + if err := redisDBSwitchConnect(nextClient, nextConfig); err != nil { return fmt.Errorf("切换数据库失败: %w", err) } - // Close old client and replace - if r.client != nil { - _ = r.client.Close() + oldClient := r.client + *r = *nextClient + if oldClient != nil { + _ = oldClient.Close() } - r.client = newClient - r.singleClient = newClient - r.clusterClient = nil - r.currentDB = index - r.config.RedisDB = index logger.Infof("Redis 切换到数据库: db%d", index) return nil diff --git a/internal/redis/redis_impl_test.go b/internal/redis/redis_impl_test.go index ecdcf58..7fd2bfa 100644 --- a/internal/redis/redis_impl_test.go +++ b/internal/redis/redis_impl_test.go @@ -9,6 +9,8 @@ import ( "sort" "strings" "testing" + + goredis "github.com/redis/go-redis/v9" ) // 回归保护:HGETALL 在 RESP3 下返回 map[interface{}]interface{}(go-redis v9 默认 RESP3), @@ -275,6 +277,70 @@ func TestRedisClusterKeepsSSHValidation(t *testing.T) { } } +func TestRedisSelectDBReconnectsWithSentinelConfig(t *testing.T) { + oldConnect := redisDBSwitchConnect + defer func() { + redisDBSwitchConnect = oldConnect + }() + + var captured connection.ConnectionConfig + redisDBSwitchConnect = func(client *RedisClientImpl, config connection.ConnectionConfig) error { + captured = config + next := goredis.NewClient(&goredis.Options{Addr: "127.0.0.1:0"}) + client.client = next + client.singleClient = next + client.config = config + client.currentDB = config.RedisDB + return nil + } + + oldClient := goredis.NewClient(&goredis.Options{Addr: "127.0.0.1:0"}) + client := &RedisClientImpl{ + client: oldClient, + singleClient: oldClient, + config: connection.ConnectionConfig{ + Type: "redis", + Host: "sentinel-a.local", + Port: 26379, + Hosts: []string{"sentinel-b.local:26379", "sentinel-c.local:26379"}, + Topology: "sentinel", + User: "data-user", + Password: "data-pass", + RedisSentinelMaster: "mymaster", + RedisSentinelUser: "sentinel-user", + RedisSentinelPassword: "sentinel-pass", + UseSSL: true, + SSLMode: "required", + RedisDB: 0, + }, + currentDB: 0, + } + defer client.Close() + + if err := client.SelectDB(6); err != nil { + t.Fatalf("SelectDB returned error: %v", err) + } + + if captured.RedisDB != 6 || client.currentDB != 6 { + t.Fatalf("expected RedisDB/currentDB=6, captured=%d current=%d", captured.RedisDB, client.currentDB) + } + if captured.Topology != "sentinel" { + t.Fatalf("expected sentinel topology to be preserved, got %q", captured.Topology) + } + if captured.RedisSentinelMaster != "mymaster" { + t.Fatalf("expected Sentinel master to be preserved, got %q", captured.RedisSentinelMaster) + } + if captured.RedisSentinelUser != "sentinel-user" || captured.RedisSentinelPassword != "sentinel-pass" { + t.Fatalf("expected Sentinel credentials to be preserved, got user=%q password=%q", captured.RedisSentinelUser, captured.RedisSentinelPassword) + } + if len(captured.Hosts) != 2 || captured.Hosts[0] != "sentinel-b.local:26379" || captured.Hosts[1] != "sentinel-c.local:26379" { + t.Fatalf("expected Sentinel hosts to be preserved, got %#v", captured.Hosts) + } + if !captured.UseSSL || captured.SSLMode != "required" { + t.Fatalf("expected TLS settings to be preserved, got useSSL=%v sslMode=%q", captured.UseSSL, captured.SSLMode) + } +} + func TestIsRedisKeyGone(t *testing.T) { tests := []struct { name string