From 00a499c6972fd65ad6cf4f9db000d09e61d9ef9c Mon Sep 17 00:00:00 2001 From: Kiril Kalchev Date: Tue, 5 Aug 2025 17:14:44 +0300 Subject: [PATCH 1/8] add support for redis cluster as backend --- backends/redis.go | 96 +++++++++++------------ config.yaml | 3 + config/backends.go | 39 ++++++--- config/config.go | 2 + config/configtest/sample_full_config.yaml | 3 + 5 files changed, 86 insertions(+), 57 deletions(-) diff --git a/backends/redis.go b/backends/redis.go index d7128555..8dfac89b 100644 --- a/backends/redis.go +++ b/backends/redis.go @@ -12,74 +12,74 @@ import ( log "github.com/sirupsen/logrus" ) -// RedisDB is an interface that helps us communicate with an instance of a -// Redis database. Its implementation is intended to use the "github.com/go-redis/redis" -// client -type RedisDB interface { - Get(ctx context.Context, key string) (string, error) - Put(ctx context.Context, key string, value string, ttlSeconds int) (bool, error) -} - -// RedisDBClient is a wrapper for the Redis client that implements -// the RedisDB interface -type RedisDBClient struct { - client *redis.Client -} - -// Get returns the value associated with the provided `key` parameter -func (db RedisDBClient) Get(ctx context.Context, key string) (string, error) { - return db.client.Get(ctx, key).Result() -} - -// Put will set 'key' to hold string 'value' if 'key' does not exist in the redis storage. -// When key already holds a value, no operation is performed. That's the reason this adapter -// uses the 'github.com/go-redis/redis's library SetNX. SetNX is short for "SET if Not eXists". -func (db RedisDBClient) Put(ctx context.Context, key, value string, ttlSeconds int) (bool, error) { - return db.client.SetNX(ctx, key, value, time.Duration(ttlSeconds)*time.Second).Result() -} - // RedisBackend when initialized will instantiate and configure the Redis client. It implements // the Backend interface. type RedisBackend struct { cfg config.Redis - client RedisDB + client redis.Cmdable // This interface is implemented by both redis.Client and redis.ClusterClient } // NewRedisBackend initializes the redis client and pings to make sure connection was successful func NewRedisBackend(cfg config.Redis, ctx context.Context) *RedisBackend { - constr := cfg.Host + ":" + strconv.Itoa(cfg.Port) + var client redis.Cmdable - options := &redis.Options{ - Addr: constr, - Password: cfg.Password, - DB: cfg.Db, - } + if cfg.Cluster.Enabled { + // Cluster mode + clusterOptions := &redis.ClusterOptions{ + Addrs: cfg.Cluster.Hosts, + Password: cfg.Password, + // Note: DB selection is not supported in cluster mode + } - if cfg.TLS.Enabled { - options = &redis.Options{ + if cfg.TLS.Enabled { + clusterOptions.TLSConfig = &tls.Config{ + InsecureSkipVerify: cfg.TLS.InsecureSkipVerify, + } + } + + clusterClient := redis.NewClusterClient(clusterOptions) + + // Test cluster connection + _, err := clusterClient.Ping(ctx).Result() + if err != nil { + log.Fatalf("Error creating Redis cluster backend: %v", err) + panic("RedisBackend failure. This shouldn't happen.") + } + + log.Infof("Connected to Redis cluster with hosts: %v", cfg.Cluster.Hosts) + client = clusterClient + } else { + // Single-node mode + constr := cfg.Host + ":" + strconv.Itoa(cfg.Port) + + options := &redis.Options{ Addr: constr, Password: cfg.Password, DB: cfg.Db, - TLSConfig: &tls.Config{ + } + + if cfg.TLS.Enabled { + options.TLSConfig = &tls.Config{ InsecureSkipVerify: cfg.TLS.InsecureSkipVerify, - }, + } } - } - redisClient := RedisDBClient{client: redis.NewClient(options)} + singleClient := redis.NewClient(options) - _, err := redisClient.client.Ping(ctx).Result() + // Test single-node connection + _, err := singleClient.Ping(ctx).Result() + if err != nil { + log.Fatalf("Error creating Redis backend: %v", err) + panic("RedisBackend failure. This shouldn't happen.") + } - if err != nil { - log.Fatalf("Error creating Redis backend: %v", err) - panic("RedisBackend failure. This shouldn't happen.") + log.Infof("Connected to Redis at %s:%d", cfg.Host, cfg.Port) + client = singleClient } - log.Infof("Connected to Redis at %s:%d", cfg.Host, cfg.Port) - return &RedisBackend{ cfg: cfg, - client: redisClient, + client: client, } } @@ -87,7 +87,7 @@ func NewRedisBackend(cfg config.Redis, ctx context.Context) *RedisBackend { // parameter and interprets its response. A `Nil` error reply of the Redis client means // the `key` does not exist. func (b *RedisBackend) Get(ctx context.Context, key string) (string, error) { - res, err := b.client.Get(ctx, key) + res, err := b.client.Get(ctx, key).Result() if err == redis.Nil { err = utils.NewPBCError(utils.KEY_NOT_FOUND) @@ -101,7 +101,7 @@ func (b *RedisBackend) Get(ctx context.Context, key string) (string, error) { // not being written because the `key` already holds a value, and a RecordExistsError is returned func (b *RedisBackend) Put(ctx context.Context, key string, value string, ttlSeconds int) error { - success, err := b.client.Put(ctx, key, value, ttlSeconds) + success, err := b.client.SetNX(ctx, key, value, time.Duration(ttlSeconds)*time.Second).Result() if err != nil && err != redis.Nil { return err } diff --git a/config.yaml b/config.yaml index 277cee95..4d7b3b57 100644 --- a/config.yaml +++ b/config.yaml @@ -34,6 +34,9 @@ backend: # tls: # enabled: false # insecure_skip_verify: false +# cluster: +# enabled: false +# hosts: ["redis-node1:6379", "redis-node2:6379", "redis-node3:6379"] # ignite: # scheme: "http" # host: "127.0.0.1" diff --git a/config/backends.go b/config/backends.go index 2f5c8694..05aec3f1 100644 --- a/config/backends.go +++ b/config/backends.go @@ -151,12 +151,18 @@ func (cfg *Memcache) validateAndLog() error { } type Redis struct { - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - Password string `mapstructure:"password"` - Db int `mapstructure:"db"` - ExpirationMinutes int `mapstructure:"expiration"` - TLS RedisTLS `mapstructure:"tls"` + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + Password string `mapstructure:"password"` + Db int `mapstructure:"db"` + ExpirationMinutes int `mapstructure:"expiration"` + TLS RedisTLS `mapstructure:"tls"` + Cluster RedisCluster `mapstructure:"cluster"` +} + +type RedisCluster struct { + Enabled bool `mapstructure:"enabled"` + Hosts []string `mapstructure:"hosts"` } type RedisTLS struct { @@ -165,9 +171,24 @@ type RedisTLS struct { } func (cfg *Redis) validateAndLog() error { - log.Infof("config.backend.redis.host: %s", cfg.Host) - log.Infof("config.backend.redis.port: %d", cfg.Port) - log.Infof("config.backend.redis.db: %d", cfg.Db) + if cfg.Cluster.Enabled { + // Cluster mode validation and logging + if len(cfg.Cluster.Hosts) == 0 { + return errors.New("Redis cluster is enabled but no hosts are provided") + } + log.Infof("config.backend.redis.cluster.enabled: %t", cfg.Cluster.Enabled) + log.Infof("config.backend.redis.cluster.hosts: %v", cfg.Cluster.Hosts) + if cfg.Db != 0 { + log.Warnf("config.backend.redis.db: %d. Note that database selection is not supported in Redis cluster mode and will be ignored", cfg.Db) + } + } else { + // Single-node mode validation and logging + log.Infof("config.backend.redis.host: %s", cfg.Host) + log.Infof("config.backend.redis.port: %d", cfg.Port) + log.Infof("config.backend.redis.db: %d", cfg.Db) + } + + // Common configuration logging if cfg.ExpirationMinutes > 0 { log.Infof("config.backend.redis.expiration: %d. Note that this configuration option is being deprecated in favor of config.request_limits.max_ttl_seconds", cfg.ExpirationMinutes) } diff --git a/config/config.go b/config/config.go index 6e714a7e..1b3782de 100644 --- a/config/config.go +++ b/config/config.go @@ -71,6 +71,8 @@ func setConfigDefaults(v *viper.Viper) { v.SetDefault("backend.redis.expiration", utils.REDIS_DEFAULT_EXPIRATION_MINUTES) v.SetDefault("backend.redis.tls.enabled", false) v.SetDefault("backend.redis.tls.insecure_skip_verify", false) + v.SetDefault("backend.redis.cluster.enabled", false) + v.SetDefault("backend.redis.cluster.hosts", []string{}) v.SetDefault("backend.ignite.scheme", "") v.SetDefault("backend.ignite.host", "") v.SetDefault("backend.ignite.port", 0) diff --git a/config/configtest/sample_full_config.yaml b/config/configtest/sample_full_config.yaml index 23854c9f..d1d7fd66 100644 --- a/config/configtest/sample_full_config.yaml +++ b/config/configtest/sample_full_config.yaml @@ -38,6 +38,9 @@ backend: tls: enabled: false insecure_skip_verify: false + cluster: + enabled: false + hosts: ["redis-node1:6379", "redis-node2:6379", "redis-node3:6379"] ignite: scheme: "http" host: "127.0.0.1" From ed5ab434c4715062c62131b11af2b1574942af15 Mon Sep 17 00:00:00 2001 From: Kiril Kalchev Date: Tue, 5 Aug 2025 17:37:48 +0300 Subject: [PATCH 2/8] fix config tests --- config/config.go | 1 - config/config_test.go | 8 ++++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index 1b3782de..fbe3a368 100644 --- a/config/config.go +++ b/config/config.go @@ -72,7 +72,6 @@ func setConfigDefaults(v *viper.Viper) { v.SetDefault("backend.redis.tls.enabled", false) v.SetDefault("backend.redis.tls.insecure_skip_verify", false) v.SetDefault("backend.redis.cluster.enabled", false) - v.SetDefault("backend.redis.cluster.hosts", []string{}) v.SetDefault("backend.ignite.scheme", "") v.SetDefault("backend.ignite.host", "") v.SetDefault("backend.ignite.port", 0) diff --git a/config/config_test.go b/config/config_test.go index 7e1a5477..e554072e 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1300,6 +1300,10 @@ func getExpectedDefaultConfig() Configuration { }, Redis: Redis{ ExpirationMinutes: utils.REDIS_DEFAULT_EXPIRATION_MINUTES, + Cluster: RedisCluster{ + Enabled: false, + Hosts: nil, + }, }, Ignite: Ignite{ Headers: map[string]string{}, @@ -1378,6 +1382,10 @@ func getExpectedFullConfigForTestFile() Configuration { Enabled: false, InsecureSkipVerify: false, }, + Cluster: RedisCluster{ + Enabled: false, + Hosts: []string{"redis-node1:6379", "redis-node2:6379", "redis-node3:6379"}, + }, }, Ignite: Ignite{ Scheme: "http", From 3a54234971f88303505e1a534b883a3b17961868 Mon Sep 17 00:00:00 2001 From: Kiril Kalchev Date: Tue, 5 Aug 2025 18:06:57 +0300 Subject: [PATCH 3/8] finalize redis cluster implementation --- backends/redis.go | 54 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/backends/redis.go b/backends/redis.go index 8dfac89b..a0296627 100644 --- a/backends/redis.go +++ b/backends/redis.go @@ -12,16 +12,56 @@ import ( log "github.com/sirupsen/logrus" ) +// RedisDB is an interface that helps us communicate with an instance of a +// Redis database. Its implementation is intended to use the "github.com/go-redis/redis" +// client +type RedisDB interface { + Get(ctx context.Context, key string) (string, error) + Put(ctx context.Context, key string, value string, ttlSeconds int) (bool, error) +} + +// RedisDBClient is a wrapper for the Redis client that implements +// the RedisDB interface. It can work with both standalone and cluster clients. +type RedisDBClient struct { + client interface{} // Can be either *redis.Client or *redis.ClusterClient +} + +// Get returns the value associated with the provided `key` parameter +func (db RedisDBClient) Get(ctx context.Context, key string) (string, error) { + switch c := db.client.(type) { + case *redis.Client: + return c.Get(ctx, key).Result() + case *redis.ClusterClient: + return c.Get(ctx, key).Result() + default: + return "", utils.NewPBCError(utils.KEY_NOT_FOUND) + } +} + +// Put will set 'key' to hold string 'value' if 'key' does not exist in the redis storage. +// When key already holds a value, no operation is performed. That's the reason this adapter +// uses the 'github.com/go-redis/redis's library SetNX. SetNX is short for "SET if Not eXists". +func (db RedisDBClient) Put(ctx context.Context, key, value string, ttlSeconds int) (bool, error) { + switch c := db.client.(type) { + case *redis.Client: + return c.SetNX(ctx, key, value, time.Duration(ttlSeconds)*time.Second).Result() + case *redis.ClusterClient: + return c.SetNX(ctx, key, value, time.Duration(ttlSeconds)*time.Second).Result() + default: + return false, utils.NewPBCError(utils.KEY_NOT_FOUND) + } +} + // RedisBackend when initialized will instantiate and configure the Redis client. It implements // the Backend interface. type RedisBackend struct { cfg config.Redis - client redis.Cmdable // This interface is implemented by both redis.Client and redis.ClusterClient + client RedisDB } // NewRedisBackend initializes the redis client and pings to make sure connection was successful func NewRedisBackend(cfg config.Redis, ctx context.Context) *RedisBackend { - var client redis.Cmdable + var redisClient RedisDBClient if cfg.Cluster.Enabled { // Cluster mode @@ -47,7 +87,7 @@ func NewRedisBackend(cfg config.Redis, ctx context.Context) *RedisBackend { } log.Infof("Connected to Redis cluster with hosts: %v", cfg.Cluster.Hosts) - client = clusterClient + redisClient = RedisDBClient{client: clusterClient} } else { // Single-node mode constr := cfg.Host + ":" + strconv.Itoa(cfg.Port) @@ -74,12 +114,12 @@ func NewRedisBackend(cfg config.Redis, ctx context.Context) *RedisBackend { } log.Infof("Connected to Redis at %s:%d", cfg.Host, cfg.Port) - client = singleClient + redisClient = RedisDBClient{client: singleClient} } return &RedisBackend{ cfg: cfg, - client: client, + client: redisClient, } } @@ -87,7 +127,7 @@ func NewRedisBackend(cfg config.Redis, ctx context.Context) *RedisBackend { // parameter and interprets its response. A `Nil` error reply of the Redis client means // the `key` does not exist. func (b *RedisBackend) Get(ctx context.Context, key string) (string, error) { - res, err := b.client.Get(ctx, key).Result() + res, err := b.client.Get(ctx, key) if err == redis.Nil { err = utils.NewPBCError(utils.KEY_NOT_FOUND) @@ -101,7 +141,7 @@ func (b *RedisBackend) Get(ctx context.Context, key string) (string, error) { // not being written because the `key` already holds a value, and a RecordExistsError is returned func (b *RedisBackend) Put(ctx context.Context, key string, value string, ttlSeconds int) error { - success, err := b.client.SetNX(ctx, key, value, time.Duration(ttlSeconds)*time.Second).Result() + success, err := b.client.Put(ctx, key, value, ttlSeconds) if err != nil && err != redis.Nil { return err } From a7b2421b7ed8e9b4443e5141418155c42a1f8199 Mon Sep 17 00:00:00 2001 From: Kiril Kalchev Date: Tue, 5 Aug 2025 18:33:17 +0300 Subject: [PATCH 4/8] fix redis hosts parsing --- config/config.go | 1 + config/config_test.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index fbe3a368..1b3782de 100644 --- a/config/config.go +++ b/config/config.go @@ -72,6 +72,7 @@ func setConfigDefaults(v *viper.Viper) { v.SetDefault("backend.redis.tls.enabled", false) v.SetDefault("backend.redis.tls.insecure_skip_verify", false) v.SetDefault("backend.redis.cluster.enabled", false) + v.SetDefault("backend.redis.cluster.hosts", []string{}) v.SetDefault("backend.ignite.scheme", "") v.SetDefault("backend.ignite.host", "") v.SetDefault("backend.ignite.port", 0) diff --git a/config/config_test.go b/config/config_test.go index e554072e..50fee980 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1302,7 +1302,7 @@ func getExpectedDefaultConfig() Configuration { ExpirationMinutes: utils.REDIS_DEFAULT_EXPIRATION_MINUTES, Cluster: RedisCluster{ Enabled: false, - Hosts: nil, + Hosts: []string{}, }, }, Ignite: Ignite{ From 379b4ab1b8f0dc7e253c18f10f4fb036599da4c4 Mon Sep 17 00:00:00 2001 From: Kiril Kalchev Date: Tue, 5 Aug 2025 18:58:53 +0300 Subject: [PATCH 5/8] update go redis library --- backends/redis.go | 2 +- backends/redis_test.go | 2 +- endpoints/put_test.go | 2 +- go.mod | 4 ++-- go.sum | 17 ++++++++--------- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/backends/redis.go b/backends/redis.go index a0296627..4b0d8f10 100644 --- a/backends/redis.go +++ b/backends/redis.go @@ -6,7 +6,7 @@ import ( "strconv" "time" - "github.com/go-redis/redis/v8" + "github.com/redis/go-redis/v9" "github.com/prebid/prebid-cache/config" "github.com/prebid/prebid-cache/utils" log "github.com/sirupsen/logrus" diff --git a/backends/redis_test.go b/backends/redis_test.go index 7238ae5c..61b721a5 100644 --- a/backends/redis_test.go +++ b/backends/redis_test.go @@ -5,7 +5,7 @@ import ( "errors" "testing" - "github.com/go-redis/redis/v8" + "github.com/redis/go-redis/v9" "github.com/prebid/prebid-cache/utils" "github.com/stretchr/testify/assert" ) diff --git a/endpoints/put_test.go b/endpoints/put_test.go index 428c052c..d394f894 100644 --- a/endpoints/put_test.go +++ b/endpoints/put_test.go @@ -15,7 +15,7 @@ import ( "testing" "time" - "github.com/go-redis/redis/v8" + "github.com/redis/go-redis/v9" "github.com/gofrs/uuid" "github.com/julienschmidt/httprouter" "github.com/prebid/prebid-cache/backends" diff --git a/go.mod b/go.mod index cd8fdb74..5d2c3710 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.19 require ( github.com/aerospike/aerospike-client-go/v6 v6.7.0 github.com/didip/tollbooth/v6 v6.1.2 - github.com/go-redis/redis/v8 v8.11.5 github.com/gocql/gocql v1.0.0 github.com/gofrs/uuid v4.2.0+incompatible github.com/golang/snappy v0.0.4 @@ -14,6 +13,7 @@ require ( github.com/prometheus/client_golang v1.12.2 github.com/prometheus/client_model v0.2.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 + github.com/redis/go-redis/v9 v9.12.0 github.com/rs/cors v1.11.0 github.com/sirupsen/logrus v1.6.0 github.com/spf13/viper v1.11.0 @@ -24,7 +24,7 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bitly/go-hostpool v0.1.0 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect diff --git a/go.sum b/go.sum index b241657e..1ad3552c 100644 --- a/go.sum +++ b/go.sum @@ -54,10 +54,13 @@ github.com/bitly/go-hostpool v0.1.0 h1:XKmsF6k5el6xHG3WPJ8U0Ku/ye7njX7W81Ng7O2io github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -93,8 +96,6 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-pkgz/expirable-cache v0.0.3 h1:rTh6qNPp78z0bQE6HDhXBHUwqnV9i09Vm6dksJLXQDc= github.com/go-pkgz/expirable-cache v0.0.3/go.mod h1:+IauqN00R2FqNRLCLA+X5YljQJrwB179PfiAoMPlTlQ= -github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= -github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gocql/gocql v1.0.0 h1:UnbTERpP72VZ/viKE1Q1gPtmLvyTZTvuAstvSRydw/c= @@ -221,12 +222,12 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= -github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDSb3jwDggjz3Z0= @@ -263,9 +264,9 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/redis/go-redis/v9 v9.12.0 h1:XlVPGlflh4nxfhsNXPA8Qp6EmEfTo0rp8oaBzPipXnU= +github.com/redis/go-redis/v9 v9.12.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= -github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -631,8 +632,6 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= From 97a63a48f7bf8c5df07df95613a537f251095522 Mon Sep 17 00:00:00 2001 From: Kiril Kalchev Date: Tue, 5 Aug 2025 20:02:34 +0300 Subject: [PATCH 6/8] add some redis performance configurations --- backends/redis.go | 108 +++++++++++++++++++++ config.yaml | 40 +++++++- config/backends.go | 109 ++++++++++++++++++++-- config/configtest/sample_full_config.yaml | 2 +- 4 files changed, 248 insertions(+), 11 deletions(-) diff --git a/backends/redis.go b/backends/redis.go index 4b0d8f10..1bd14ab5 100644 --- a/backends/redis.go +++ b/backends/redis.go @@ -71,6 +71,9 @@ func NewRedisBackend(cfg config.Redis, ctx context.Context) *RedisBackend { // Note: DB selection is not supported in cluster mode } + // Apply performance configuration for cluster + applyClusterPerformanceOptions(clusterOptions, cfg) + if cfg.TLS.Enabled { clusterOptions.TLSConfig = &tls.Config{ InsecureSkipVerify: cfg.TLS.InsecureSkipVerify, @@ -98,6 +101,9 @@ func NewRedisBackend(cfg config.Redis, ctx context.Context) *RedisBackend { DB: cfg.Db, } + // Apply performance configuration for single-node + applySingleNodePerformanceOptions(options, cfg) + if cfg.TLS.Enabled { options.TLSConfig = &tls.Config{ InsecureSkipVerify: cfg.TLS.InsecureSkipVerify, @@ -123,6 +129,108 @@ func NewRedisBackend(cfg config.Redis, ctx context.Context) *RedisBackend { } } +// applySingleNodePerformanceOptions applies performance tuning options to single-node Redis client +func applySingleNodePerformanceOptions(options *redis.Options, cfg config.Redis) { + // Apply pool configuration + if cfg.Pool != nil { + if cfg.Pool.Size > 0 { + options.PoolSize = cfg.Pool.Size + } + if cfg.Pool.Timeout > 0 { + options.PoolTimeout = cfg.Pool.Timeout + } + if cfg.Pool.MinIdleConns > 0 { + options.MinIdleConns = cfg.Pool.MinIdleConns + } + if cfg.Pool.MaxIdleConns > 0 { + options.MaxIdleConns = cfg.Pool.MaxIdleConns + } + if cfg.Pool.ConnMaxIdleTime > 0 { + options.ConnMaxIdleTime = cfg.Pool.ConnMaxIdleTime + } + if cfg.Pool.ConnMaxLifetime > 0 { + options.ConnMaxLifetime = cfg.Pool.ConnMaxLifetime + } + } + + // Apply timeout configuration + if cfg.Timeouts != nil { + if cfg.Timeouts.DialTimeout > 0 { + options.DialTimeout = cfg.Timeouts.DialTimeout + } + if cfg.Timeouts.ReadTimeout > 0 { + options.ReadTimeout = cfg.Timeouts.ReadTimeout + } + if cfg.Timeouts.WriteTimeout > 0 { + options.WriteTimeout = cfg.Timeouts.WriteTimeout + } + } + + // Apply retry configuration + if cfg.Retry != nil { + if cfg.Retry.MaxRetries >= 0 { + options.MaxRetries = cfg.Retry.MaxRetries + } + if cfg.Retry.MinRetryBackoff > 0 { + options.MinRetryBackoff = cfg.Retry.MinRetryBackoff + } + if cfg.Retry.MaxRetryBackoff > 0 { + options.MaxRetryBackoff = cfg.Retry.MaxRetryBackoff + } + } +} + +// applyClusterPerformanceOptions applies performance tuning options to cluster Redis client +func applyClusterPerformanceOptions(options *redis.ClusterOptions, cfg config.Redis) { + // Apply pool configuration + if cfg.Pool != nil { + if cfg.Pool.Size > 0 { + options.PoolSize = cfg.Pool.Size + } + if cfg.Pool.Timeout > 0 { + options.PoolTimeout = cfg.Pool.Timeout + } + if cfg.Pool.MinIdleConns > 0 { + options.MinIdleConns = cfg.Pool.MinIdleConns + } + if cfg.Pool.MaxIdleConns > 0 { + options.MaxIdleConns = cfg.Pool.MaxIdleConns + } + if cfg.Pool.ConnMaxIdleTime > 0 { + options.ConnMaxIdleTime = cfg.Pool.ConnMaxIdleTime + } + if cfg.Pool.ConnMaxLifetime > 0 { + options.ConnMaxLifetime = cfg.Pool.ConnMaxLifetime + } + } + + // Apply timeout configuration + if cfg.Timeouts != nil { + if cfg.Timeouts.DialTimeout > 0 { + options.DialTimeout = cfg.Timeouts.DialTimeout + } + if cfg.Timeouts.ReadTimeout > 0 { + options.ReadTimeout = cfg.Timeouts.ReadTimeout + } + if cfg.Timeouts.WriteTimeout > 0 { + options.WriteTimeout = cfg.Timeouts.WriteTimeout + } + } + + // Apply retry configuration + if cfg.Retry != nil { + if cfg.Retry.MaxRetries >= 0 { + options.MaxRetries = cfg.Retry.MaxRetries + } + if cfg.Retry.MinRetryBackoff > 0 { + options.MinRetryBackoff = cfg.Retry.MinRetryBackoff + } + if cfg.Retry.MaxRetryBackoff > 0 { + options.MaxRetryBackoff = cfg.Retry.MaxRetryBackoff + } + } +} + // Get calls the Redis client to return the value associated with the provided `key` // parameter and interprets its response. A `Nil` error reply of the Redis client means // the `key` does not exist. diff --git a/config.yaml b/config.yaml index 4d7b3b57..6b0c2e6d 100644 --- a/config.yaml +++ b/config.yaml @@ -13,7 +13,41 @@ request_limits: request_logging: referer_sampling_rate: 0.0 backend: - type: "memory" # Switch to be "aerospike", "cassandra", "memcache", "ignite" or "redis" for production. + type: memory + + # Redis backend configuration (when type: redis) + redis: + host: localhost + port: 6379 + password: "" + db: 0 + + # Optional: Redis performance tuning (uncomment to use) + # pool: + # size: 100 # Connection pool size (0 = default: 10 * GOMAXPROCS) + # timeout: 5s # Max time to wait for connection from pool + # min_idle_conns: 10 # Minimum idle connections to maintain + # max_idle_conns: 20 # Maximum idle connections + # conn_max_idle_time: 30m # Close connections after remaining idle + # conn_max_lifetime: 60m # Close connections after this lifetime + + # timeouts: + # dial_timeout: 5s # Timeout for establishing connections + # read_timeout: 3s # Timeout for socket reads + # write_timeout: 3s # Timeout for socket writes + + # retry: + # max_retries: 3 # Maximum number of retries + # min_retry_backoff: 8ms # Minimum backoff between retries + # max_retry_backoff: 512ms # Maximum backoff between retries + + tls: + enabled: false + insecure_skip_verify: false + + cluster: + enabled: false + hosts: [] # aerospike: # hosts: [ "aerospike.prebid.com" ] # port: 3000 @@ -24,7 +58,7 @@ backend: # memcache: # config_host: "" # Configuration endpoint for auto discovery. Replaced at docker build. # poll_interval_seconds: 30 # Node change polling interval when auto discovery is used -# hosts: "10.0.0.1:11211" # List of nodes when not using auto discovery. Can also use an array for multiple hosts. +# hosts: "10.0.0.1:11211" # List of nodes when not using auto discovery. Can also use an array for multiple hosts. # redis: # host: "127.0.0.1" # port: 6379 @@ -50,7 +84,7 @@ backend: compression: type: "snappy" # Can also be "none" metrics: - type: "none" # Can also be "influx" + type: "none" # Can also be "influx" influx: host: "http://influx.prebid.com" database: "some-database" diff --git a/config/backends.go b/config/backends.go index 05aec3f1..bac69b65 100644 --- a/config/backends.go +++ b/config/backends.go @@ -3,6 +3,7 @@ package config import ( "errors" "fmt" + "time" log "github.com/sirupsen/logrus" ) @@ -151,13 +152,37 @@ func (cfg *Memcache) validateAndLog() error { } type Redis struct { - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - Password string `mapstructure:"password"` - Db int `mapstructure:"db"` - ExpirationMinutes int `mapstructure:"expiration"` - TLS RedisTLS `mapstructure:"tls"` - Cluster RedisCluster `mapstructure:"cluster"` + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + Password string `mapstructure:"password"` + Db int `mapstructure:"db"` + ExpirationMinutes int `mapstructure:"expiration"` + TLS RedisTLS `mapstructure:"tls"` + Cluster RedisCluster `mapstructure:"cluster"` + Pool *RedisPool `mapstructure:"pool"` + Timeouts *RedisTimeouts `mapstructure:"timeouts"` + Retry *RedisRetry `mapstructure:"retry"` +} + +type RedisPool struct { + Size int `mapstructure:"size"` + Timeout time.Duration `mapstructure:"timeout"` + MinIdleConns int `mapstructure:"min_idle_conns"` + MaxIdleConns int `mapstructure:"max_idle_conns"` + ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"` + ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` +} + +type RedisTimeouts struct { + DialTimeout time.Duration `mapstructure:"dial_timeout"` + ReadTimeout time.Duration `mapstructure:"read_timeout"` + WriteTimeout time.Duration `mapstructure:"write_timeout"` +} + +type RedisRetry struct { + MaxRetries int `mapstructure:"max_retries"` + MinRetryBackoff time.Duration `mapstructure:"min_retry_backoff"` + MaxRetryBackoff time.Duration `mapstructure:"max_retry_backoff"` } type RedisCluster struct { @@ -194,6 +219,76 @@ func (cfg *Redis) validateAndLog() error { } log.Infof("config.backend.redis.tls.enabled: %t", cfg.TLS.Enabled) log.Infof("config.backend.redis.tls.insecure_skip_verify: %t", cfg.TLS.InsecureSkipVerify) + + // Validate and log pool configuration + if cfg.Pool != nil { + if cfg.Pool.Size < 0 { + return errors.New("Redis pool size cannot be negative") + } + if cfg.Pool.MinIdleConns < 0 { + return errors.New("Redis pool min_idle_conns cannot be negative") + } + if cfg.Pool.MaxIdleConns < 0 { + return errors.New("Redis pool max_idle_conns cannot be negative") + } + if cfg.Pool.MinIdleConns > 0 && cfg.Pool.MaxIdleConns > 0 && cfg.Pool.MinIdleConns > cfg.Pool.MaxIdleConns { + return errors.New("Redis pool min_idle_conns cannot be greater than max_idle_conns") + } + if cfg.Pool.Timeout < 0 { + return errors.New("Redis pool timeout cannot be negative") + } + if cfg.Pool.ConnMaxIdleTime < 0 { + return errors.New("Redis pool conn_max_idle_time cannot be negative") + } + if cfg.Pool.ConnMaxLifetime < 0 { + return errors.New("Redis pool conn_max_lifetime cannot be negative") + } + + log.Infof("config.backend.redis.pool.size: %d", cfg.Pool.Size) + log.Infof("config.backend.redis.pool.timeout: %v", cfg.Pool.Timeout) + log.Infof("config.backend.redis.pool.min_idle_conns: %d", cfg.Pool.MinIdleConns) + log.Infof("config.backend.redis.pool.max_idle_conns: %d", cfg.Pool.MaxIdleConns) + log.Infof("config.backend.redis.pool.conn_max_idle_time: %v", cfg.Pool.ConnMaxIdleTime) + log.Infof("config.backend.redis.pool.conn_max_lifetime: %v", cfg.Pool.ConnMaxLifetime) + } + + // Validate and log timeout configuration + if cfg.Timeouts != nil { + if cfg.Timeouts.DialTimeout < 0 { + return errors.New("Redis dial_timeout cannot be negative") + } + if cfg.Timeouts.ReadTimeout < 0 { + return errors.New("Redis read_timeout cannot be negative") + } + if cfg.Timeouts.WriteTimeout < 0 { + return errors.New("Redis write_timeout cannot be negative") + } + + log.Infof("config.backend.redis.timeouts.dial_timeout: %v", cfg.Timeouts.DialTimeout) + log.Infof("config.backend.redis.timeouts.read_timeout: %v", cfg.Timeouts.ReadTimeout) + log.Infof("config.backend.redis.timeouts.write_timeout: %v", cfg.Timeouts.WriteTimeout) + } + + // Validate and log retry configuration + if cfg.Retry != nil { + if cfg.Retry.MaxRetries < 0 { + return errors.New("Redis max_retries cannot be negative") + } + if cfg.Retry.MinRetryBackoff < 0 { + return errors.New("Redis min_retry_backoff cannot be negative") + } + if cfg.Retry.MaxRetryBackoff < 0 { + return errors.New("Redis max_retry_backoff cannot be negative") + } + if cfg.Retry.MinRetryBackoff > 0 && cfg.Retry.MaxRetryBackoff > 0 && cfg.Retry.MinRetryBackoff > cfg.Retry.MaxRetryBackoff { + return errors.New("Redis min_retry_backoff cannot be greater than max_retry_backoff") + } + + log.Infof("config.backend.redis.retry.max_retries: %d", cfg.Retry.MaxRetries) + log.Infof("config.backend.redis.retry.min_retry_backoff: %v", cfg.Retry.MinRetryBackoff) + log.Infof("config.backend.redis.retry.max_retry_backoff: %v", cfg.Retry.MaxRetryBackoff) + } + return nil } diff --git a/config/configtest/sample_full_config.yaml b/config/configtest/sample_full_config.yaml index d1d7fd66..1e870963 100644 --- a/config/configtest/sample_full_config.yaml +++ b/config/configtest/sample_full_config.yaml @@ -28,7 +28,7 @@ backend: keyspace: "prebid" default_ttl_seconds: 60 memcache: - hosts: ["10.0.0.1:11211","127.0.0.1"] + hosts: ["10.0.0.1:11211", "127.0.0.1"] redis: host: "127.0.0.1" port: 6379 From 64564d847b07b6368ec127c86a265a532d904fd1 Mon Sep 17 00:00:00 2001 From: Kiril Kalchev Date: Tue, 5 Aug 2025 23:54:48 +0300 Subject: [PATCH 7/8] initialize redis configuration --- config/config.go | 12 ++++++++++++ config/config_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/config/config.go b/config/config.go index 1b3782de..0f917641 100644 --- a/config/config.go +++ b/config/config.go @@ -73,6 +73,18 @@ func setConfigDefaults(v *viper.Viper) { v.SetDefault("backend.redis.tls.insecure_skip_verify", false) v.SetDefault("backend.redis.cluster.enabled", false) v.SetDefault("backend.redis.cluster.hosts", []string{}) + v.SetDefault("backend.redis.pool.size", 0) + v.SetDefault("backend.redis.pool.timeout", time.Duration(0)) + v.SetDefault("backend.redis.pool.min_idle_conns", 0) + v.SetDefault("backend.redis.pool.max_idle_conns", 0) + v.SetDefault("backend.redis.pool.conn_max_idle_time", time.Duration(0)) + v.SetDefault("backend.redis.pool.conn_max_lifetime", time.Duration(0)) + v.SetDefault("backend.redis.timeouts.dial_timeout", time.Duration(0)) + v.SetDefault("backend.redis.timeouts.read_timeout", time.Duration(0)) + v.SetDefault("backend.redis.timeouts.write_timeout", time.Duration(0)) + v.SetDefault("backend.redis.retry.max_retries", 0) + v.SetDefault("backend.redis.retry.min_retry_backoff", time.Duration(0)) + v.SetDefault("backend.redis.retry.max_retry_backoff", time.Duration(0)) v.SetDefault("backend.ignite.scheme", "") v.SetDefault("backend.ignite.host", "") v.SetDefault("backend.ignite.port", 0) diff --git a/config/config_test.go b/config/config_test.go index 50fee980..967daaaa 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1304,6 +1304,24 @@ func getExpectedDefaultConfig() Configuration { Enabled: false, Hosts: []string{}, }, + Pool: &RedisPool{ + Size: 0, + Timeout: time.Duration(0), + MinIdleConns: 0, + MaxIdleConns: 0, + ConnMaxIdleTime: time.Duration(0), + ConnMaxLifetime: time.Duration(0), + }, + Timeouts: &RedisTimeouts{ + DialTimeout: time.Duration(0), + ReadTimeout: time.Duration(0), + WriteTimeout: time.Duration(0), + }, + Retry: &RedisRetry{ + MaxRetries: 0, + MinRetryBackoff: time.Duration(0), + MaxRetryBackoff: time.Duration(0), + }, }, Ignite: Ignite{ Headers: map[string]string{}, @@ -1386,6 +1404,24 @@ func getExpectedFullConfigForTestFile() Configuration { Enabled: false, Hosts: []string{"redis-node1:6379", "redis-node2:6379", "redis-node3:6379"}, }, + Pool: &RedisPool{ + Size: 0, + Timeout: time.Duration(0), + MinIdleConns: 0, + MaxIdleConns: 0, + ConnMaxIdleTime: time.Duration(0), + ConnMaxLifetime: time.Duration(0), + }, + Timeouts: &RedisTimeouts{ + DialTimeout: time.Duration(0), + ReadTimeout: time.Duration(0), + WriteTimeout: time.Duration(0), + }, + Retry: &RedisRetry{ + MaxRetries: 0, + MinRetryBackoff: time.Duration(0), + MaxRetryBackoff: time.Duration(0), + }, }, Ignite: Ignite{ Scheme: "http", From 610303c45dc967e8579a2e1c406c206f06c6b6b7 Mon Sep 17 00:00:00 2001 From: Kiril Kalchev Date: Wed, 6 Aug 2025 00:06:02 +0300 Subject: [PATCH 8/8] remove code duplication from applySingleNodePerformanceOptions and applySingleNodePerformanceOptions --- backends/redis.go | 109 ++++++++++++++++++++-------------------------- 1 file changed, 48 insertions(+), 61 deletions(-) diff --git a/backends/redis.go b/backends/redis.go index 1bd14ab5..938361f2 100644 --- a/backends/redis.go +++ b/backends/redis.go @@ -3,6 +3,7 @@ package backends import ( "context" "crypto/tls" + "reflect" "strconv" "time" @@ -129,106 +130,92 @@ func NewRedisBackend(cfg config.Redis, ctx context.Context) *RedisBackend { } } -// applySingleNodePerformanceOptions applies performance tuning options to single-node Redis client -func applySingleNodePerformanceOptions(options *redis.Options, cfg config.Redis) { +// applyPerformanceOptions applies performance tuning options to both single-node and cluster Redis clients +// This function uses reflection to set common fields on both redis.Options and redis.ClusterOptions +func applyPerformanceOptions(options interface{}, cfg config.Redis) { + optionsValue := reflect.ValueOf(options).Elem() + // Apply pool configuration if cfg.Pool != nil { if cfg.Pool.Size > 0 { - options.PoolSize = cfg.Pool.Size + if field := optionsValue.FieldByName("PoolSize"); field.IsValid() && field.CanSet() { + field.SetInt(int64(cfg.Pool.Size)) + } } if cfg.Pool.Timeout > 0 { - options.PoolTimeout = cfg.Pool.Timeout + if field := optionsValue.FieldByName("PoolTimeout"); field.IsValid() && field.CanSet() { + field.Set(reflect.ValueOf(cfg.Pool.Timeout)) + } } if cfg.Pool.MinIdleConns > 0 { - options.MinIdleConns = cfg.Pool.MinIdleConns + if field := optionsValue.FieldByName("MinIdleConns"); field.IsValid() && field.CanSet() { + field.SetInt(int64(cfg.Pool.MinIdleConns)) + } } if cfg.Pool.MaxIdleConns > 0 { - options.MaxIdleConns = cfg.Pool.MaxIdleConns + if field := optionsValue.FieldByName("MaxIdleConns"); field.IsValid() && field.CanSet() { + field.SetInt(int64(cfg.Pool.MaxIdleConns)) + } } if cfg.Pool.ConnMaxIdleTime > 0 { - options.ConnMaxIdleTime = cfg.Pool.ConnMaxIdleTime + if field := optionsValue.FieldByName("ConnMaxIdleTime"); field.IsValid() && field.CanSet() { + field.Set(reflect.ValueOf(cfg.Pool.ConnMaxIdleTime)) + } } if cfg.Pool.ConnMaxLifetime > 0 { - options.ConnMaxLifetime = cfg.Pool.ConnMaxLifetime + if field := optionsValue.FieldByName("ConnMaxLifetime"); field.IsValid() && field.CanSet() { + field.Set(reflect.ValueOf(cfg.Pool.ConnMaxLifetime)) + } } } // Apply timeout configuration if cfg.Timeouts != nil { if cfg.Timeouts.DialTimeout > 0 { - options.DialTimeout = cfg.Timeouts.DialTimeout + if field := optionsValue.FieldByName("DialTimeout"); field.IsValid() && field.CanSet() { + field.Set(reflect.ValueOf(cfg.Timeouts.DialTimeout)) + } } if cfg.Timeouts.ReadTimeout > 0 { - options.ReadTimeout = cfg.Timeouts.ReadTimeout + if field := optionsValue.FieldByName("ReadTimeout"); field.IsValid() && field.CanSet() { + field.Set(reflect.ValueOf(cfg.Timeouts.ReadTimeout)) + } } if cfg.Timeouts.WriteTimeout > 0 { - options.WriteTimeout = cfg.Timeouts.WriteTimeout + if field := optionsValue.FieldByName("WriteTimeout"); field.IsValid() && field.CanSet() { + field.Set(reflect.ValueOf(cfg.Timeouts.WriteTimeout)) + } } } // Apply retry configuration if cfg.Retry != nil { if cfg.Retry.MaxRetries >= 0 { - options.MaxRetries = cfg.Retry.MaxRetries + if field := optionsValue.FieldByName("MaxRetries"); field.IsValid() && field.CanSet() { + field.SetInt(int64(cfg.Retry.MaxRetries)) + } } if cfg.Retry.MinRetryBackoff > 0 { - options.MinRetryBackoff = cfg.Retry.MinRetryBackoff + if field := optionsValue.FieldByName("MinRetryBackoff"); field.IsValid() && field.CanSet() { + field.Set(reflect.ValueOf(cfg.Retry.MinRetryBackoff)) + } } if cfg.Retry.MaxRetryBackoff > 0 { - options.MaxRetryBackoff = cfg.Retry.MaxRetryBackoff + if field := optionsValue.FieldByName("MaxRetryBackoff"); field.IsValid() && field.CanSet() { + field.Set(reflect.ValueOf(cfg.Retry.MaxRetryBackoff)) + } } } } +// applySingleNodePerformanceOptions applies performance tuning options to single-node Redis client +func applySingleNodePerformanceOptions(options *redis.Options, cfg config.Redis) { + applyPerformanceOptions(options, cfg) +} + // applyClusterPerformanceOptions applies performance tuning options to cluster Redis client func applyClusterPerformanceOptions(options *redis.ClusterOptions, cfg config.Redis) { - // Apply pool configuration - if cfg.Pool != nil { - if cfg.Pool.Size > 0 { - options.PoolSize = cfg.Pool.Size - } - if cfg.Pool.Timeout > 0 { - options.PoolTimeout = cfg.Pool.Timeout - } - if cfg.Pool.MinIdleConns > 0 { - options.MinIdleConns = cfg.Pool.MinIdleConns - } - if cfg.Pool.MaxIdleConns > 0 { - options.MaxIdleConns = cfg.Pool.MaxIdleConns - } - if cfg.Pool.ConnMaxIdleTime > 0 { - options.ConnMaxIdleTime = cfg.Pool.ConnMaxIdleTime - } - if cfg.Pool.ConnMaxLifetime > 0 { - options.ConnMaxLifetime = cfg.Pool.ConnMaxLifetime - } - } - - // Apply timeout configuration - if cfg.Timeouts != nil { - if cfg.Timeouts.DialTimeout > 0 { - options.DialTimeout = cfg.Timeouts.DialTimeout - } - if cfg.Timeouts.ReadTimeout > 0 { - options.ReadTimeout = cfg.Timeouts.ReadTimeout - } - if cfg.Timeouts.WriteTimeout > 0 { - options.WriteTimeout = cfg.Timeouts.WriteTimeout - } - } - - // Apply retry configuration - if cfg.Retry != nil { - if cfg.Retry.MaxRetries >= 0 { - options.MaxRetries = cfg.Retry.MaxRetries - } - if cfg.Retry.MinRetryBackoff > 0 { - options.MinRetryBackoff = cfg.Retry.MinRetryBackoff - } - if cfg.Retry.MaxRetryBackoff > 0 { - options.MaxRetryBackoff = cfg.Retry.MaxRetryBackoff - } - } + applyPerformanceOptions(options, cfg) } // Get calls the Redis client to return the value associated with the provided `key`