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 - RedisCacheHost
- string - string
- default: "redis:6379" - 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 - RedisCachePassword
- string - string
- default: "" - default: ""
@@ -640,7 +644,10 @@ http:
forwardedHeadersCustomName: X-Custom-Header forwardedHeadersCustomName: X-Custom-Header
remediationHeadersCustomName: cs-remediation remediationHeadersCustomName: cs-remediation
redisCacheEnabled: false redisCacheEnabled: false
redisCacheHost: "redis:6379" redisCacheHost: "redis-primary:6379"
redisCacheReadHosts:
- "redis-replica-1:6379"
- "redis-replica-2:6379"
redisCachePassword: password redisCachePassword: password
redisCacheDatabase: "5" redisCacheDatabase: "5"
redisCacheUnreachableBlock: true redisCacheUnreachableBlock: true
+1
View File
@@ -268,6 +268,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam
log, log,
config.RedisCacheEnabled, config.RedisCacheEnabled,
config.RedisCacheHost, config.RedisCacheHost,
config.RedisCacheReadHosts,
config.RedisCachePassword, config.RedisCachePassword,
config.RedisCacheDatabase, config.RedisCacheDatabase,
) )
+43 -19
View File
@@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"sync/atomic"
ttl_map "github.com/leprosus/golang-ttl-map" ttl_map "github.com/leprosus/golang-ttl-map"
simpleredis "github.com/maxlerebourg/simpleredis" simpleredis "github.com/maxlerebourg/simpleredis"
@@ -27,10 +28,7 @@ const (
) )
//nolint:gochecknoglobals //nolint:gochecknoglobals
var ( var cache = ttl_map.New()
redis simpleredis.SimpleRedis
cache = ttl_map.New()
)
type localCache struct{} type localCache struct{}
@@ -53,32 +51,52 @@ func (localCache) delete(key string) {
type redisCache struct { 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) { func (rc *redisCache) nextReader() *simpleredis.SimpleRedis {
value, err := redis.Get(key) n := len(rc.readers)
if n == 0 {
return &rc.writer
}
idx := rc.counter.Add(1) % uint64(n)
return &rc.readers[idx]
}
func redisResultToString(value []byte, err error) (string, error) {
if err == nil {
valueString := string(value) valueString := string(value)
if err == nil && len(valueString) > 0 { if len(valueString) > 0 {
return valueString, nil return valueString, nil
} }
errRedisMessage := err.Error()
if errRedisMessage == simpleredis.RedisMiss {
return "", errors.New(CacheMiss)
} }
if errRedisMessage == simpleredis.RedisUnreachable { if err != nil {
switch err.Error() {
case simpleredis.RedisMiss:
return "", errors.New(CacheMiss)
case simpleredis.RedisUnreachable:
return "", errors.New(CacheUnreachable) return "", errors.New(CacheUnreachable)
} }
return "", err return "", err
}
return "", errors.New(CacheMiss)
} }
func (rc redisCache) set(key, value string, duration int64) { func (rc *redisCache) get(key string) (string, error) {
if err := redis.Set(key, []byte(value), duration); err != nil { 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()) rc.log.Error("cache:setDecisionRedisCache" + err.Error())
} }
} }
func (rc redisCache) delete(key string) { func (rc *redisCache) delete(key string) {
if err := redis.Del(key); err != nil { if err := rc.writer.Del(key); err != nil {
rc.log.Error("cache:deleteDecisionRedisCache " + err.Error()) rc.log.Error("cache:deleteDecisionRedisCache " + err.Error())
} }
} }
@@ -96,15 +114,21 @@ type Client struct {
} }
// New Initialize cache client. // 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 c.log = log
if isRedis { if isRedis {
redis.Init(host, pass, database) rc := &redisCache{log: log}
c.cache = &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 { } else {
c.cache = &localCache{} 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. // Delete delete decision in cache.
+2
View File
@@ -96,6 +96,7 @@ type Config struct {
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"` ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"` RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
RedisCacheHost string `json:"redisCacheHost,omitempty"` RedisCacheHost string `json:"redisCacheHost,omitempty"`
RedisCacheReadHosts []string `json:"redisCacheReadHosts,omitempty"`
RedisCachePassword string `json:"redisCachePassword,omitempty"` RedisCachePassword string `json:"redisCachePassword,omitempty"`
RedisCachePasswordFile string `json:"redisCachePasswordFile,omitempty"` RedisCachePasswordFile string `json:"redisCachePasswordFile,omitempty"`
RedisCacheDatabase string `json:"redisCacheDatabase,omitempty"` RedisCacheDatabase string `json:"redisCacheDatabase,omitempty"`
@@ -172,6 +173,7 @@ func New() *Config {
ClientTrustedIPs: []string{}, ClientTrustedIPs: []string{},
RedisCacheEnabled: false, RedisCacheEnabled: false,
RedisCacheHost: "redis:6379", RedisCacheHost: "redis:6379",
RedisCacheReadHosts: []string{},
RedisCachePassword: "", RedisCachePassword: "",
RedisCacheDatabase: "", RedisCacheDatabase: "",
RedisCacheUnreachableBlock: true, RedisCacheUnreachableBlock: true,