feat: Allow cache reading from replicas

This commit is contained in:
Sam Toxopeus
2026-07-23 13:03:57 +02:00
parent 1c98c70f14
commit ad7596ff85
4 changed files with 62 additions and 28 deletions
+9 -2
View File
@@ -444,7 +444,11 @@ make run
- RedisCacheHost
- string
- default: "redis:6379"
- hostname and port for the Redis service
- hostname and port for the Redis write host (primary)
- RedisCacheReadHosts
- []string
- default: []
- List of Redis replica hostnames (host:port) to use for read operations. Reads are distributed round-robin across replicas. Falls back to RedisCacheHost when empty.
- RedisCachePassword
- string
- default: ""
@@ -640,7 +644,10 @@ http:
forwardedHeadersCustomName: X-Custom-Header
remediationHeadersCustomName: cs-remediation
redisCacheEnabled: false
redisCacheHost: "redis:6379"
redisCacheHost: "redis-primary:6379"
redisCacheReadHosts:
- "redis-replica-1:6379"
- "redis-replica-2:6379"
redisCachePassword: password
redisCacheDatabase: "5"
redisCacheUnreachableBlock: true
+1
View File
@@ -268,6 +268,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
log,
config.RedisCacheEnabled,
config.RedisCacheHost,
config.RedisCacheReadHosts,
config.RedisCachePassword,
config.RedisCacheDatabase,
)
+50 -26
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"log/slog"
"sync/atomic"
ttl_map "github.com/leprosus/golang-ttl-map"
simpleredis "github.com/maxlerebourg/simpleredis"
@@ -27,10 +28,7 @@ const (
)
//nolint:gochecknoglobals
var (
redis simpleredis.SimpleRedis
cache = ttl_map.New()
)
var cache = ttl_map.New()
type localCache struct{}
@@ -52,33 +50,53 @@ func (localCache) delete(key string) {
}
type redisCache struct {
log *slog.Logger
log *slog.Logger
writer simpleredis.SimpleRedis
readers []simpleredis.SimpleRedis
counter atomic.Uint64
}
func (redisCache) get(key string) (string, error) {
value, err := redis.Get(key)
valueString := string(value)
if err == nil && len(valueString) > 0 {
return valueString, nil
func (rc *redisCache) nextReader() *simpleredis.SimpleRedis {
n := len(rc.readers)
if n == 0 {
return &rc.writer
}
errRedisMessage := err.Error()
if errRedisMessage == simpleredis.RedisMiss {
return "", errors.New(CacheMiss)
}
if errRedisMessage == simpleredis.RedisUnreachable {
return "", errors.New(CacheUnreachable)
}
return "", err
idx := rc.counter.Add(1) % uint64(n)
return &rc.readers[idx]
}
func (rc redisCache) set(key, value string, duration int64) {
if err := redis.Set(key, []byte(value), duration); err != nil {
func redisResultToString(value []byte, err error) (string, error) {
if err == nil {
valueString := string(value)
if len(valueString) > 0 {
return valueString, nil
}
}
if err != nil {
switch err.Error() {
case simpleredis.RedisMiss:
return "", errors.New(CacheMiss)
case simpleredis.RedisUnreachable:
return "", errors.New(CacheUnreachable)
}
return "", err
}
return "", errors.New(CacheMiss)
}
func (rc *redisCache) get(key string) (string, error) {
value, err := rc.nextReader().Get(key)
return redisResultToString(value, err)
}
func (rc *redisCache) set(key, value string, duration int64) {
if err := rc.writer.Set(key, []byte(value), duration); err != nil {
rc.log.Error("cache:setDecisionRedisCache" + err.Error())
}
}
func (rc redisCache) delete(key string) {
if err := redis.Del(key); err != nil {
func (rc *redisCache) delete(key string) {
if err := rc.writer.Del(key); err != nil {
rc.log.Error("cache:deleteDecisionRedisCache " + err.Error())
}
}
@@ -96,15 +114,21 @@ type Client struct {
}
// New Initialize cache client.
func (c *Client) New(log *slog.Logger, isRedis bool, host, pass, database string) {
func (c *Client) New(log *slog.Logger, isRedis bool, writeHost string, readHosts []string, pass, database string) {
c.log = log
if isRedis {
redis.Init(host, pass, database)
c.cache = &redisCache{log: log}
rc := &redisCache{log: log}
rc.writer.Init(writeHost, pass, database)
for _, h := range readHosts {
var r simpleredis.SimpleRedis
r.Init(h, pass, database)
rc.readers = append(rc.readers, r)
}
c.cache = rc
} else {
c.cache = &localCache{}
}
c.log.Debug(fmt.Sprintf("cache:New initialized isRedis:%v", isRedis))
c.log.Debug(fmt.Sprintf("cache:New initialized isRedis:%v writeHost:%v readHosts:%v", isRedis, writeHost, readHosts))
}
// Delete delete decision in cache.
+2
View File
@@ -96,6 +96,7 @@ type Config struct {
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
RedisCacheHost string `json:"redisCacheHost,omitempty"`
RedisCacheReadHosts []string `json:"redisCacheReadHosts,omitempty"`
RedisCachePassword string `json:"redisCachePassword,omitempty"`
RedisCachePasswordFile string `json:"redisCachePasswordFile,omitempty"`
RedisCacheDatabase string `json:"redisCacheDatabase,omitempty"`
@@ -172,6 +173,7 @@ func New() *Config {
ClientTrustedIPs: []string{},
RedisCacheEnabled: false,
RedisCacheHost: "redis:6379",
RedisCacheReadHosts: []string{},
RedisCachePassword: "",
RedisCacheDatabase: "",
RedisCacheUnreachableBlock: true,