🐛 fix(redis): 修复 Sentinel 切换数据库配置丢失

- 切换 Redis DB 时复用完整 Connect 逻辑,保留 Sentinel、TLS、SSH 等连接参数

- 补充 Sentinel 切 DB 与 Redis RPC 配置字段回归测试
This commit is contained in:
Syngnat
2026-06-12 03:42:12 +08:00
parent 233894f027
commit 03e08bec32
3 changed files with 102 additions and 39 deletions

View File

@@ -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',

View File

@@ -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

View File

@@ -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