snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
123
internal/redisshard/init.go
Normal file
123
internal/redisshard/init.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package redisshard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func BuildRedisShards(redisConf configtypes.Redis) ([]*RedisShard, error) {
|
||||
redisShardConfigs, err := getRedisShardConfigs(redisConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
redisShards := make([]*RedisShard, 0, len(redisShardConfigs))
|
||||
|
||||
for _, redisCfg := range redisShardConfigs {
|
||||
redisShard, err := NewRedisShard(redisCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
redisShards = append(redisShards, redisShard)
|
||||
}
|
||||
return redisShards, nil
|
||||
}
|
||||
|
||||
func addRedisShardCommonSettings(shardConf *RedisShardConfig, redisConf configtypes.Redis) {
|
||||
shardConf.DB = redisConf.DB
|
||||
shardConf.User = redisConf.User
|
||||
shardConf.Password = redisConf.Password
|
||||
shardConf.ClientName = redisConf.ClientName
|
||||
if redisConf.TLS.Enabled {
|
||||
tlsConfig, err := redisConf.TLS.ToGoTLSConfig("redis")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error creating Redis TLS config")
|
||||
}
|
||||
shardConf.TLSConfig = tlsConfig
|
||||
}
|
||||
shardConf.ConnectTimeout = redisConf.ConnectTimeout.ToDuration()
|
||||
shardConf.IOTimeout = redisConf.IOTimeout.ToDuration()
|
||||
shardConf.ForceRESP2 = redisConf.ForceResp2
|
||||
shardConf.ReplicaClientEnabled = redisConf.ReplicaClient.Enabled
|
||||
}
|
||||
|
||||
func getRedisShardConfigs(redisConf configtypes.Redis) ([]RedisShardConfig, error) {
|
||||
var shardConfigs []RedisShardConfig
|
||||
|
||||
clusterShards := redisConf.ClusterAddress
|
||||
var useCluster bool
|
||||
if len(clusterShards) > 0 {
|
||||
useCluster = true
|
||||
}
|
||||
|
||||
if useCluster {
|
||||
for _, clusterAddress := range clusterShards {
|
||||
clusterAddresses := strings.Split(clusterAddress, ",")
|
||||
for _, address := range clusterAddresses {
|
||||
if _, _, err := net.SplitHostPort(address); err != nil {
|
||||
return nil, fmt.Errorf("malformed Redis Cluster address: %s", address)
|
||||
}
|
||||
}
|
||||
conf := &RedisShardConfig{
|
||||
ClusterAddresses: clusterAddresses,
|
||||
}
|
||||
addRedisShardCommonSettings(conf, redisConf)
|
||||
shardConfigs = append(shardConfigs, *conf)
|
||||
}
|
||||
return shardConfigs, nil
|
||||
}
|
||||
|
||||
sentinelShards := redisConf.SentinelAddress
|
||||
var useSentinel bool
|
||||
if len(sentinelShards) > 0 {
|
||||
useSentinel = true
|
||||
}
|
||||
|
||||
if useSentinel {
|
||||
for _, sentinelAddress := range sentinelShards {
|
||||
sentinelAddresses := strings.Split(sentinelAddress, ",")
|
||||
for _, address := range sentinelAddresses {
|
||||
if _, _, err := net.SplitHostPort(address); err != nil {
|
||||
return nil, fmt.Errorf("malformed Redis Sentinel address: %s", address)
|
||||
}
|
||||
}
|
||||
conf := &RedisShardConfig{
|
||||
SentinelAddresses: sentinelAddresses,
|
||||
}
|
||||
addRedisShardCommonSettings(conf, redisConf)
|
||||
conf.SentinelUser = redisConf.SentinelUser
|
||||
conf.SentinelPassword = redisConf.SentinelPassword
|
||||
conf.SentinelMasterName = redisConf.SentinelMasterName
|
||||
if conf.SentinelMasterName == "" {
|
||||
return nil, fmt.Errorf("master name must be set when using Redis Sentinel")
|
||||
}
|
||||
conf.SentinelClientName = redisConf.SentinelClientName
|
||||
if redisConf.SentinelTLS.Enabled {
|
||||
tlsConfig, err := redisConf.SentinelTLS.ToGoTLSConfig("redis_sentinel")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error creating Redis Sentinel TLS config")
|
||||
}
|
||||
conf.SentinelTLSConfig = tlsConfig
|
||||
}
|
||||
shardConfigs = append(shardConfigs, *conf)
|
||||
}
|
||||
return shardConfigs, nil
|
||||
}
|
||||
|
||||
redisAddresses := redisConf.Address
|
||||
if len(redisAddresses) == 0 {
|
||||
return nil, fmt.Errorf("no Redis address configured")
|
||||
}
|
||||
for _, redisAddress := range redisAddresses {
|
||||
conf := &RedisShardConfig{
|
||||
Address: redisAddress,
|
||||
}
|
||||
addRedisShardCommonSettings(conf, redisConf)
|
||||
shardConfigs = append(shardConfigs, *conf)
|
||||
}
|
||||
return shardConfigs, nil
|
||||
}
|
||||
50
internal/redisshard/init_test.go
Normal file
50
internal/redisshard/init_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package redisshard
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetRedisShardConfigsCommonSettings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
common := configtypes.Redis{
|
||||
DB: 2,
|
||||
User: "user",
|
||||
Password: "pass",
|
||||
ClientName: "centrifugo-consumer",
|
||||
ForceResp2: true,
|
||||
}
|
||||
|
||||
standalone := common
|
||||
standalone.Address = []string{"redis://127.0.0.1:6379"}
|
||||
|
||||
cluster := common
|
||||
cluster.ClusterAddress = []string{"127.0.0.1:7000,127.0.0.1:7001"}
|
||||
|
||||
sentinel := common
|
||||
sentinel.SentinelAddress = []string{"127.0.0.1:26379"}
|
||||
sentinel.SentinelMasterName = "mymaster"
|
||||
|
||||
for name, redisConf := range map[string]configtypes.Redis{
|
||||
"standalone": standalone,
|
||||
"cluster": cluster,
|
||||
"sentinel": sentinel,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
shardConfigs, err := getRedisShardConfigs(redisConf)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, shardConfigs, 1)
|
||||
conf := shardConfigs[0]
|
||||
require.Equal(t, 2, conf.DB)
|
||||
require.Equal(t, "user", conf.User)
|
||||
require.Equal(t, "pass", conf.Password)
|
||||
require.Equal(t, "centrifugo-consumer", conf.ClientName)
|
||||
require.True(t, conf.ForceRESP2)
|
||||
})
|
||||
}
|
||||
}
|
||||
507
internal/redisshard/shard.go
Normal file
507
internal/redisshard/shard.go
Normal file
@@ -0,0 +1,507 @@
|
||||
package redisshard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/redis/rueidis"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultRedisIOTimeout = 4 * time.Second
|
||||
defaultRedisConnectTimeout = time.Second
|
||||
|
||||
// sentinelTopologyRefreshInterval makes the Sentinel client periodically
|
||||
// re-check its targets with Sentinels instead of relying solely on the
|
||||
// +switch-master event. That event is the only way the client learns about
|
||||
// a failover, and it can be missed — a demoted master keeps its connection
|
||||
// alive and a role change sends no MOVED, so a client which missed the
|
||||
// event stays pinned to a node that is now a read-only replica until
|
||||
// restart. This is easy to hit with a Sentinel co-located with each Redis
|
||||
// node (as in the Bitnami Redis Helm chart), where the client loses its
|
||||
// Sentinel connection at the same moment the master dies and Redis PUB/SUB
|
||||
// does not replay the missed message.
|
||||
sentinelTopologyRefreshInterval = 5 * time.Second
|
||||
)
|
||||
|
||||
type RedisShard struct {
|
||||
config RedisShardConfig
|
||||
client rueidis.Client
|
||||
replicaClient rueidis.Client
|
||||
closeCh chan struct{}
|
||||
closeOnce sync.Once
|
||||
isCluster bool
|
||||
finalAddress []string
|
||||
}
|
||||
|
||||
// NewRedisShard initializes new Redis shard.
|
||||
func NewRedisShard(conf RedisShardConfig) (*RedisShard, error) {
|
||||
if conf.ConnectTimeout == 0 {
|
||||
conf.ConnectTimeout = defaultRedisConnectTimeout
|
||||
}
|
||||
if conf.IOTimeout == 0 {
|
||||
conf.IOTimeout = defaultRedisIOTimeout
|
||||
}
|
||||
options := rueidis.ClientOption{
|
||||
SelectDB: conf.DB,
|
||||
ConnWriteTimeout: conf.IOTimeout,
|
||||
TLSConfig: conf.TLSConfig,
|
||||
Username: conf.User,
|
||||
Password: conf.Password,
|
||||
ClientName: conf.ClientName,
|
||||
ShuffleInit: true,
|
||||
DisableCache: true,
|
||||
AlwaysPipelining: true,
|
||||
AlwaysRESP2: conf.ForceRESP2,
|
||||
MaxFlushDelay: 100 * time.Microsecond,
|
||||
// Redis commands are issued without per-request context deadlines
|
||||
// and rely on IOTimeout to bound every call. rueidis retries
|
||||
// read-only commands on network errors until the context is done —
|
||||
// which, without a deadline, means a read command issued during a
|
||||
// Redis outage never returns instead of failing within the IOTimeout
|
||||
// envelope like writes do. Disable client-level retries entirely:
|
||||
// errors are surfaced to callers, and reads and writes should fail
|
||||
// the same way. Same change as in centrifuge RedisShard.
|
||||
DisableRetry: true,
|
||||
Dialer: net.Dialer{
|
||||
Timeout: conf.ConnectTimeout,
|
||||
// KeepAlive doubles as the rueidis keepalive PING cadence — the
|
||||
// mechanism that detects a silently stalled connection (peer
|
||||
// stops replying while TCP stays open) and errors out every
|
||||
// pending command. Worst-case failure time for a command on a
|
||||
// stalled connection is 2*KeepAlive + IOTimeout: a straggler
|
||||
// reply can make one keepalive tick look active, only the next
|
||||
// tick sends a PING, and the PING waits a full IOTimeout. The
|
||||
// rueidis default of 1s gives 6s with the default 4s IOTimeout;
|
||||
// 400ms keeps the worst case within 5s (4.8s), since commands
|
||||
// are issued without per-request context deadlines. Same change
|
||||
// as in centrifuge RedisShard, where a regression test pins the
|
||||
// bound against a blackholed connection.
|
||||
KeepAlive: 400 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
|
||||
var isCluster bool
|
||||
var isSentinel bool
|
||||
replicaClientEnabled := conf.ReplicaClientEnabled
|
||||
|
||||
if len(conf.SentinelAddresses) > 0 {
|
||||
isSentinel = true
|
||||
options.InitAddress = conf.SentinelAddresses
|
||||
options.Sentinel = rueidis.SentinelOption{
|
||||
TLSConfig: conf.SentinelTLSConfig,
|
||||
MasterSet: conf.SentinelMasterName,
|
||||
Username: conf.SentinelUser,
|
||||
Password: conf.SentinelPassword,
|
||||
ClientName: conf.SentinelClientName,
|
||||
}
|
||||
} else if len(conf.ClusterAddresses) > 0 {
|
||||
isCluster = true
|
||||
options.InitAddress = conf.ClusterAddresses
|
||||
} else {
|
||||
addressOpts, err := optionsFromAddress(conf.Address, options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error processing Redis address: %v", err)
|
||||
}
|
||||
options, isCluster, isSentinel, replicaClientEnabled =
|
||||
addressOpts.ClientOption, addressOpts.IsCluster, addressOpts.IsSentinel, addressOpts.ReplicaClientEnabled
|
||||
}
|
||||
|
||||
if isSentinel {
|
||||
if options.Sentinel.MasterSet == "" {
|
||||
return nil, errors.New("sentinel master name must be configured for Redis Sentinel setup")
|
||||
}
|
||||
options.Sentinel.TopologyRefreshInterval = sentinelTopologyRefreshInterval
|
||||
}
|
||||
|
||||
client, err := rueidis.NewClient(options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating Redis client: %v", err)
|
||||
}
|
||||
if strings.Contains(strings.ToLower(fmt.Sprintf("%T", client)), "cluster") {
|
||||
// Cluster mode is not explicitly set but client is a cluster client – thus set isCluster to true.
|
||||
// This scenario covered with tests for our main integrations: see TestNewRedisShard.
|
||||
// Centrifuge need to know that it's working with Redis Cluster to construct proper keys.
|
||||
isCluster = true
|
||||
}
|
||||
|
||||
shard := &RedisShard{
|
||||
config: conf,
|
||||
isCluster: isCluster,
|
||||
closeCh: make(chan struct{}),
|
||||
finalAddress: options.InitAddress,
|
||||
}
|
||||
|
||||
shard.client = client
|
||||
|
||||
if replicaClientEnabled {
|
||||
if !isCluster && !isSentinel {
|
||||
return nil, errors.New("replica client may be initialized only in cluster and sentinel mode")
|
||||
}
|
||||
options.ReplicaOnly = true
|
||||
replicaClient, err := rueidis.NewClient(options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating Redis replica client: %w", err)
|
||||
}
|
||||
shard.replicaClient = replicaClient
|
||||
}
|
||||
|
||||
return shard, nil
|
||||
}
|
||||
|
||||
var knownRedisURLPrefixes = []string{
|
||||
"redis://",
|
||||
"rediss://",
|
||||
"redis+sentinel://",
|
||||
"rediss+sentinel://",
|
||||
"redis+cluster://",
|
||||
"unix://",
|
||||
"tcp://",
|
||||
}
|
||||
|
||||
type fromAddressOptions struct {
|
||||
ClientOption rueidis.ClientOption
|
||||
IsCluster bool
|
||||
IsSentinel bool
|
||||
ReplicaClientEnabled bool
|
||||
}
|
||||
|
||||
func optionsFromAddress(address string, options rueidis.ClientOption) (fromAddressOptions, error) {
|
||||
result := fromAddressOptions{ClientOption: options}
|
||||
|
||||
hasKnownURLPrefix := false
|
||||
for _, prefix := range knownRedisURLPrefixes {
|
||||
if strings.HasPrefix(address, prefix) {
|
||||
hasKnownURLPrefix = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasKnownURLPrefix {
|
||||
if host, port, err := net.SplitHostPort(address); err == nil && host != "" && port != "" {
|
||||
result.ClientOption.InitAddress = []string{address}
|
||||
return result, nil
|
||||
}
|
||||
return result, errors.New("malformed connection address, must be Redis URL or host:port")
|
||||
}
|
||||
|
||||
u, err := url.Parse(address)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("malformed connection address, not a valid URL: %w", err)
|
||||
}
|
||||
|
||||
var addresses []string
|
||||
|
||||
query := u.Query()
|
||||
|
||||
if query.Has("sentinel_master_name") {
|
||||
result.ClientOption.Sentinel.MasterSet = query.Get("sentinel_master_name")
|
||||
}
|
||||
|
||||
isCluster := u.Scheme == "redis+cluster"
|
||||
isSentinel := u.Scheme == "redis+sentinel" || u.Scheme == "rediss+sentinel" || result.ClientOption.Sentinel.MasterSet != ""
|
||||
if isSentinel && result.ClientOption.Sentinel.MasterSet == "" {
|
||||
return result, errors.New("sentinel master name must be configured for Redis Sentinel setup")
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "tcp", "redis", "redis+sentinel", "redis+cluster", "rediss", "rediss+sentinel":
|
||||
addresses = []string{u.Host}
|
||||
if u.Path != "" {
|
||||
db, err := strconv.Atoi(strings.TrimPrefix(u.Path, "/"))
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("can't parse Redis DB number from connection address: %s is not a number", u.Path)
|
||||
}
|
||||
result.ClientOption.SelectDB = db
|
||||
}
|
||||
if strings.HasPrefix(u.Scheme, "rediss") {
|
||||
if result.ClientOption.TLSConfig == nil {
|
||||
result.ClientOption.TLSConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
if isSentinel && result.ClientOption.Sentinel.TLSConfig == nil {
|
||||
result.ClientOption.Sentinel.TLSConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
}
|
||||
case "unix":
|
||||
addresses = []string{u.Path}
|
||||
result.ClientOption.DialFn = func(s string, d *net.Dialer, c *tls.Config) (net.Conn, error) {
|
||||
return d.Dial("unix", s)
|
||||
}
|
||||
}
|
||||
|
||||
if u.User != nil {
|
||||
if u.User.Username() != "" {
|
||||
result.ClientOption.Username = u.User.Username()
|
||||
}
|
||||
if pass, ok := u.User.Password(); ok {
|
||||
result.ClientOption.Password = pass
|
||||
}
|
||||
}
|
||||
|
||||
addresses = append(addresses, query["addr"]...)
|
||||
|
||||
if query.Has("connect_timeout") {
|
||||
to, err := time.ParseDuration(query.Get("connect_timeout"))
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("invalid connect timeout: %q", query.Get("connect_timeout"))
|
||||
}
|
||||
result.ClientOption.Dialer.Timeout = to
|
||||
}
|
||||
|
||||
if query.Has("io_timeout") {
|
||||
to, err := time.ParseDuration(query.Get("io_timeout"))
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("invalid io timeout: %q", query.Get("io_timeout"))
|
||||
}
|
||||
result.ClientOption.ConnWriteTimeout = to
|
||||
}
|
||||
|
||||
if query.Has("tls_enabled") && result.ClientOption.TLSConfig == nil {
|
||||
val, err := strconv.ParseBool(query.Get("tls_enabled"))
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("invalid tls_enabled value: %q", query.Get("tls_enabled"))
|
||||
}
|
||||
if val {
|
||||
result.ClientOption.TLSConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if query.Has("force_resp2") {
|
||||
val, err := strconv.ParseBool(query.Get("force_resp2"))
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("invalid force_resp2 value: %q", query.Get("force_resp2"))
|
||||
}
|
||||
result.ClientOption.AlwaysRESP2 = val
|
||||
}
|
||||
|
||||
if query.Has("sentinel_user") {
|
||||
result.ClientOption.Sentinel.Username = query.Get("sentinel_user")
|
||||
}
|
||||
|
||||
if query.Has("sentinel_password") {
|
||||
result.ClientOption.Sentinel.Password = query.Get("sentinel_password")
|
||||
}
|
||||
|
||||
if query.Has("sentinel_tls_enabled") && result.ClientOption.Sentinel.TLSConfig == nil {
|
||||
val, err := strconv.ParseBool(query.Get("sentinel_tls_enabled"))
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("invalid sentinel_tls_enabled value: %q", query.Get("sentinel_tls_enabled"))
|
||||
}
|
||||
if val {
|
||||
result.ClientOption.Sentinel.TLSConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if query.Has("replica_client_enabled") {
|
||||
val, err := strconv.ParseBool(query.Get("replica_client_enabled"))
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("invalid replica_client_enabled value: %q", query.Get("replica_client_enabled"))
|
||||
}
|
||||
result.ReplicaClientEnabled = val
|
||||
}
|
||||
|
||||
result.ClientOption.InitAddress = addresses
|
||||
result.IsCluster = isCluster
|
||||
result.IsSentinel = isSentinel
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RedisShardConfig contains Redis connection options.
|
||||
type RedisShardConfig struct {
|
||||
// Address is a Redis server connection address. Address can be:
|
||||
// - host:port
|
||||
// - tcp://[[[user]:password]@]host:port[/db][?option1=value1&optionN=valueN]
|
||||
// - redis://[[[user]:password]@]host:port[/db][?option1=value1&optionN=valueN]
|
||||
// - unix://[[[user]:password]@]path[?option1=value1&optionN=valueN]
|
||||
// It's also possible to use Address with redis+sentinel:// and redis+cluster://
|
||||
// schemes when connecting to Redis Sentinel and Redis Cluster respectively.
|
||||
// Examples:
|
||||
// - redis+sentinel://[[[user]:password]@]host:port?sentinel_master_name=mymaster
|
||||
// - redis+cluster://[[[user]:password]@]host:port[?addr=host2:port2&addr=host3:port3]
|
||||
// If you need to connect to Redis Cluster then you need to provide ClusterAddresses
|
||||
// or must use redis+cluster:// scheme in Address.
|
||||
// If you need to connect to Redis Sentinel then you need to provide SentinelAddresses
|
||||
// or must use redis+sentinel:// scheme in Address.
|
||||
// I.e. Centrifuge requires you to explicitly specify the type of Redis setup you want
|
||||
// to connect to.
|
||||
Address string
|
||||
// ClusterAddresses is a slice of seed cluster addresses to connect to.
|
||||
// Each address should be in form of host:port. If ClusterAddresses set then
|
||||
// RedisShardConfig.Address not used at all.
|
||||
ClusterAddresses []string
|
||||
// SentinelAddresses is a slice of Sentinel addresses. Each address should
|
||||
// be in form of host:port. If set then Redis address will be automatically
|
||||
// discovered from Sentinel. For Sentinel the name of the master instance
|
||||
// Sentinel monitors (SentinelMasterName) must be provided. If SentinelAddresses
|
||||
// set then RedisShardConfig.Address not used at all.
|
||||
SentinelAddresses []string
|
||||
|
||||
// SentinelMasterName is a name of Redis instance master Sentinel monitors.
|
||||
SentinelMasterName string
|
||||
// SentinelUser is a user for Sentinel ACL-based auth.
|
||||
SentinelUser string
|
||||
// SentinelPassword is a password for Sentinel. Works with Sentinel >= 5.0.1.
|
||||
SentinelPassword string
|
||||
// SentinelClientName is a client name for established connections to Sentinel.
|
||||
SentinelClientName string
|
||||
// SentinelTLSConfig is a TLS configuration for Sentinel connections.
|
||||
SentinelTLSConfig *tls.Config
|
||||
|
||||
// DB is Redis database number. If not set then database 0 used. Does not make sense in Redis Cluster case.
|
||||
DB int
|
||||
// User is a username for Redis ACL-based auth.
|
||||
User string
|
||||
// Password is password to use when connecting to Redis database.
|
||||
// If zero then password not used.
|
||||
Password string
|
||||
// ClientName for established connections. See https://redis.io/commands/client-setname/
|
||||
ClientName string
|
||||
// TLSConfig contains connection TLS configuration.
|
||||
TLSConfig *tls.Config
|
||||
|
||||
// ConnectTimeout is a timeout on connect operation.
|
||||
// By default, 1 second is used.
|
||||
ConnectTimeout time.Duration
|
||||
// IOTimeout is a timeout on Redis connection operations.
|
||||
// By default, 4 seconds is used.
|
||||
IOTimeout time.Duration
|
||||
|
||||
// ForceRESP2 if set to true forces using RESP2 protocol for communicating with Redis.
|
||||
// By default, Redis client tries to detect supported Redis protocol automatically
|
||||
// trying RESP3 first.
|
||||
ForceRESP2 bool
|
||||
|
||||
// ReplicaClientEnabled once set to true will initialize replica client for this shard.
|
||||
// Replica client can then be used for read-only operations from replica nodes in Redis
|
||||
// Cluster or Redis Sentinel setups (single Redis is not allowed). Replica client will
|
||||
// be initialized with the same options as the main client but with ReplicaOnly option
|
||||
// set to true.
|
||||
ReplicaClientEnabled bool
|
||||
}
|
||||
|
||||
func (s *RedisShard) Close() {
|
||||
s.closeOnce.Do(func() {
|
||||
close(s.closeCh)
|
||||
s.client.Close()
|
||||
if s.replicaClient != nil {
|
||||
s.replicaClient.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RedisShard) String() string {
|
||||
return strings.Join(s.finalAddress, ",")
|
||||
}
|
||||
|
||||
type Op func(client rueidis.Client) rueidis.RedisResult
|
||||
|
||||
// RunOp allows to run Op on selected shard.
|
||||
func (s *RedisShard) RunOp(op Op) rueidis.RedisResult {
|
||||
return op(s.client)
|
||||
}
|
||||
|
||||
// RunOpReplica allows to run Op on selected shard, possibly on replica.
|
||||
func (s *RedisShard) RunOpReplica(op Op, onReplica bool) rueidis.RedisResult {
|
||||
if onReplica && s.replicaClient != nil {
|
||||
return op(s.replicaClient)
|
||||
}
|
||||
return op(s.client)
|
||||
}
|
||||
|
||||
type MultiOp func(client rueidis.Client) []rueidis.RedisResult
|
||||
|
||||
// RunMulti allows executing multiple commands.
|
||||
func (s *RedisShard) RunMulti(op MultiOp) []rueidis.RedisResult {
|
||||
return op(s.client)
|
||||
}
|
||||
|
||||
// Publish sends data to a Redis PUB/SUB channel.
|
||||
// Uses SPUBLISH for cluster mode, PUBLISH otherwise.
|
||||
func (s *RedisShard) Publish(ctx context.Context, channel string, data []byte) error {
|
||||
var cmd rueidis.Completed
|
||||
if s.isCluster {
|
||||
cmd = s.client.B().Spublish().Channel(channel).Message(string(data)).Build()
|
||||
} else {
|
||||
cmd = s.client.B().Publish().Channel(channel).Message(string(data)).Build()
|
||||
}
|
||||
return s.client.Do(ctx, cmd).Error()
|
||||
}
|
||||
|
||||
// Subscribe listens on a Redis PUB/SUB channel and calls handler for each message.
|
||||
// Uses SSUBSCRIBE for cluster mode, SUBSCRIBE otherwise.
|
||||
// Blocks until ctx is cancelled. Handles reconnections automatically.
|
||||
func (s *RedisShard) Subscribe(ctx context.Context, channel string, handler func(data []byte)) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
err := s.runSubscribeLoop(ctx, channel, handler)
|
||||
if err != nil && ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
// Reconnect after a short delay.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(300 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RedisShard) runSubscribeLoop(ctx context.Context, channel string, handler func(data []byte)) error {
|
||||
conn, cancel := s.client.Dedicate()
|
||||
defer cancel()
|
||||
defer conn.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
var doneOnce sync.Once
|
||||
|
||||
wait := conn.SetPubSubHooks(rueidis.PubSubHooks{
|
||||
OnMessage: func(msg rueidis.PubSubMessage) {
|
||||
handler([]byte(msg.Message))
|
||||
},
|
||||
OnSubscription: func(ps rueidis.PubSubSubscription) {
|
||||
if s.isCluster && ps.Kind == "sunsubscribe" && ps.Channel == channel {
|
||||
doneOnce.Do(func() { close(done) })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
var err error
|
||||
if s.isCluster {
|
||||
err = conn.Do(ctx, conn.B().Ssubscribe().Channel(channel).Build()).Error()
|
||||
} else {
|
||||
err = conn.Do(ctx, conn.B().Subscribe().Channel(channel).Build()).Error()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-done:
|
||||
return errors.New("subscription lost due to slot migration")
|
||||
case err = <-wait:
|
||||
return err
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user