mirror of
https://github.com/redis/go-redis.git
synced 2026-10-28 08:24:54 +08:00
* fix: do not close sentinel when replicas list is empty When a sentinel-monitored Redis master has no replicas (master-only setup), replicaAddrss() was calling closeSentinel() on receiving an empty replica list without error. This destroyed the sentinel connection (and its pub/sub failover subscription), forcing a full sentinel rediscovery on every subsequent operation — creating a continuous rediscovery loop that flooded logs and added latency. The empty-replica case is a valid, stable state for master-only deployments. The sentinel connection must be preserved for ongoing master discovery and failover monitoring via pub/sub. The fix returns early with an empty slice instead of tearing down the sentinel. A test is added to verify the sentinel survives repeated ReplicaAddrs calls when no replicas exist. Fixes: sentinel rediscovery loop on master-only clusters Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: preserve UseDisconnectedReplicas fallback path Address review feedback: the early return for zero replicas must only apply when useDisconnected=false (the normal path). When useDisconnected=true, fall through to the discovery loop which correctly passes useDisconnected to parseReplicaAddrs — since getReplicaAddrs hardcodes false, the cached sentinel path can never find disconnected replicas. Also remove unused SetSentinelForTest helper. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: use SENTINEL MONITOR to create a true zero-replica master Address review feedback: the previous test used sentinelName which has two replicas in the test environment, so getReplicaAddrs returned a non-empty list and the zero-replica code path was never exercised. Now the test registers a temporary master-only name via SENTINEL MONITOR (pointing to the existing master port but with a new name and no replicas), verifies that ReplicaAddrs returns empty without closing the sentinel, and cleans up via SENTINEL REMOVE. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: skip zero-replica sentinel test when infrastructure unavailable Add a connectivity check at the start of the test so it is skipped (not failed) in environments where sentinel processes are not running. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: use standalone Redis (no replicas) for zero-replica sentinel test Point SENTINEL MONITOR at the standalone Redis instance (redisPort) instead of sentinelMasterPort, which already has two replicas. This prevents Sentinel from discovering replicas via INFO REPLICATION and ensures the test reliably exercises the zero-replica code path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: re-trigger checks * ci: re-trigger checks (flaky PubSub ClientSetName and conn_state tests) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>
133 lines
3.1 KiB
Go
133 lines
3.1 KiB
Go
package redis
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
|
|
"github.com/redis/go-redis/v9/internal"
|
|
"github.com/redis/go-redis/v9/internal/hashtag"
|
|
"github.com/redis/go-redis/v9/internal/pool"
|
|
)
|
|
|
|
func (c *baseClient) Pool() pool.Pooler {
|
|
return c.connPool
|
|
}
|
|
|
|
func (c *PubSub) SetNetConn(netConn net.Conn) {
|
|
c.cn = pool.NewConn(netConn)
|
|
}
|
|
|
|
func (c *ClusterClient) LoadState(ctx context.Context) (*clusterState, error) {
|
|
// return c.state.Reload(ctx)
|
|
return c.loadState(ctx)
|
|
}
|
|
|
|
func (c *ClusterClient) SlotAddrs(ctx context.Context, slot int) []string {
|
|
state, err := c.state.Get(ctx)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
var addrs []string
|
|
for _, n := range state.slotNodes(slot) {
|
|
addrs = append(addrs, n.Client.getAddr())
|
|
}
|
|
return addrs
|
|
}
|
|
|
|
func (c *ClusterClient) Nodes(ctx context.Context, key string) ([]*clusterNode, error) {
|
|
state, err := c.state.Reload(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
slot := hashtag.Slot(key)
|
|
nodes := state.slotNodes(slot)
|
|
if len(nodes) != 2 {
|
|
return nil, fmt.Errorf("slot=%d does not have enough nodes: %v", slot, nodes)
|
|
}
|
|
return nodes, nil
|
|
}
|
|
|
|
func (c *ClusterClient) SwapNodes(ctx context.Context, key string) error {
|
|
nodes, err := c.Nodes(ctx, key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nodes[0], nodes[1] = nodes[1], nodes[0]
|
|
return nil
|
|
}
|
|
|
|
func (c *clusterState) IsConsistent(ctx context.Context) bool {
|
|
if len(c.Masters) < 3 {
|
|
return false
|
|
}
|
|
for _, master := range c.Masters {
|
|
s := master.Client.Info(ctx, "replication").Val()
|
|
if !strings.Contains(s, "role:master") {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if len(c.Slaves) < 3 {
|
|
return false
|
|
}
|
|
for _, slave := range c.Slaves {
|
|
s := slave.Client.Info(ctx, "replication").Val()
|
|
if !strings.Contains(s, "role:slave") {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func GetSlavesAddrByName(ctx context.Context, c *SentinelClient, name string) []string {
|
|
addrs, err := c.Replicas(ctx, name).Result()
|
|
if err != nil {
|
|
internal.Logger.Printf(ctx, "sentinel: Replicas name=%q failed: %s",
|
|
name, err)
|
|
return []string{}
|
|
}
|
|
return parseReplicaAddrs(addrs, false)
|
|
}
|
|
|
|
func (c *Ring) ShardByName(name string) *ringShard {
|
|
shard, _ := c.sharding.GetByName(name)
|
|
return shard
|
|
}
|
|
|
|
func (c *ModuleLoadexConfig) ToArgs() []interface{} {
|
|
return c.toArgs()
|
|
}
|
|
|
|
func ShouldRetry(err error, retryTimeout bool) bool {
|
|
return shouldRetry(err, retryTimeout)
|
|
}
|
|
|
|
// SentinelFailoverHasSentinel returns true if the sentinelFailover
|
|
// still holds a live sentinel connection. Used in tests to verify
|
|
// that replicaAddrs does not tear down the sentinel for master-only
|
|
// (zero-replica) setups.
|
|
func (c *sentinelFailover) HasSentinel() bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.sentinel != nil
|
|
}
|
|
|
|
// NewTestSentinelFailover creates a sentinelFailover with minimal
|
|
// options for unit testing. The caller must set c.sentinel manually.
|
|
func NewTestSentinelFailover(opt *FailoverOptions, sentinelAddrs []string) *sentinelFailover {
|
|
return &sentinelFailover{
|
|
opt: opt,
|
|
sentinelAddrs: sentinelAddrs,
|
|
}
|
|
}
|
|
|
|
// ReplicaAddrs is exported for testing.
|
|
func (c *sentinelFailover) ReplicaAddrs(ctx context.Context) ([]string, error) {
|
|
return c.replicaAddrs(ctx, false)
|
|
}
|