feat: use postgres

This commit is contained in:
Ilia Denisov
2026-04-26 20:34:39 +02:00
committed by GitHub
parent 48b0056b49
commit fe829285a6
365 changed files with 29223 additions and 24049 deletions
+23 -71
View File
@@ -9,8 +9,12 @@ import (
"strconv"
"strings"
"time"
"galaxy/redisconn"
)
const gatewayRedisEnvPrefix = "GATEWAY"
const (
// shutdownTimeoutEnvVar names the environment variable that controls the
// maximum time granted to each component shutdown call.
@@ -143,35 +147,14 @@ const (
// rate-limit burst.
authenticatedGRPCMessageClassRateLimitBurstEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_BURST"
// sessionCacheRedisAddrEnvVar names the environment variable that configures
// the Redis address used for SessionCache lookups.
sessionCacheRedisAddrEnvVar = "GATEWAY_SESSION_CACHE_REDIS_ADDR"
// sessionCacheRedisUsernameEnvVar names the environment variable that
// configures the Redis username used for SessionCache lookups.
sessionCacheRedisUsernameEnvVar = "GATEWAY_SESSION_CACHE_REDIS_USERNAME"
// sessionCacheRedisPasswordEnvVar names the environment variable that
// configures the Redis password used for SessionCache lookups.
sessionCacheRedisPasswordEnvVar = "GATEWAY_SESSION_CACHE_REDIS_PASSWORD"
// sessionCacheRedisDBEnvVar names the environment variable that configures
// the Redis logical database used for SessionCache lookups.
sessionCacheRedisDBEnvVar = "GATEWAY_SESSION_CACHE_REDIS_DB"
// sessionCacheRedisKeyPrefixEnvVar names the environment variable that
// configures the Redis key prefix used for SessionCache records.
sessionCacheRedisKeyPrefixEnvVar = "GATEWAY_SESSION_CACHE_REDIS_KEY_PREFIX"
// sessionCacheRedisLookupTimeoutEnvVar names the environment variable that
// configures the timeout used for SessionCache Redis lookups and startup
// connectivity checks.
// configures the timeout used for SessionCache Redis lookups.
sessionCacheRedisLookupTimeoutEnvVar = "GATEWAY_SESSION_CACHE_REDIS_LOOKUP_TIMEOUT"
// sessionCacheRedisTLSEnabledEnvVar names the environment variable that
// configures whether SessionCache Redis connections use TLS.
sessionCacheRedisTLSEnabledEnvVar = "GATEWAY_SESSION_CACHE_REDIS_TLS_ENABLED"
// replayRedisKeyPrefixEnvVar names the environment variable that configures
// the Redis key prefix used for authenticated replay reservations.
replayRedisKeyPrefixEnvVar = "GATEWAY_REPLAY_REDIS_KEY_PREFIX"
@@ -333,7 +316,6 @@ const (
defaultAuthenticatedGRPCMessageClassRateLimitRequests = 60
defaultAuthenticatedGRPCMessageClassRateLimitBurst = 20
defaultSessionCacheRedisDB = 0
defaultSessionCacheRedisKeyPrefix = "gateway:session:"
defaultSessionCacheRedisLookupTimeout = 250 * time.Millisecond
@@ -535,29 +517,16 @@ type AuthenticatedGRPCConfig struct {
AntiAbuse AuthenticatedGRPCAntiAbuseConfig
}
// SessionCacheRedisConfig describes the Redis connection used for authenticated
// SessionCache lookups.
// SessionCacheRedisConfig describes the namespace and timeout used for
// authenticated SessionCache lookups. Connection topology is shared with the
// other Redis-backed gateway components and lives on Config.Redis (see
// `pkg/redisconn`).
type SessionCacheRedisConfig struct {
// Addr is the Redis endpoint used for SessionCache requests.
Addr string
// Username is the optional Redis ACL username used for authentication.
Username string
// Password is the optional Redis password used for authentication.
Password string
// DB is the Redis logical database number used for SessionCache keys.
DB int
// KeyPrefix is prepended to every SessionCache Redis key.
KeyPrefix string
// LookupTimeout bounds individual SessionCache Redis operations.
LookupTimeout time.Duration
// TLSEnabled reports whether SessionCache Redis connections should use TLS.
TLSEnabled bool
}
// ReplayRedisConfig describes the Redis namespace and timeout used for
@@ -635,6 +604,11 @@ type Config struct {
// AuthenticatedGRPC configures the authenticated gRPC listener.
AuthenticatedGRPC AuthenticatedGRPCConfig
// Redis carries the master/replica/password connection topology shared by
// every gateway Redis component, sourced from the GATEWAY_REDIS_*
// environment variables managed by `pkg/redisconn`.
Redis redisconn.Config
// SessionCacheRedis configures the Redis-backed authenticated SessionCache.
SessionCacheRedis SessionCacheRedisConfig
@@ -759,12 +733,10 @@ func DefaultLoggingConfig() LoggingConfig {
return LoggingConfig{Level: defaultLogLevel}
}
// DefaultSessionCacheRedisConfig returns the default optional settings for the
// Redis-backed authenticated SessionCache. Addr remains empty and must be
// supplied explicitly.
// DefaultSessionCacheRedisConfig returns the default optional namespace and
// timeout settings for the Redis-backed authenticated SessionCache.
func DefaultSessionCacheRedisConfig() SessionCacheRedisConfig {
return SessionCacheRedisConfig{
DB: defaultSessionCacheRedisDB,
KeyPrefix: defaultSessionCacheRedisKeyPrefix,
LookupTimeout: defaultSessionCacheRedisLookupTimeout,
}
@@ -827,6 +799,7 @@ func LoadFromEnv() (Config, error) {
UserService: DefaultUserServiceConfig(),
AdminHTTP: DefaultAdminHTTPConfig(),
AuthenticatedGRPC: DefaultAuthenticatedGRPCConfig(),
Redis: redisconn.DefaultConfig(),
SessionCacheRedis: DefaultSessionCacheRedisConfig(),
ReplayRedis: DefaultReplayRedisConfig(),
SessionEventsRedis: DefaultSessionEventsRedisConfig(),
@@ -977,26 +950,11 @@ func LoadFromEnv() (Config, error) {
}
cfg.AuthenticatedGRPC.AntiAbuse.MessageClass = messageClassRateLimit
rawSessionCacheRedisAddr, ok := os.LookupEnv(sessionCacheRedisAddrEnvVar)
if ok {
cfg.SessionCacheRedis.Addr = rawSessionCacheRedisAddr
}
rawSessionCacheRedisUsername, ok := os.LookupEnv(sessionCacheRedisUsernameEnvVar)
if ok {
cfg.SessionCacheRedis.Username = rawSessionCacheRedisUsername
}
rawSessionCacheRedisPassword, ok := os.LookupEnv(sessionCacheRedisPasswordEnvVar)
if ok {
cfg.SessionCacheRedis.Password = rawSessionCacheRedisPassword
}
sessionCacheRedisDB, err := loadIntEnvWithDefault(sessionCacheRedisDBEnvVar, cfg.SessionCacheRedis.DB)
redisConn, err := redisconn.LoadFromEnv(gatewayRedisEnvPrefix)
if err != nil {
return Config{}, err
}
cfg.SessionCacheRedis.DB = sessionCacheRedisDB
cfg.Redis = redisConn
rawSessionCacheRedisKeyPrefix, ok := os.LookupEnv(sessionCacheRedisKeyPrefixEnvVar)
if ok {
@@ -1009,12 +967,6 @@ func LoadFromEnv() (Config, error) {
}
cfg.SessionCacheRedis.LookupTimeout = sessionCacheRedisLookupTimeout
sessionCacheRedisTLSEnabled, err := loadBoolEnvWithDefault(sessionCacheRedisTLSEnabledEnvVar, cfg.SessionCacheRedis.TLSEnabled)
if err != nil {
return Config{}, err
}
cfg.SessionCacheRedis.TLSEnabled = sessionCacheRedisTLSEnabled
rawReplayRedisKeyPrefix, ok := os.LookupEnv(replayRedisKeyPrefixEnvVar)
if ok {
cfg.ReplayRedis.KeyPrefix = rawReplayRedisKeyPrefix
@@ -1222,11 +1174,11 @@ func LoadFromEnv() (Config, error) {
); err != nil {
return Config{}, err
}
if strings.TrimSpace(cfg.SessionCacheRedis.Addr) == "" {
return Config{}, fmt.Errorf("load gateway config: %s must not be empty", sessionCacheRedisAddrEnvVar)
if err := cfg.Redis.Validate(); err != nil {
return Config{}, fmt.Errorf("load gateway config: redis: %w", err)
}
if cfg.SessionCacheRedis.DB < 0 {
return Config{}, fmt.Errorf("load gateway config: %s must not be negative", sessionCacheRedisDBEnvVar)
if strings.TrimSpace(cfg.SessionCacheRedis.KeyPrefix) == "" {
return Config{}, fmt.Errorf("load gateway config: %s must not be empty", sessionCacheRedisKeyPrefixEnvVar)
}
if cfg.SessionCacheRedis.LookupTimeout <= 0 {
return Config{}, fmt.Errorf("load gateway config: %s must be positive", sessionCacheRedisLookupTimeoutEnvVar)
+149 -93
View File
@@ -11,12 +11,36 @@ import (
"testing"
"time"
"galaxy/redisconn"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var configEnvMu sync.Mutex
const (
gatewayRedisMasterAddrEnvVar = "GATEWAY_REDIS_MASTER_ADDR"
gatewayRedisPasswordEnvVar = "GATEWAY_REDIS_PASSWORD"
gatewayRedisReplicaAddrsEnvVar = "GATEWAY_REDIS_REPLICA_ADDRS"
gatewayRedisDBEnvVar = "GATEWAY_REDIS_DB"
gatewayRedisOpTimeoutEnvVar = "GATEWAY_REDIS_OPERATION_TIMEOUT"
gatewayRedisTLSEnabledEnvVar = "GATEWAY_REDIS_TLS_ENABLED"
gatewayRedisUsernameEnvVar = "GATEWAY_REDIS_USERNAME"
)
var (
defaultTestRedisMasterAddrValue = "127.0.0.1:6379"
defaultTestRedisPasswordValue = "secret"
)
func defaultRedisConnConfigForTest() redisconn.Config {
cfg := redisconn.DefaultConfig()
cfg.MasterAddr = defaultTestRedisMasterAddrValue
cfg.Password = defaultTestRedisPasswordValue
return cfg
}
func TestLoadFromEnv(t *testing.T) {
customResponseSignerPrivateKeyPEMPath := new(string)
*customResponseSignerPrivateKeyPEMPath = writeTestResponseSignerPEMFile(t)
@@ -90,6 +114,7 @@ func TestLoadFromEnv(t *testing.T) {
authenticatedGRPCAddr *string
authenticatedGRPCFreshnessWindow *string
sessionCacheRedisAddr *string
skipRedis bool
responseSignerPrivateKeyPEMPath *string
want Config
wantErr string
@@ -104,9 +129,8 @@ func TestLoadFromEnv(t *testing.T) {
PublicHTTP: DefaultPublicHTTPConfig(),
AdminHTTP: DefaultAdminHTTPConfig(),
AuthenticatedGRPC: DefaultAuthenticatedGRPCConfig(),
Redis: defaultRedisConnConfigForTest(),
SessionCacheRedis: SessionCacheRedisConfig{
Addr: "127.0.0.1:6379",
DB: defaultSessionCacheRedisDB,
KeyPrefix: defaultSessionCacheRedisKeyPrefix,
LookupTimeout: defaultSessionCacheRedisLookupTimeout,
},
@@ -135,9 +159,8 @@ func TestLoadFromEnv(t *testing.T) {
PublicHTTP: DefaultPublicHTTPConfig(),
AdminHTTP: DefaultAdminHTTPConfig(),
AuthenticatedGRPC: DefaultAuthenticatedGRPCConfig(),
Redis: defaultRedisConnConfigForTest(),
SessionCacheRedis: SessionCacheRedisConfig{
Addr: "127.0.0.1:6379",
DB: defaultSessionCacheRedisDB,
KeyPrefix: defaultSessionCacheRedisKeyPrefix,
LookupTimeout: defaultSessionCacheRedisLookupTimeout,
},
@@ -170,9 +193,8 @@ func TestLoadFromEnv(t *testing.T) {
}(),
AdminHTTP: DefaultAdminHTTPConfig(),
AuthenticatedGRPC: DefaultAuthenticatedGRPCConfig(),
Redis: defaultRedisConnConfigForTest(),
SessionCacheRedis: SessionCacheRedisConfig{
Addr: "127.0.0.1:6379",
DB: defaultSessionCacheRedisDB,
KeyPrefix: defaultSessionCacheRedisKeyPrefix,
LookupTimeout: defaultSessionCacheRedisLookupTimeout,
},
@@ -204,9 +226,8 @@ func TestLoadFromEnv(t *testing.T) {
},
AdminHTTP: DefaultAdminHTTPConfig(),
AuthenticatedGRPC: DefaultAuthenticatedGRPCConfig(),
Redis: defaultRedisConnConfigForTest(),
SessionCacheRedis: SessionCacheRedisConfig{
Addr: "127.0.0.1:6379",
DB: defaultSessionCacheRedisDB,
KeyPrefix: defaultSessionCacheRedisKeyPrefix,
LookupTimeout: defaultSessionCacheRedisLookupTimeout,
},
@@ -238,9 +259,8 @@ func TestLoadFromEnv(t *testing.T) {
},
AdminHTTP: DefaultAdminHTTPConfig(),
AuthenticatedGRPC: DefaultAuthenticatedGRPCConfig(),
Redis: defaultRedisConnConfigForTest(),
SessionCacheRedis: SessionCacheRedisConfig{
Addr: "127.0.0.1:6379",
DB: defaultSessionCacheRedisDB,
KeyPrefix: defaultSessionCacheRedisKeyPrefix,
LookupTimeout: defaultSessionCacheRedisLookupTimeout,
},
@@ -273,9 +293,8 @@ func TestLoadFromEnv(t *testing.T) {
cfg.Addr = "127.0.0.1:9191"
return cfg
}(),
Redis: defaultRedisConnConfigForTest(),
SessionCacheRedis: SessionCacheRedisConfig{
Addr: "127.0.0.1:6379",
DB: defaultSessionCacheRedisDB,
KeyPrefix: defaultSessionCacheRedisKeyPrefix,
LookupTimeout: defaultSessionCacheRedisLookupTimeout,
},
@@ -308,9 +327,8 @@ func TestLoadFromEnv(t *testing.T) {
cfg.FreshnessWindow = 90 * time.Second
return cfg
}(),
Redis: defaultRedisConnConfigForTest(),
SessionCacheRedis: SessionCacheRedisConfig{
Addr: "127.0.0.1:6379",
DB: defaultSessionCacheRedisDB,
KeyPrefix: defaultSessionCacheRedisKeyPrefix,
LookupTimeout: defaultSessionCacheRedisLookupTimeout,
},
@@ -378,21 +396,10 @@ func TestLoadFromEnv(t *testing.T) {
wantErr: "parse " + authenticatedGRPCFreshnessWindowEnvVar,
},
{
name: "missing session cache redis address",
name: "missing redis master addr",
responseSignerPrivateKeyPEMPath: customResponseSignerPrivateKeyPEMPath,
wantErr: "GATEWAY_SESSION_CACHE_REDIS_ADDR must not be empty",
},
{
name: "empty session cache redis address",
sessionCacheRedisAddr: emptySessionCacheRedisAddr,
responseSignerPrivateKeyPEMPath: customResponseSignerPrivateKeyPEMPath,
wantErr: "GATEWAY_SESSION_CACHE_REDIS_ADDR must not be empty",
},
{
name: "whitespace session cache redis address",
sessionCacheRedisAddr: whitespaceSessionCacheRedisAddr,
responseSignerPrivateKeyPEMPath: customResponseSignerPrivateKeyPEMPath,
wantErr: "GATEWAY_SESSION_CACHE_REDIS_ADDR must not be empty",
skipRedis: true,
wantErr: "GATEWAY_REDIS_MASTER_ADDR must be set",
},
{
name: "missing response signer private key path",
@@ -412,7 +419,8 @@ func TestLoadFromEnv(t *testing.T) {
userServiceBaseURLEnvVar,
authenticatedGRPCAddrEnvVar,
authenticatedGRPCFreshnessWindowEnvVar,
sessionCacheRedisAddrEnvVar,
gatewayRedisMasterAddrEnvVar,
gatewayRedisPasswordEnvVar,
sessionEventsRedisStreamEnvVar,
clientEventsRedisStreamEnvVar,
responseSignerPrivateKeyPEMPathEnvVar,
@@ -424,7 +432,14 @@ func TestLoadFromEnv(t *testing.T) {
setEnvValue(t, userServiceBaseURLEnvVar, tt.userServiceBaseURL)
setEnvValue(t, authenticatedGRPCAddrEnvVar, tt.authenticatedGRPCAddr)
setEnvValue(t, authenticatedGRPCFreshnessWindowEnvVar, tt.authenticatedGRPCFreshnessWindow)
setEnvValue(t, sessionCacheRedisAddrEnvVar, tt.sessionCacheRedisAddr)
redisAddr := tt.sessionCacheRedisAddr
if !tt.skipRedis && redisAddr == nil {
redisAddr = customSessionCacheRedisAddr
}
setEnvValue(t, gatewayRedisMasterAddrEnvVar, redisAddr)
if !tt.skipRedis {
setEnvValue(t, gatewayRedisPasswordEnvVar, &defaultTestRedisPasswordValue)
}
setEnvValue(t, sessionEventsRedisStreamEnvVar, customSessionEventsRedisStream)
setEnvValue(t, clientEventsRedisStreamEnvVar, customClientEventsRedisStream)
setEnvValue(t, responseSignerPrivateKeyPEMPathEnvVar, tt.responseSignerPrivateKeyPEMPath)
@@ -490,7 +505,7 @@ func TestLoadFromEnvOperationalSettings(t *testing.T) {
{
name: "custom operational settings",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customSessionCacheRedisAddr,
gatewayRedisMasterAddrEnvVar: customSessionCacheRedisAddr,
sessionEventsRedisStreamEnvVar: customSessionEventsRedisStream,
clientEventsRedisStreamEnvVar: customClientEventsRedisStream,
responseSignerPrivateKeyPEMPathEnvVar: customResponseSignerPrivateKeyPEMPath,
@@ -516,7 +531,7 @@ func TestLoadFromEnvOperationalSettings(t *testing.T) {
{
name: "invalid log level",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customSessionCacheRedisAddr,
gatewayRedisMasterAddrEnvVar: customSessionCacheRedisAddr,
sessionEventsRedisStreamEnvVar: customSessionEventsRedisStream,
clientEventsRedisStreamEnvVar: customClientEventsRedisStream,
responseSignerPrivateKeyPEMPathEnvVar: customResponseSignerPrivateKeyPEMPath,
@@ -608,13 +623,14 @@ func TestLoadFromEnvAuthService(t *testing.T) {
authServiceBaseURLEnvVar,
userServiceBaseURLEnvVar,
logLevelEnvVar,
sessionCacheRedisAddrEnvVar,
gatewayRedisMasterAddrEnvVar,
sessionEventsRedisStreamEnvVar,
clientEventsRedisStreamEnvVar,
responseSignerPrivateKeyPEMPathEnvVar,
)
setEnvValue(t, authServiceBaseURLEnvVar, tt.value)
setEnvValue(t, sessionCacheRedisAddrEnvVar, customSessionCacheRedisAddr)
setEnvValue(t, gatewayRedisMasterAddrEnvVar, customSessionCacheRedisAddr)
setEnvValue(t, gatewayRedisPasswordEnvVar, &defaultTestRedisPasswordValue)
setEnvValue(t, sessionEventsRedisStreamEnvVar, customSessionEventsRedisStream)
setEnvValue(t, clientEventsRedisStreamEnvVar, customClientEventsRedisStream)
setEnvValue(t, responseSignerPrivateKeyPEMPathEnvVar, customResponseSignerPrivateKeyPEMPath)
@@ -674,13 +690,14 @@ func TestLoadFromEnvUserService(t *testing.T) {
authServiceBaseURLEnvVar,
userServiceBaseURLEnvVar,
logLevelEnvVar,
sessionCacheRedisAddrEnvVar,
gatewayRedisMasterAddrEnvVar,
sessionEventsRedisStreamEnvVar,
clientEventsRedisStreamEnvVar,
responseSignerPrivateKeyPEMPathEnvVar,
)
setEnvValue(t, userServiceBaseURLEnvVar, tt.value)
setEnvValue(t, sessionCacheRedisAddrEnvVar, customSessionCacheRedisAddr)
setEnvValue(t, gatewayRedisMasterAddrEnvVar, customSessionCacheRedisAddr)
setEnvValue(t, gatewayRedisPasswordEnvVar, &defaultTestRedisPasswordValue)
setEnvValue(t, sessionEventsRedisStreamEnvVar, customSessionEventsRedisStream)
setEnvValue(t, clientEventsRedisStreamEnvVar, customClientEventsRedisStream)
setEnvValue(t, responseSignerPrivateKeyPEMPathEnvVar, customResponseSignerPrivateKeyPEMPath)
@@ -811,7 +828,7 @@ func TestLoadFromEnvAuthenticatedGRPCAntiAbuse(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
restoreEnvs(
t,
sessionCacheRedisAddrEnvVar,
gatewayRedisMasterAddrEnvVar,
authenticatedGRPCIPRateLimitRequestsEnvVar,
authenticatedGRPCIPRateLimitWindowEnvVar,
authenticatedGRPCIPRateLimitBurstEnvVar,
@@ -829,7 +846,8 @@ func TestLoadFromEnvAuthenticatedGRPCAntiAbuse(t *testing.T) {
responseSignerPrivateKeyPEMPathEnvVar,
)
setEnvValue(t, sessionCacheRedisAddrEnvVar, customSessionCacheRedisAddr)
setEnvValue(t, gatewayRedisMasterAddrEnvVar, customSessionCacheRedisAddr)
setEnvValue(t, gatewayRedisPasswordEnvVar, &defaultTestRedisPasswordValue)
setEnvValue(t, sessionEventsRedisStreamEnvVar, customSessionEventsRedisStream)
setEnvValue(t, clientEventsRedisStreamEnvVar, customClientEventsRedisStream)
setEnvValue(t, responseSignerPrivateKeyPEMPathEnvVar, customResponseSignerPrivateKeyPEMPath)
@@ -859,7 +877,7 @@ func TestLoadFromEnvAuthenticatedGRPCAntiAbuse(t *testing.T) {
}
}
func TestLoadFromEnvSessionCacheRedis(t *testing.T) {
func TestLoadFromEnvRedis(t *testing.T) {
customResponseSignerPrivateKeyPEMPath := new(string)
*customResponseSignerPrivateKeyPEMPath = writeTestResponseSignerPEMFile(t)
@@ -872,8 +890,8 @@ func TestLoadFromEnvSessionCacheRedis(t *testing.T) {
customRedisAddr := new(string)
*customRedisAddr = "127.0.0.1:6380"
customRedisUsername := new(string)
*customRedisUsername = "gateway"
customRedisReplicas := new(string)
*customRedisReplicas = "127.0.0.1:6481,127.0.0.1:6482"
customRedisPassword := new(string)
*customRedisPassword = "secret"
@@ -881,14 +899,14 @@ func TestLoadFromEnvSessionCacheRedis(t *testing.T) {
customRedisDB := new(string)
*customRedisDB = "7"
customRedisOpTimeout := new(string)
*customRedisOpTimeout = "750ms"
customRedisKeyPrefix := new(string)
*customRedisKeyPrefix = "edge:session:"
customRedisLookupTimeout := new(string)
*customRedisLookupTimeout = "750ms"
customRedisTLSEnabled := new(string)
*customRedisTLSEnabled = "true"
*customRedisLookupTimeout = "950ms"
negativeRedisDB := new(string)
*negativeRedisDB = "-1"
@@ -896,67 +914,100 @@ func TestLoadFromEnvSessionCacheRedis(t *testing.T) {
invalidRedisLookupTimeout := new(string)
*invalidRedisLookupTimeout = "later"
invalidRedisTLSEnabled := new(string)
*invalidRedisTLSEnabled = "maybe"
deprecatedTLSEnabled := new(string)
*deprecatedTLSEnabled = "true"
deprecatedUsername := new(string)
*deprecatedUsername = "gateway"
type want struct {
conn redisconn.Config
sessionRedis SessionCacheRedisConfig
}
tests := []struct {
name string
envs map[string]*string
want SessionCacheRedisConfig
want *want
wantErr string
}{
{
name: "custom redis config",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customRedisAddr,
sessionCacheRedisUsernameEnvVar: customRedisUsername,
sessionCacheRedisPasswordEnvVar: customRedisPassword,
sessionCacheRedisDBEnvVar: customRedisDB,
gatewayRedisMasterAddrEnvVar: customRedisAddr,
gatewayRedisReplicaAddrsEnvVar: customRedisReplicas,
gatewayRedisPasswordEnvVar: customRedisPassword,
gatewayRedisDBEnvVar: customRedisDB,
gatewayRedisOpTimeoutEnvVar: customRedisOpTimeout,
sessionCacheRedisKeyPrefixEnvVar: customRedisKeyPrefix,
sessionCacheRedisLookupTimeoutEnvVar: customRedisLookupTimeout,
sessionCacheRedisTLSEnabledEnvVar: customRedisTLSEnabled,
},
want: SessionCacheRedisConfig{
Addr: "127.0.0.1:6380",
Username: "gateway",
Password: "secret",
DB: 7,
KeyPrefix: "edge:session:",
LookupTimeout: 750 * time.Millisecond,
TLSEnabled: true,
want: &want{
conn: redisconn.Config{
MasterAddr: "127.0.0.1:6380",
ReplicaAddrs: []string{"127.0.0.1:6481", "127.0.0.1:6482"},
Password: "secret",
DB: 7,
OperationTimeout: 750 * time.Millisecond,
},
sessionRedis: SessionCacheRedisConfig{
KeyPrefix: "edge:session:",
LookupTimeout: 950 * time.Millisecond,
},
},
},
{
name: "negative redis db",
name: "negative redis db rejected by pkg/redisconn",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customRedisAddr,
sessionCacheRedisDBEnvVar: negativeRedisDB,
gatewayRedisMasterAddrEnvVar: customRedisAddr,
gatewayRedisPasswordEnvVar: customRedisPassword,
gatewayRedisDBEnvVar: negativeRedisDB,
},
wantErr: sessionCacheRedisDBEnvVar + " must not be negative",
wantErr: "redis db must not be negative",
},
{
name: "invalid redis lookup timeout",
name: "invalid session cache lookup timeout",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customRedisAddr,
gatewayRedisMasterAddrEnvVar: customRedisAddr,
gatewayRedisPasswordEnvVar: customRedisPassword,
sessionCacheRedisLookupTimeoutEnvVar: invalidRedisLookupTimeout,
},
wantErr: "parse " + sessionCacheRedisLookupTimeoutEnvVar,
},
{
name: "invalid redis tls flag",
name: "missing redis password rejected",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customRedisAddr,
sessionCacheRedisTLSEnabledEnvVar: invalidRedisTLSEnabled,
gatewayRedisMasterAddrEnvVar: customRedisAddr,
},
wantErr: "parse " + sessionCacheRedisTLSEnabledEnvVar,
wantErr: gatewayRedisPasswordEnvVar + " must be set",
},
{
name: "deprecated tls enabled var rejected",
envs: map[string]*string{
gatewayRedisMasterAddrEnvVar: customRedisAddr,
gatewayRedisPasswordEnvVar: customRedisPassword,
gatewayRedisTLSEnabledEnvVar: deprecatedTLSEnabled,
},
wantErr: gatewayRedisTLSEnabledEnvVar,
},
{
name: "deprecated username var rejected",
envs: map[string]*string{
gatewayRedisMasterAddrEnvVar: customRedisAddr,
gatewayRedisPasswordEnvVar: customRedisPassword,
gatewayRedisUsernameEnvVar: deprecatedUsername,
},
wantErr: gatewayRedisUsernameEnvVar,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
restoreEnvs(t, append(append(append(sessionCacheRedisEnvVars(), sessionEventsRedisEnvVars()...), clientEventsRedisEnvVars()...), responseSignerPrivateKeyPEMPathEnvVar)...)
redisEnvVars := sessionCacheRedisEnvVars()
restoreEnvs(t, append(append(append(redisEnvVars, sessionEventsRedisEnvVars()...), clientEventsRedisEnvVars()...), responseSignerPrivateKeyPEMPathEnvVar)...)
for _, envVar := range redisEnvVars {
setEnvValue(t, envVar, nil)
}
setEnvValue(t, responseSignerPrivateKeyPEMPathEnvVar, customResponseSignerPrivateKeyPEMPath)
setEnvValue(t, sessionEventsRedisStreamEnvVar, customSessionEventsRedisStream)
setEnvValue(t, clientEventsRedisStreamEnvVar, customClientEventsRedisStream)
@@ -973,7 +1024,9 @@ func TestLoadFromEnvSessionCacheRedis(t *testing.T) {
}
require.NoError(t, err)
assert.Equal(t, tt.want, cfg.SessionCacheRedis)
require.NotNil(t, tt.want)
assert.Equal(t, tt.want.conn, cfg.Redis)
assert.Equal(t, tt.want.sessionRedis, cfg.SessionCacheRedis)
})
}
}
@@ -1012,7 +1065,7 @@ func TestLoadFromEnvReplayRedis(t *testing.T) {
{
name: "custom replay redis config",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customSessionCacheRedisAddr,
gatewayRedisMasterAddrEnvVar: customSessionCacheRedisAddr,
replayRedisKeyPrefixEnvVar: customReplayRedisKeyPrefix,
replayRedisReserveTimeoutEnvVar: customReplayRedisReserveTimeout,
},
@@ -1024,7 +1077,7 @@ func TestLoadFromEnvReplayRedis(t *testing.T) {
{
name: "empty replay redis key prefix",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customSessionCacheRedisAddr,
gatewayRedisMasterAddrEnvVar: customSessionCacheRedisAddr,
replayRedisKeyPrefixEnvVar: emptyReplayRedisKeyPrefix,
},
wantErr: replayRedisKeyPrefixEnvVar + " must not be empty",
@@ -1032,7 +1085,7 @@ func TestLoadFromEnvReplayRedis(t *testing.T) {
{
name: "invalid replay redis reserve timeout",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customSessionCacheRedisAddr,
gatewayRedisMasterAddrEnvVar: customSessionCacheRedisAddr,
replayRedisReserveTimeoutEnvVar: invalidReplayRedisReserveTimeout,
},
wantErr: "parse " + replayRedisReserveTimeoutEnvVar,
@@ -1096,7 +1149,7 @@ func TestLoadFromEnvSessionEventsRedis(t *testing.T) {
{
name: "custom session events redis config",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customSessionCacheRedisAddr,
gatewayRedisMasterAddrEnvVar: customSessionCacheRedisAddr,
sessionEventsRedisStreamEnvVar: customStream,
sessionEventsRedisReadBlockTimeoutEnvVar: customReadBlockTimeout,
},
@@ -1108,14 +1161,14 @@ func TestLoadFromEnvSessionEventsRedis(t *testing.T) {
{
name: "missing session events redis stream",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customSessionCacheRedisAddr,
gatewayRedisMasterAddrEnvVar: customSessionCacheRedisAddr,
},
wantErr: sessionEventsRedisStreamEnvVar + " must not be empty",
},
{
name: "empty session events redis stream",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customSessionCacheRedisAddr,
gatewayRedisMasterAddrEnvVar: customSessionCacheRedisAddr,
sessionEventsRedisStreamEnvVar: emptyStream,
},
wantErr: sessionEventsRedisStreamEnvVar + " must not be empty",
@@ -1123,7 +1176,7 @@ func TestLoadFromEnvSessionEventsRedis(t *testing.T) {
{
name: "invalid session events read block timeout",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customSessionCacheRedisAddr,
gatewayRedisMasterAddrEnvVar: customSessionCacheRedisAddr,
sessionEventsRedisStreamEnvVar: customStream,
sessionEventsRedisReadBlockTimeoutEnvVar: invalidReadBlockTimeout,
},
@@ -1187,7 +1240,7 @@ func TestLoadFromEnvClientEventsRedis(t *testing.T) {
{
name: "custom client events redis config",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customSessionCacheRedisAddr,
gatewayRedisMasterAddrEnvVar: customSessionCacheRedisAddr,
clientEventsRedisStreamEnvVar: customStream,
clientEventsRedisReadBlockTimeoutEnvVar: customReadBlockTimeout,
},
@@ -1199,14 +1252,14 @@ func TestLoadFromEnvClientEventsRedis(t *testing.T) {
{
name: "missing client events redis stream",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customSessionCacheRedisAddr,
gatewayRedisMasterAddrEnvVar: customSessionCacheRedisAddr,
},
wantErr: clientEventsRedisStreamEnvVar + " must not be empty",
},
{
name: "empty client events redis stream",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customSessionCacheRedisAddr,
gatewayRedisMasterAddrEnvVar: customSessionCacheRedisAddr,
clientEventsRedisStreamEnvVar: emptyStream,
},
wantErr: clientEventsRedisStreamEnvVar + " must not be empty",
@@ -1214,7 +1267,7 @@ func TestLoadFromEnvClientEventsRedis(t *testing.T) {
{
name: "invalid client events read block timeout",
envs: map[string]*string{
sessionCacheRedisAddrEnvVar: customSessionCacheRedisAddr,
gatewayRedisMasterAddrEnvVar: customSessionCacheRedisAddr,
clientEventsRedisStreamEnvVar: customStream,
clientEventsRedisReadBlockTimeoutEnvVar: invalidReadBlockTimeout,
},
@@ -1331,8 +1384,9 @@ func TestLoadFromEnvPublicHTTPAntiAbuse(t *testing.T) {
tt := tt
t.Run(tt.name, func(t *testing.T) {
restoreEnvs(t, append(append(append(append(publicAntiAbuseEnvVars(), sessionCacheRedisAddrEnvVar), sessionEventsRedisEnvVars()...), clientEventsRedisEnvVars()...), responseSignerPrivateKeyPEMPathEnvVar)...)
setEnvValue(t, sessionCacheRedisAddrEnvVar, requiredSessionCacheRedisAddr)
restoreEnvs(t, append(append(append(append(publicAntiAbuseEnvVars(), gatewayRedisMasterAddrEnvVar), sessionEventsRedisEnvVars()...), clientEventsRedisEnvVars()...), responseSignerPrivateKeyPEMPathEnvVar)...)
setEnvValue(t, gatewayRedisMasterAddrEnvVar, requiredSessionCacheRedisAddr)
setEnvValue(t, gatewayRedisPasswordEnvVar, &defaultTestRedisPasswordValue)
setEnvValue(t, sessionEventsRedisStreamEnvVar, requiredSessionEventsRedisStream)
setEnvValue(t, clientEventsRedisStreamEnvVar, requiredClientEventsRedisStream)
setEnvValue(t, responseSignerPrivateKeyPEMPathEnvVar, requiredResponseSignerPrivateKeyPEMPath)
@@ -1444,13 +1498,15 @@ func operationalEnvVars() []string {
func sessionCacheRedisEnvVars() []string {
return []string{
sessionCacheRedisAddrEnvVar,
sessionCacheRedisUsernameEnvVar,
sessionCacheRedisPasswordEnvVar,
sessionCacheRedisDBEnvVar,
gatewayRedisMasterAddrEnvVar,
gatewayRedisReplicaAddrsEnvVar,
gatewayRedisPasswordEnvVar,
gatewayRedisDBEnvVar,
gatewayRedisOpTimeoutEnvVar,
gatewayRedisTLSEnabledEnvVar,
gatewayRedisUsernameEnvVar,
sessionCacheRedisKeyPrefixEnvVar,
sessionCacheRedisLookupTimeoutEnvVar,
sessionCacheRedisTLSEnabledEnvVar,
}
}
+18 -60
View File
@@ -3,7 +3,6 @@ package events
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"strings"
@@ -39,26 +38,23 @@ type RedisClientEventSubscriber struct {
logger *zap.Logger
metrics *telemetry.Runtime
closeOnce sync.Once
startedOnce sync.Once
started chan struct{}
}
// NewRedisClientEventSubscriber constructs a Redis Stream subscriber that
// reuses the SessionCache Redis connection settings and forwards decoded
// client-facing events to publisher.
func NewRedisClientEventSubscriber(sessionCfg config.SessionCacheRedisConfig, eventsCfg config.ClientEventsRedisConfig, publisher ClientEventPublisher) (*RedisClientEventSubscriber, error) {
return NewRedisClientEventSubscriberWithObservability(sessionCfg, eventsCfg, publisher, nil, nil)
// NewRedisClientEventSubscriber constructs a Redis Stream subscriber that uses
// client and forwards decoded client-facing events to publisher.
func NewRedisClientEventSubscriber(client *redis.Client, sessionCfg config.SessionCacheRedisConfig, eventsCfg config.ClientEventsRedisConfig, publisher ClientEventPublisher) (*RedisClientEventSubscriber, error) {
return NewRedisClientEventSubscriberWithObservability(client, sessionCfg, eventsCfg, publisher, nil, nil)
}
// NewRedisClientEventSubscriberWithObservability constructs a Redis Stream
// subscriber that also records malformed or dropped internal events.
func NewRedisClientEventSubscriberWithObservability(sessionCfg config.SessionCacheRedisConfig, eventsCfg config.ClientEventsRedisConfig, publisher ClientEventPublisher, logger *zap.Logger, metrics *telemetry.Runtime) (*RedisClientEventSubscriber, error) {
if strings.TrimSpace(sessionCfg.Addr) == "" {
return nil, errors.New("new redis client event subscriber: redis addr must not be empty")
}
if sessionCfg.DB < 0 {
return nil, errors.New("new redis client event subscriber: redis db must not be negative")
// subscriber that also records malformed or dropped internal events. The
// subscriber does not own the client; the runtime supplies a shared
// *redis.Client.
func NewRedisClientEventSubscriberWithObservability(client *redis.Client, sessionCfg config.SessionCacheRedisConfig, eventsCfg config.ClientEventsRedisConfig, publisher ClientEventPublisher, logger *zap.Logger, metrics *telemetry.Runtime) (*RedisClientEventSubscriber, error) {
if client == nil {
return nil, errors.New("new redis client event subscriber: nil redis client")
}
if sessionCfg.LookupTimeout <= 0 {
return nil, errors.New("new redis client event subscriber: lookup timeout must be positive")
@@ -73,23 +69,12 @@ func NewRedisClientEventSubscriberWithObservability(sessionCfg config.SessionCac
return nil, errors.New("new redis client event subscriber: nil publisher")
}
options := &redis.Options{
Addr: sessionCfg.Addr,
Username: sessionCfg.Username,
Password: sessionCfg.Password,
DB: sessionCfg.DB,
Protocol: 2,
DisableIdentity: true,
}
if sessionCfg.TLSEnabled {
options.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12}
}
if logger == nil {
logger = zap.NewNop()
}
return &RedisClientEventSubscriber{
client: redis.NewClient(options),
client: client,
stream: eventsCfg.Stream,
pingTimeout: sessionCfg.LookupTimeout,
readBlockTimeout: eventsCfg.ReadBlockTimeout,
@@ -100,26 +85,6 @@ func NewRedisClientEventSubscriberWithObservability(sessionCfg config.SessionCac
}, nil
}
// Ping verifies that the Redis backend used for client-facing event fan-out is
// reachable within the configured timeout budget.
func (s *RedisClientEventSubscriber) Ping(ctx context.Context) error {
if s == nil || s.client == nil {
return errors.New("ping redis client event subscriber: nil subscriber")
}
if ctx == nil {
return errors.New("ping redis client event subscriber: nil context")
}
pingCtx, cancel := context.WithTimeout(ctx, s.pingTimeout)
defer cancel()
if err := s.client.Ping(pingCtx).Err(); err != nil {
return fmt.Errorf("ping redis client event subscriber: %w", err)
}
return nil
}
// Run consumes client-facing events until ctx is canceled or Redis returns an
// unexpected error.
func (s *RedisClientEventSubscriber) Run(ctx context.Context) error {
@@ -184,28 +149,21 @@ func (s *RedisClientEventSubscriber) resolveStartID(ctx context.Context) (string
return messages[0].ID, nil
}
// Shutdown closes the Redis client so a blocking stream read can terminate
// promptly during gateway shutdown.
// Shutdown is a no-op kept for App framework compatibility. The blocking
// XRead loop terminates when its context is cancelled by the parent runtime,
// which also owns and closes the shared Redis client.
func (s *RedisClientEventSubscriber) Shutdown(ctx context.Context) error {
if ctx == nil {
return errors.New("shutdown redis client event subscriber: nil context")
}
return s.Close()
return nil
}
// Close releases the underlying Redis client resources.
// Close is a no-op kept for backwards-compatible cleanup wiring; the
// subscriber does not own the shared Redis client.
func (s *RedisClientEventSubscriber) Close() error {
if s == nil || s.client == nil {
return nil
}
var err error
s.closeOnce.Do(func() {
err = s.client.Close()
})
return err
return nil
}
func (s *RedisClientEventSubscriber) signalStarted() {
@@ -153,8 +153,9 @@ func TestRedisClientEventSubscriberLogsAndCountsMalformedEvents(t *testing.T) {
telemetryRuntime := testutil.NewTelemetryRuntime(t, logger)
subscriber, err := NewRedisClientEventSubscriberWithObservability(
newTestRedisClient(t, server),
config.SessionCacheRedisConfig{
Addr: server.Addr(),
KeyPrefix: "gateway:session:",
LookupTimeout: 250 * time.Millisecond,
},
config.ClientEventsRedisConfig{
@@ -166,9 +167,6 @@ func TestRedisClientEventSubscriberLogsAndCountsMalformedEvents(t *testing.T) {
telemetryRuntime,
)
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, subscriber.Close())
})
running := runTestClientEventSubscriber(t, subscriber)
defer running.stop(t)
@@ -195,8 +193,9 @@ func newTestRedisClientEventSubscriber(t *testing.T, server *miniredis.Miniredis
t.Helper()
subscriber, err := NewRedisClientEventSubscriber(
newTestRedisClient(t, server),
config.SessionCacheRedisConfig{
Addr: server.Addr(),
KeyPrefix: "gateway:session:",
LookupTimeout: 250 * time.Millisecond,
},
config.ClientEventsRedisConfig{
@@ -207,10 +206,6 @@ func newTestRedisClientEventSubscriber(t *testing.T, server *miniredis.Miniredis
)
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, subscriber.Close())
})
return subscriber
}
+22 -64
View File
@@ -5,7 +5,6 @@ package events
import (
"context"
"crypto/tls"
"errors"
"fmt"
"strconv"
@@ -43,33 +42,30 @@ type RedisSessionSubscriber struct {
logger *zap.Logger
metrics *telemetry.Runtime
closeOnce sync.Once
startedOnce sync.Once
started chan struct{}
}
// NewRedisSessionSubscriber constructs a Redis Stream subscriber that reuses
// the SessionCache Redis connection settings and applies updates to store.
func NewRedisSessionSubscriber(sessionCfg config.SessionCacheRedisConfig, eventsCfg config.SessionEventsRedisConfig, store session.SnapshotStore) (*RedisSessionSubscriber, error) {
return NewRedisSessionSubscriberWithObservability(sessionCfg, eventsCfg, store, nil, nil, nil)
// NewRedisSessionSubscriber constructs a Redis Stream subscriber that uses
// client and applies updates to store.
func NewRedisSessionSubscriber(client *redis.Client, sessionCfg config.SessionCacheRedisConfig, eventsCfg config.SessionEventsRedisConfig, store session.SnapshotStore) (*RedisSessionSubscriber, error) {
return NewRedisSessionSubscriberWithObservability(client, sessionCfg, eventsCfg, store, nil, nil, nil)
}
// NewRedisSessionSubscriberWithRevocationHandler constructs a Redis Stream
// subscriber that reuses the SessionCache Redis connection settings, applies
// updates to store, and optionally tears down active resources for revoked
// sessions.
func NewRedisSessionSubscriberWithRevocationHandler(sessionCfg config.SessionCacheRedisConfig, eventsCfg config.SessionEventsRedisConfig, store session.SnapshotStore, revocationHandler SessionRevocationHandler) (*RedisSessionSubscriber, error) {
return NewRedisSessionSubscriberWithObservability(sessionCfg, eventsCfg, store, revocationHandler, nil, nil)
// subscriber that uses client, applies updates to store, and optionally tears
// down active resources for revoked sessions.
func NewRedisSessionSubscriberWithRevocationHandler(client *redis.Client, sessionCfg config.SessionCacheRedisConfig, eventsCfg config.SessionEventsRedisConfig, store session.SnapshotStore, revocationHandler SessionRevocationHandler) (*RedisSessionSubscriber, error) {
return NewRedisSessionSubscriberWithObservability(client, sessionCfg, eventsCfg, store, revocationHandler, nil, nil)
}
// NewRedisSessionSubscriberWithObservability constructs a Redis Stream
// subscriber that also logs and counts malformed internal session events.
func NewRedisSessionSubscriberWithObservability(sessionCfg config.SessionCacheRedisConfig, eventsCfg config.SessionEventsRedisConfig, store session.SnapshotStore, revocationHandler SessionRevocationHandler, logger *zap.Logger, metrics *telemetry.Runtime) (*RedisSessionSubscriber, error) {
if strings.TrimSpace(sessionCfg.Addr) == "" {
return nil, errors.New("new redis session subscriber: redis addr must not be empty")
}
if sessionCfg.DB < 0 {
return nil, errors.New("new redis session subscriber: redis db must not be negative")
// subscriber that also logs and counts malformed internal session events. The
// subscriber does not own the client; the runtime supplies a shared
// *redis.Client.
func NewRedisSessionSubscriberWithObservability(client *redis.Client, sessionCfg config.SessionCacheRedisConfig, eventsCfg config.SessionEventsRedisConfig, store session.SnapshotStore, revocationHandler SessionRevocationHandler, logger *zap.Logger, metrics *telemetry.Runtime) (*RedisSessionSubscriber, error) {
if client == nil {
return nil, errors.New("new redis session subscriber: nil redis client")
}
if sessionCfg.LookupTimeout <= 0 {
return nil, errors.New("new redis session subscriber: lookup timeout must be positive")
@@ -84,23 +80,12 @@ func NewRedisSessionSubscriberWithObservability(sessionCfg config.SessionCacheRe
return nil, errors.New("new redis session subscriber: nil session snapshot store")
}
options := &redis.Options{
Addr: sessionCfg.Addr,
Username: sessionCfg.Username,
Password: sessionCfg.Password,
DB: sessionCfg.DB,
Protocol: 2,
DisableIdentity: true,
}
if sessionCfg.TLSEnabled {
options.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12}
}
if logger == nil {
logger = zap.NewNop()
}
return &RedisSessionSubscriber{
client: redis.NewClient(options),
client: client,
stream: eventsCfg.Stream,
pingTimeout: sessionCfg.LookupTimeout,
readBlockTimeout: eventsCfg.ReadBlockTimeout,
@@ -112,26 +97,6 @@ func NewRedisSessionSubscriberWithObservability(sessionCfg config.SessionCacheRe
}, nil
}
// Ping verifies that the Redis backend used for session lifecycle events is
// reachable within the configured timeout budget.
func (s *RedisSessionSubscriber) Ping(ctx context.Context) error {
if s == nil || s.client == nil {
return errors.New("ping redis session subscriber: nil subscriber")
}
if ctx == nil {
return errors.New("ping redis session subscriber: nil context")
}
pingCtx, cancel := context.WithTimeout(ctx, s.pingTimeout)
defer cancel()
if err := s.client.Ping(pingCtx).Err(); err != nil {
return fmt.Errorf("ping redis session subscriber: %w", err)
}
return nil
}
// Run consumes session lifecycle events until ctx is canceled or Redis returns
// an unexpected error.
func (s *RedisSessionSubscriber) Run(ctx context.Context) error {
@@ -196,28 +161,21 @@ func (s *RedisSessionSubscriber) resolveStartID(ctx context.Context) (string, er
return messages[0].ID, nil
}
// Shutdown closes the Redis client so a blocking stream read can terminate
// promptly during gateway shutdown.
// Shutdown is a no-op kept for App framework compatibility. The blocking
// XRead loop terminates when its context is cancelled by the parent runtime,
// which also owns and closes the shared Redis client.
func (s *RedisSessionSubscriber) Shutdown(ctx context.Context) error {
if ctx == nil {
return errors.New("shutdown redis session subscriber: nil context")
}
return s.Close()
return nil
}
// Close releases the underlying Redis client resources.
// Close is a no-op kept for backwards-compatible cleanup wiring; the
// subscriber does not own the shared Redis client.
func (s *RedisSessionSubscriber) Close() error {
if s == nil || s.client == nil {
return nil
}
var err error
s.closeOnce.Do(func() {
err = s.client.Close()
})
return err
return nil
}
func (s *RedisSessionSubscriber) signalStarted() {
+18 -3
View File
@@ -10,6 +10,7 @@ import (
"galaxy/gateway/internal/session"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -262,9 +263,12 @@ func newTestRedisSessionSubscriber(t *testing.T, server *miniredis.Miniredis, st
func newTestRedisSessionSubscriberWithRevocationHandler(t *testing.T, server *miniredis.Miniredis, store session.SnapshotStore, revocationHandler SessionRevocationHandler) *RedisSessionSubscriber {
t.Helper()
client := newTestRedisClient(t, server)
subscriber, err := NewRedisSessionSubscriberWithRevocationHandler(
client,
config.SessionCacheRedisConfig{
Addr: server.Addr(),
KeyPrefix: "gateway:session:",
LookupTimeout: 250 * time.Millisecond,
},
config.SessionEventsRedisConfig{
@@ -276,11 +280,22 @@ func newTestRedisSessionSubscriberWithRevocationHandler(t *testing.T, server *mi
)
require.NoError(t, err)
return subscriber
}
func newTestRedisClient(t *testing.T, server *miniredis.Miniredis) *redis.Client {
t.Helper()
client := redis.NewClient(&redis.Options{
Addr: server.Addr(),
Protocol: 2,
DisableIdentity: true,
})
t.Cleanup(func() {
assert.NoError(t, subscriber.Close())
assert.NoError(t, client.Close())
})
return subscriber
return client
}
type recordingSessionRevocationHandler struct {
+55
View File
@@ -0,0 +1,55 @@
// Package redisclient provides the Redis client helpers used by Gateway
// runtime wiring. The helpers wrap `pkg/redisconn` so the runtime keeps the
// same construction surface as the other Galaxy services.
package redisclient
import (
"context"
"fmt"
"galaxy/gateway/internal/telemetry"
"galaxy/redisconn"
"github.com/redis/go-redis/v9"
)
// NewClient constructs one Redis client from cfg using the shared
// `pkg/redisconn` helper, which enforces the master/replica/password env-var
// shape.
func NewClient(cfg redisconn.Config) *redis.Client {
return redisconn.NewMasterClient(cfg)
}
// InstrumentClient attaches Redis tracing and metrics exporters to client
// when telemetryRuntime is available.
func InstrumentClient(client *redis.Client, telemetryRuntime *telemetry.Runtime) error {
if client == nil {
return fmt.Errorf("instrument redis client: nil client")
}
if telemetryRuntime == nil {
return nil
}
return redisconn.Instrument(
client,
redisconn.WithTracerProvider(telemetryRuntime.TracerProvider()),
redisconn.WithMeterProvider(telemetryRuntime.MeterProvider()),
)
}
// Ping performs the startup Redis connectivity check bounded by
// cfg.OperationTimeout.
func Ping(ctx context.Context, cfg redisconn.Config, client *redis.Client) error {
if client == nil {
return fmt.Errorf("ping redis: nil client")
}
pingCtx, cancel := context.WithTimeout(ctx, cfg.OperationTimeout)
defer cancel()
if err := client.Ping(pingCtx).Err(); err != nil {
return fmt.Errorf("ping redis: %w", err)
}
return nil
}
+8 -52
View File
@@ -2,7 +2,6 @@ package replay
import (
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
@@ -22,15 +21,13 @@ type RedisStore struct {
reserveTimeout time.Duration
}
// NewRedisStore constructs a Redis-backed replay store that reuses the
// SessionCache Redis deployment settings and applies the replay-specific key
// namespace and timeout controls from replayCfg.
func NewRedisStore(sessionCfg config.SessionCacheRedisConfig, replayCfg config.ReplayRedisConfig) (*RedisStore, error) {
if strings.TrimSpace(sessionCfg.Addr) == "" {
return nil, errors.New("new redis replay store: redis addr must not be empty")
}
if sessionCfg.DB < 0 {
return nil, errors.New("new redis replay store: redis db must not be negative")
// NewRedisStore constructs a Redis-backed replay store that uses client and
// applies the replay-specific namespace and timeout controls from replayCfg.
// The store does not own the client; the runtime supplies a shared
// *redis.Client.
func NewRedisStore(client *redis.Client, replayCfg config.ReplayRedisConfig) (*RedisStore, error) {
if client == nil {
return nil, errors.New("new redis replay store: nil redis client")
}
if strings.TrimSpace(replayCfg.KeyPrefix) == "" {
return nil, errors.New("new redis replay store: replay key prefix must not be empty")
@@ -39,54 +36,13 @@ func NewRedisStore(sessionCfg config.SessionCacheRedisConfig, replayCfg config.R
return nil, errors.New("new redis replay store: reserve timeout must be positive")
}
options := &redis.Options{
Addr: sessionCfg.Addr,
Username: sessionCfg.Username,
Password: sessionCfg.Password,
DB: sessionCfg.DB,
Protocol: 2,
DisableIdentity: true,
}
if sessionCfg.TLSEnabled {
options.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12}
}
return &RedisStore{
client: redis.NewClient(options),
client: client,
keyPrefix: replayCfg.KeyPrefix,
reserveTimeout: replayCfg.ReserveTimeout,
}, nil
}
// Close releases the underlying Redis client resources.
func (s *RedisStore) Close() error {
if s == nil || s.client == nil {
return nil
}
return s.client.Close()
}
// Ping verifies that the configured Redis backend is reachable within the
// replay reserve timeout budget.
func (s *RedisStore) Ping(ctx context.Context) error {
if s == nil || s.client == nil {
return errors.New("ping redis replay store: nil store")
}
if ctx == nil {
return errors.New("ping redis replay store: nil context")
}
pingCtx, cancel := context.WithTimeout(ctx, s.reserveTimeout)
defer cancel()
if err := s.client.Ping(pingCtx).Err(); err != nil {
return fmt.Errorf("ping redis replay store: %w", err)
}
return nil
}
// Reserve records the authenticated deviceSessionID and requestID pair for
// ttl. It rejects duplicates while the reservation remains active.
func (s *RedisStore) Reserve(ctx context.Context, deviceSessionID string, requestID string, ttl time.Duration) error {
+44 -86
View File
@@ -10,81 +10,64 @@ import (
"galaxy/gateway/internal/config"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newRedisClient(t *testing.T, addr string) *redis.Client {
t.Helper()
client := redis.NewClient(&redis.Options{
Addr: addr,
Protocol: 2,
DisableIdentity: true,
})
t.Cleanup(func() {
assert.NoError(t, client.Close())
})
return client
}
func TestNewRedisStore(t *testing.T) {
t.Parallel()
server := miniredis.RunT(t)
client := newRedisClient(t, server.Addr())
validCfg := config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
ReserveTimeout: 250 * time.Millisecond,
}
tests := []struct {
name string
sessionCfg config.SessionCacheRedisConfig
replayCfg config.ReplayRedisConfig
wantErr string
name string
client *redis.Client
cfg config.ReplayRedisConfig
wantErr string
}{
{name: "valid config", client: client, cfg: validCfg},
{name: "nil client", client: nil, cfg: validCfg, wantErr: "nil redis client"},
{
name: "valid config",
sessionCfg: config.SessionCacheRedisConfig{
Addr: server.Addr(),
DB: 2,
},
replayCfg: config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
ReserveTimeout: 250 * time.Millisecond,
},
},
{
name: "empty redis addr",
replayCfg: config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
ReserveTimeout: 250 * time.Millisecond,
},
wantErr: "redis addr must not be empty",
},
{
name: "negative redis db",
sessionCfg: config.SessionCacheRedisConfig{
Addr: server.Addr(),
DB: -1,
},
replayCfg: config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
ReserveTimeout: 250 * time.Millisecond,
},
wantErr: "redis db must not be negative",
},
{
name: "empty replay key prefix",
sessionCfg: config.SessionCacheRedisConfig{
Addr: server.Addr(),
},
replayCfg: config.ReplayRedisConfig{
ReserveTimeout: 250 * time.Millisecond,
},
name: "empty replay key prefix",
client: client,
cfg: config.ReplayRedisConfig{ReserveTimeout: 250 * time.Millisecond},
wantErr: "replay key prefix must not be empty",
},
{
name: "non-positive reserve timeout",
sessionCfg: config.SessionCacheRedisConfig{
Addr: server.Addr(),
},
replayCfg: config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
},
name: "non-positive reserve timeout",
client: client,
cfg: config.ReplayRedisConfig{KeyPrefix: "gateway:replay:"},
wantErr: "reserve timeout must be positive",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
store, err := NewRedisStore(tt.sessionCfg, tt.replayCfg)
store, err := NewRedisStore(tt.client, tt.cfg)
if tt.wantErr != "" {
require.Error(t, err)
require.ErrorContains(t, err, tt.wantErr)
@@ -92,28 +75,16 @@ func TestNewRedisStore(t *testing.T) {
}
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, store.Close())
})
require.NotNil(t, store)
})
}
}
func TestRedisStorePing(t *testing.T) {
t.Parallel()
server := miniredis.RunT(t)
store := newTestRedisStore(t, server, config.SessionCacheRedisConfig{}, config.ReplayRedisConfig{})
require.NoError(t, store.Ping(context.Background()))
}
func TestRedisStoreReserve(t *testing.T) {
t.Parallel()
tests := []struct {
name string
sessionCfg config.SessionCacheRedisConfig
replayCfg config.ReplayRedisConfig
deviceSessionID string
requestID string
@@ -170,13 +141,11 @@ func TestRedisStoreReserve(t *testing.T) {
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
server := miniredis.RunT(t)
store := newTestRedisStore(t, server, tt.sessionCfg, tt.replayCfg)
store := newTestRedisStore(t, server, tt.replayCfg)
err := store.Reserve(context.Background(), tt.deviceSessionID, tt.requestID, tt.ttl)
if tt.wantErrIs != nil || tt.wantErrText != "" {
@@ -201,17 +170,12 @@ func TestRedisStoreReserve(t *testing.T) {
func TestRedisStoreReserveReturnsBackendError(t *testing.T) {
t.Parallel()
store, err := NewRedisStore(
config.SessionCacheRedisConfig{Addr: unusedTCPAddr(t)},
config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
ReserveTimeout: 100 * time.Millisecond,
},
)
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, store.Close())
client := newRedisClient(t, unusedTCPAddr(t))
store, err := NewRedisStore(client, config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
ReserveTimeout: 100 * time.Millisecond,
})
require.NoError(t, err)
err = store.Reserve(context.Background(), "device-session-123", "request-123", 5*time.Second)
require.Error(t, err)
@@ -219,12 +183,9 @@ func TestRedisStoreReserveReturnsBackendError(t *testing.T) {
assert.ErrorContains(t, err, "reserve replay request in redis")
}
func newTestRedisStore(t *testing.T, server *miniredis.Miniredis, sessionCfg config.SessionCacheRedisConfig, replayCfg config.ReplayRedisConfig) *RedisStore {
func newTestRedisStore(t *testing.T, server *miniredis.Miniredis, replayCfg config.ReplayRedisConfig) *RedisStore {
t.Helper()
if sessionCfg.Addr == "" {
sessionCfg.Addr = server.Addr()
}
if replayCfg.KeyPrefix == "" {
replayCfg.KeyPrefix = "gateway:replay:"
}
@@ -232,11 +193,8 @@ func newTestRedisStore(t *testing.T, server *miniredis.Miniredis, sessionCfg con
replayCfg.ReserveTimeout = 250 * time.Millisecond
}
store, err := NewRedisStore(sessionCfg, replayCfg)
store, err := NewRedisStore(newRedisClient(t, server.Addr()), replayCfg)
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, store.Close())
})
return store
}
+9 -51
View File
@@ -3,7 +3,6 @@ package session
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
@@ -32,68 +31,27 @@ type redisRecord struct {
RevokedAtMS *int64 `json:"revoked_at_ms,omitempty"`
}
// NewRedisCache constructs a Redis-backed SessionCache from cfg. The returned
// cache is read-only from the gateway perspective and does not write or mutate
// Redis state.
func NewRedisCache(cfg config.SessionCacheRedisConfig) (*RedisCache, error) {
if strings.TrimSpace(cfg.Addr) == "" {
return nil, errors.New("new redis session cache: redis addr must not be empty")
// NewRedisCache constructs a Redis-backed SessionCache that uses client and
// applies the namespace and timeout settings from cfg. The cache does not own
// the client; the runtime supplies a shared *redis.Client.
func NewRedisCache(client *redis.Client, cfg config.SessionCacheRedisConfig) (*RedisCache, error) {
if client == nil {
return nil, errors.New("new redis session cache: nil redis client")
}
if cfg.DB < 0 {
return nil, errors.New("new redis session cache: redis db must not be negative")
if strings.TrimSpace(cfg.KeyPrefix) == "" {
return nil, errors.New("new redis session cache: redis key prefix must not be empty")
}
if cfg.LookupTimeout <= 0 {
return nil, errors.New("new redis session cache: lookup timeout must be positive")
}
options := &redis.Options{
Addr: cfg.Addr,
Username: cfg.Username,
Password: cfg.Password,
DB: cfg.DB,
Protocol: 2,
DisableIdentity: true,
}
if cfg.TLSEnabled {
options.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12}
}
return &RedisCache{
client: redis.NewClient(options),
client: client,
keyPrefix: cfg.KeyPrefix,
lookupTimeout: cfg.LookupTimeout,
}, nil
}
// Close releases the underlying Redis client resources.
func (c *RedisCache) Close() error {
if c == nil || c.client == nil {
return nil
}
return c.client.Close()
}
// Ping verifies that the configured Redis backend is reachable within the
// cache lookup timeout budget.
func (c *RedisCache) Ping(ctx context.Context) error {
if c == nil || c.client == nil {
return errors.New("ping redis session cache: nil cache")
}
if ctx == nil {
return errors.New("ping redis session cache: nil context")
}
pingCtx, cancel := context.WithTimeout(ctx, c.lookupTimeout)
defer cancel()
if err := c.client.Ping(pingCtx).Err(); err != nil {
return fmt.Errorf("ping redis session cache: %w", err)
}
return nil
}
// Lookup resolves deviceSessionID from Redis, validates the cached JSON
// payload strictly, and returns the decoded session record.
func (c *RedisCache) Lookup(ctx context.Context, deviceSessionID string) (Record, error) {
+37 -51
View File
@@ -10,61 +10,64 @@ import (
"galaxy/gateway/internal/config"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newRedisClient(t *testing.T, server *miniredis.Miniredis) *redis.Client {
t.Helper()
client := redis.NewClient(&redis.Options{
Addr: server.Addr(),
Protocol: 2,
DisableIdentity: true,
})
t.Cleanup(func() {
assert.NoError(t, client.Close())
})
return client
}
func TestNewRedisCache(t *testing.T) {
t.Parallel()
server := miniredis.RunT(t)
client := newRedisClient(t, server)
validCfg := config.SessionCacheRedisConfig{
KeyPrefix: "gateway:session:",
LookupTimeout: 250 * time.Millisecond,
}
tests := []struct {
name string
client *redis.Client
cfg config.SessionCacheRedisConfig
wantErr string
}{
{name: "valid config", client: client, cfg: validCfg},
{name: "nil client", client: nil, cfg: validCfg, wantErr: "nil redis client"},
{
name: "valid config",
cfg: config.SessionCacheRedisConfig{
Addr: server.Addr(),
DB: 2,
KeyPrefix: "gateway:session:",
LookupTimeout: 250 * time.Millisecond,
},
name: "empty key prefix",
client: client,
cfg: config.SessionCacheRedisConfig{LookupTimeout: 250 * time.Millisecond},
wantErr: "redis key prefix must not be empty",
},
{
name: "empty addr",
cfg: config.SessionCacheRedisConfig{
LookupTimeout: 250 * time.Millisecond,
},
wantErr: "redis addr must not be empty",
},
{
name: "negative db",
cfg: config.SessionCacheRedisConfig{
Addr: server.Addr(),
DB: -1,
LookupTimeout: 250 * time.Millisecond,
},
wantErr: "redis db must not be negative",
},
{
name: "non-positive lookup timeout",
cfg: config.SessionCacheRedisConfig{
Addr: server.Addr(),
},
name: "non-positive lookup timeout",
client: client,
cfg: config.SessionCacheRedisConfig{KeyPrefix: "gateway:session:"},
wantErr: "lookup timeout must be positive",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
cache, err := NewRedisCache(tt.cfg)
cache, err := NewRedisCache(tt.client, tt.cfg)
if tt.wantErr != "" {
require.Error(t, err)
require.ErrorContains(t, err, tt.wantErr)
@@ -72,22 +75,11 @@ func TestNewRedisCache(t *testing.T) {
}
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, cache.Close())
})
require.NotNil(t, cache)
})
}
}
func TestRedisCachePing(t *testing.T) {
t.Parallel()
server := miniredis.RunT(t)
cache := newTestRedisCache(t, server, config.SessionCacheRedisConfig{})
require.NoError(t, cache.Ping(context.Background()))
}
func TestRedisCacheLookup(t *testing.T) {
t.Parallel()
@@ -259,8 +251,6 @@ func TestRedisCacheLookup(t *testing.T) {
server := miniredis.RunT(t)
cfg := tt.cfg
cfg.Addr = server.Addr()
cfg.DB = 0
cfg.LookupTimeout = 250 * time.Millisecond
if tt.seed != nil {
@@ -292,20 +282,16 @@ func TestRedisCacheLookup(t *testing.T) {
func newTestRedisCache(t *testing.T, server *miniredis.Miniredis, cfg config.SessionCacheRedisConfig) *RedisCache {
t.Helper()
if cfg.Addr == "" {
cfg.Addr = server.Addr()
if cfg.KeyPrefix == "" {
cfg.KeyPrefix = "gateway:session:"
}
if cfg.LookupTimeout == 0 {
cfg.LookupTimeout = 250 * time.Millisecond
}
cache, err := NewRedisCache(cfg)
cache, err := NewRedisCache(newRedisClient(t, server), cfg)
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, cache.Close())
})
return cache
}
+21
View File
@@ -20,6 +20,7 @@ import (
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
oteltrace "go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
)
@@ -149,6 +150,26 @@ func (r *Runtime) Handler() http.Handler {
return r.promHandler
}
// TracerProvider returns the runtime tracer provider, falling back to the
// global one when r is not initialised.
func (r *Runtime) TracerProvider() oteltrace.TracerProvider {
if r == nil || r.tracerProvider == nil {
return otel.GetTracerProvider()
}
return r.tracerProvider
}
// MeterProvider returns the runtime meter provider, falling back to the
// global one when r is not initialised.
func (r *Runtime) MeterProvider() metric.MeterProvider {
if r == nil || r.meterProvider == nil {
return otel.GetMeterProvider()
}
return r.meterProvider
}
// Shutdown flushes the configured telemetry providers.
func (r *Runtime) Shutdown(ctx context.Context) error {
if r == nil {