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
+8 -4
View File
@@ -1,5 +1,6 @@
# Required startup settings.
GATEWAY_SESSION_CACHE_REDIS_ADDR=127.0.0.1:6379
GATEWAY_REDIS_MASTER_ADDR=127.0.0.1:6379
GATEWAY_REDIS_PASSWORD=changeme
GATEWAY_SESSION_EVENTS_REDIS_STREAM=gateway:session_events
GATEWAY_CLIENT_EVENTS_REDIS_STREAM=gateway:client_events
GATEWAY_RESPONSE_SIGNER_PRIVATE_KEY_PEM_PATH=./secrets/response-signer.pem
@@ -11,11 +12,14 @@ GATEWAY_AUTHENTICATED_GRPC_ADDR=127.0.0.1:9090
# Optional admin listener.
# GATEWAY_ADMIN_HTTP_ADDR=127.0.0.1:9091
# Optional Redis tuning.
# GATEWAY_SESSION_CACHE_REDIS_DB=0
# Optional Redis tuning. The legacy GATEWAY_REDIS_TLS_ENABLED and
# GATEWAY_REDIS_USERNAME variables are no longer accepted; see
# docs/redis-config.md.
# GATEWAY_REDIS_REPLICA_ADDRS=127.0.0.1:6479,127.0.0.1:6480
# GATEWAY_REDIS_DB=0
# GATEWAY_REDIS_OPERATION_TIMEOUT=250ms
# GATEWAY_SESSION_CACHE_REDIS_KEY_PREFIX=gateway:session:
# GATEWAY_REPLAY_REDIS_KEY_PREFIX=gateway:replay:
# GATEWAY_SESSION_CACHE_REDIS_TLS_ENABLED=false
# Optional public-auth integration. Without a configured Auth / Session Service
# base URL the routes stay mounted and return 503 service_unavailable.
+35 -12
View File
@@ -13,7 +13,8 @@
Required startup environment variables:
- `GATEWAY_SESSION_CACHE_REDIS_ADDR`
- `GATEWAY_REDIS_MASTER_ADDR`
- `GATEWAY_REDIS_PASSWORD`
- `GATEWAY_SESSION_EVENTS_REDIS_STREAM`
- `GATEWAY_CLIENT_EVENTS_REDIS_STREAM`
- `GATEWAY_RESPONSE_SIGNER_PRIVATE_KEY_PEM_PATH`
@@ -609,23 +610,45 @@ eviction policy. Session lifecycle events are the authoritative mechanism for
keeping the hot path current, while Redis fallback remains the safety net for
cold misses and process restarts.
The Redis fallback implementation uses `go-redis/v9`.
`cmd/gateway` requires the Redis fallback backend during startup, issues a
bounded `PING`, and refuses to start when Redis is misconfigured or
unavailable.
The Redis fallback implementation uses `go-redis/v9`. `cmd/gateway` opens one
shared `*redis.Client` via `pkg/redisconn` (instrumented with OpenTelemetry
tracing and metrics), issues a single bounded `PING` on startup, and refuses
to start when Redis is misconfigured or unavailable. The session cache,
replay store, session-events subscriber, and client-events subscriber all
use that shared client. See `docs/redis-config.md` for the rationale behind
the shape and the project-wide rules in
`ARCHITECTURE.md §Persistence Backends`.
Required environment variable:
Required Redis connection variables:
- `GATEWAY_SESSION_CACHE_REDIS_ADDR`
- `GATEWAY_REDIS_MASTER_ADDR`
- `GATEWAY_REDIS_PASSWORD`
Optional environment variables:
Optional Redis connection variables:
- `GATEWAY_REDIS_REPLICA_ADDRS` (comma-separated; reserved for future
read-routing — currently unused)
- `GATEWAY_REDIS_DB` with default `0`
- `GATEWAY_REDIS_OPERATION_TIMEOUT` with default `250ms`
> Removed: `GATEWAY_SESSION_CACHE_REDIS_ADDR`,
> `GATEWAY_SESSION_CACHE_REDIS_USERNAME`,
> `GATEWAY_SESSION_CACHE_REDIS_PASSWORD`,
> `GATEWAY_SESSION_CACHE_REDIS_DB`,
> `GATEWAY_SESSION_CACHE_REDIS_TLS_ENABLED`. `pkg/redisconn.LoadFromEnv`
> rejects the deprecated `GATEWAY_REDIS_TLS_ENABLED` and
> `GATEWAY_REDIS_USERNAME` variables at startup.
Per-subsystem Redis behavior variables (namespace, stream, timeouts):
- `GATEWAY_SESSION_CACHE_REDIS_USERNAME`
- `GATEWAY_SESSION_CACHE_REDIS_PASSWORD`
- `GATEWAY_SESSION_CACHE_REDIS_DB` with default `0`
- `GATEWAY_SESSION_CACHE_REDIS_KEY_PREFIX` with default `gateway:session:`
- `GATEWAY_SESSION_CACHE_REDIS_LOOKUP_TIMEOUT` with default `250ms`
- `GATEWAY_SESSION_CACHE_REDIS_TLS_ENABLED` with default `false`
- `GATEWAY_REPLAY_REDIS_KEY_PREFIX` with default `gateway:replay:`
- `GATEWAY_REPLAY_REDIS_RESERVE_TIMEOUT` with default `250ms`
- `GATEWAY_SESSION_EVENTS_REDIS_STREAM`
- `GATEWAY_SESSION_EVENTS_REDIS_READ_BLOCK_TIMEOUT` with default `1s`
- `GATEWAY_CLIENT_EVENTS_REDIS_STREAM`
- `GATEWAY_CLIENT_EVENTS_REDIS_READ_BLOCK_TIMEOUT` with default `1s`
The Redis key format is:
+42 -69
View File
@@ -18,11 +18,13 @@ import (
"galaxy/gateway/internal/grpcapi"
"galaxy/gateway/internal/logging"
"galaxy/gateway/internal/push"
"galaxy/gateway/internal/redisclient"
"galaxy/gateway/internal/replay"
"galaxy/gateway/internal/restapi"
"galaxy/gateway/internal/session"
"galaxy/gateway/internal/telemetry"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
@@ -132,112 +134,83 @@ func newAuthenticatedGRPCDependencies(ctx context.Context, cfg config.Config, lo
return grpcapi.ServerDependencies{}, nil, nil, fmt.Errorf("build authenticated grpc dependencies: load response signer: %w", err)
}
fallbackSessionCache, err := session.NewRedisCache(cfg.SessionCacheRedis)
if err != nil {
return grpcapi.ServerDependencies{}, nil, nil, fmt.Errorf("build authenticated grpc dependencies: %w", err)
}
replayStore, err := replay.NewRedisStore(cfg.SessionCacheRedis, cfg.ReplayRedis)
if err != nil {
closeErr := fallbackSessionCache.Close()
redisClient := redisclient.NewClient(cfg.Redis)
if err := redisclient.InstrumentClient(redisClient, telemetryRuntime); err != nil {
closeErr := redisClient.Close()
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: %w", err),
closeErr,
)
}
closeRedisClient := func() error {
err := redisClient.Close()
if errors.Is(err, redis.ErrClosed) {
return nil
}
return err
}
if err := redisclient.Ping(ctx, cfg.Redis, redisClient); err != nil {
closeErr := closeRedisClient()
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: %w", err),
closeErr,
)
}
fallbackSessionCache, err := session.NewRedisCache(redisClient, cfg.SessionCacheRedis)
if err != nil {
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: %w", err),
closeRedisClient(),
)
}
replayStore, err := replay.NewRedisStore(redisClient, cfg.ReplayRedis)
if err != nil {
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: %w", err),
closeRedisClient(),
)
}
localSessionCache := session.NewMemoryCache()
sessionCache, err := session.NewReadThroughCache(localSessionCache, fallbackSessionCache)
if err != nil {
closeErr := errors.Join(
fallbackSessionCache.Close(),
replayStore.Close(),
)
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: %w", err),
closeErr,
closeRedisClient(),
)
}
pushHub := push.NewHubWithObserver(0, telemetry.NewPushObserver(telemetryRuntime))
sessionSubscriber, err := events.NewRedisSessionSubscriberWithObservability(cfg.SessionCacheRedis, cfg.SessionEventsRedis, localSessionCache, pushHub, logger, telemetryRuntime)
sessionSubscriber, err := events.NewRedisSessionSubscriberWithObservability(redisClient, cfg.SessionCacheRedis, cfg.SessionEventsRedis, localSessionCache, pushHub, logger, telemetryRuntime)
if err != nil {
closeErr := errors.Join(
fallbackSessionCache.Close(),
replayStore.Close(),
)
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: %w", err),
closeErr,
closeRedisClient(),
)
}
clientEventSubscriber, err := events.NewRedisClientEventSubscriberWithObservability(cfg.SessionCacheRedis, cfg.ClientEventsRedis, pushHub, logger, telemetryRuntime)
clientEventSubscriber, err := events.NewRedisClientEventSubscriberWithObservability(redisClient, cfg.SessionCacheRedis, cfg.ClientEventsRedis, pushHub, logger, telemetryRuntime)
if err != nil {
closeErr := errors.Join(
fallbackSessionCache.Close(),
replayStore.Close(),
sessionSubscriber.Close(),
)
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: %w", err),
closeErr,
closeRedisClient(),
)
}
userRoutes, closeUserServiceRoutes, err := userservice.NewRoutes(cfg.UserService.BaseURL)
if err != nil {
closeErr := errors.Join(
fallbackSessionCache.Close(),
replayStore.Close(),
sessionSubscriber.Close(),
clientEventSubscriber.Close(),
)
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: user service routes: %w", err),
closeErr,
closeRedisClient(),
)
}
cleanup := func() error {
return errors.Join(
fallbackSessionCache.Close(),
replayStore.Close(),
sessionSubscriber.Close(),
clientEventSubscriber.Close(),
closeUserServiceRoutes(),
)
}
if err := fallbackSessionCache.Ping(ctx); err != nil {
closeErr := cleanup()
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: %w", err),
closeErr,
)
}
if err := replayStore.Ping(ctx); err != nil {
closeErr := cleanup()
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: %w", err),
closeErr,
)
}
if err := sessionSubscriber.Ping(ctx); err != nil {
closeErr := cleanup()
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: %w", err),
closeErr,
)
}
if err := clientEventSubscriber.Ping(ctx); err != nil {
closeErr := cleanup()
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: %w", err),
closeErr,
closeRedisClient(),
)
}
+21 -9
View File
@@ -15,6 +15,7 @@ import (
"galaxy/gateway/internal/config"
"galaxy/gateway/internal/restapi"
"galaxy/redisconn"
"github.com/alicebob/miniredis/v2"
"github.com/stretchr/testify/assert"
@@ -22,6 +23,16 @@ import (
"go.uber.org/zap"
)
func testRedisConn(masterAddr string, opTimeout time.Duration) redisconn.Config {
cfg := redisconn.DefaultConfig()
cfg.MasterAddr = masterAddr
cfg.Password = "integration"
if opTimeout > 0 {
cfg.OperationTimeout = opTimeout
}
return cfg
}
func TestNewPublicRESTDependencies(t *testing.T) {
t.Parallel()
@@ -102,8 +113,8 @@ func TestNewAuthenticatedGRPCDependencies(t *testing.T) {
{
name: "success",
cfg: config.Config{
Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
SessionCacheRedis: config.SessionCacheRedisConfig{
Addr: server.Addr(),
KeyPrefix: "gateway:session:",
LookupTimeout: 250 * time.Millisecond,
},
@@ -125,8 +136,9 @@ func TestNewAuthenticatedGRPCDependencies(t *testing.T) {
},
},
{
name: "invalid redis config",
name: "invalid session cache key prefix",
cfg: config.Config{
Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
SessionCacheRedis: config.SessionCacheRedisConfig{
LookupTimeout: 250 * time.Millisecond,
},
@@ -146,13 +158,13 @@ func TestNewAuthenticatedGRPCDependencies(t *testing.T) {
PrivateKeyPEMPath: responseSignerPEMPath,
},
},
wantErr: "redis addr must not be empty",
wantErr: "redis key prefix must not be empty",
},
{
name: "startup ping failure",
cfg: config.Config{
Redis: testRedisConn(unusedTCPAddr(t), 100*time.Millisecond),
SessionCacheRedis: config.SessionCacheRedisConfig{
Addr: unusedTCPAddr(t),
KeyPrefix: "gateway:session:",
LookupTimeout: 100 * time.Millisecond,
},
@@ -172,13 +184,13 @@ func TestNewAuthenticatedGRPCDependencies(t *testing.T) {
PrivateKeyPEMPath: responseSignerPEMPath,
},
},
wantErr: "ping redis session cache",
wantErr: "ping redis",
},
{
name: "invalid replay config",
cfg: config.Config{
Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
SessionCacheRedis: config.SessionCacheRedisConfig{
Addr: server.Addr(),
KeyPrefix: "gateway:session:",
LookupTimeout: 250 * time.Millisecond,
},
@@ -202,8 +214,8 @@ func TestNewAuthenticatedGRPCDependencies(t *testing.T) {
{
name: "invalid client event config",
cfg: config.Config{
Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
SessionCacheRedis: config.SessionCacheRedisConfig{
Addr: server.Addr(),
KeyPrefix: "gateway:session:",
LookupTimeout: 250 * time.Millisecond,
},
@@ -227,8 +239,8 @@ func TestNewAuthenticatedGRPCDependencies(t *testing.T) {
{
name: "missing response signer path",
cfg: config.Config{
Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
SessionCacheRedis: config.SessionCacheRedisConfig{
Addr: server.Addr(),
KeyPrefix: "gateway:session:",
LookupTimeout: 250 * time.Millisecond,
},
@@ -250,8 +262,8 @@ func TestNewAuthenticatedGRPCDependencies(t *testing.T) {
{
name: "invalid response signer pem",
cfg: config.Config{
Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
SessionCacheRedis: config.SessionCacheRedisConfig{
Addr: server.Addr(),
KeyPrefix: "gateway:session:",
LookupTimeout: 250 * time.Millisecond,
},
+109
View File
@@ -0,0 +1,109 @@
# Decision: Redis configuration shape
PG_PLAN.md §7. Captures the standing rules adopted by Edge Gateway when it
joined the project-wide Redis topology defined in
`ARCHITECTURE.md §Persistence Backends`.
## Context
Gateway intentionally stays Redis-only. All gateway state Redis serves is
TTL-bounded or runtime-coordination state:
- the session cache is a read-through projection of authsession's
source-of-truth session records (rebuildable via re-authentication);
- the replay store is a short-lived `SETNX` reservation namespace per
authenticated request (`GATEWAY_REPLAY_REDIS_RESERVE_TIMEOUT`);
- the session-events stream is a runtime fan-out of session lifecycle
updates;
- the client-events stream is a runtime push fan-out.
Stage 7 brought gateway in line with the steady-state rules established in
Stage 0: every Galaxy service uses one master plus zero-or-more replicas
with a mandatory password, no TLS, and no Redis ACL username; the connection
is configured by the shared `pkg/redisconn` helper.
## Decisions
### One shared `*redis.Client` owned by the runtime
`cmd/gateway/main.go` constructs a single `*redis.Client` via
`internal/redisclient.NewClient`, attaches OpenTelemetry tracing and metrics
via `internal/redisclient.InstrumentClient`, performs one bounded `PING`
via `internal/redisclient.Ping`, and registers `client.Close` for shutdown.
The session cache, replay store, session-events subscriber, and
client-events subscriber all receive this same client.
Adapters no longer build or own a Redis client. Their `Config` structs hold
only behavior settings (key prefix, stream name, per-subsystem timeouts).
Adapter constructors take `(*redis.Client, …)`. The stream subscribers'
`Close`/`Shutdown` methods became no-ops; the runtime's context cancellation
unblocks the `XRead` loop and the runtime closes the shared client.
### One env-var prefix for the connection
Connection topology is loaded from a single `GATEWAY_REDIS_*` group via
`redisconn.LoadFromEnv("GATEWAY")`:
- `GATEWAY_REDIS_MASTER_ADDR` (required)
- `GATEWAY_REDIS_REPLICA_ADDRS` (optional, comma-separated; currently
unused, reserved for future read-routing)
- `GATEWAY_REDIS_PASSWORD` (required)
- `GATEWAY_REDIS_DB` (default `0`)
- `GATEWAY_REDIS_OPERATION_TIMEOUT` (default `250ms`)
Per-subsystem behavior env vars keep their existing prefixes — they do not
describe connection topology, only namespace and timing:
- `GATEWAY_SESSION_CACHE_REDIS_KEY_PREFIX`,
`GATEWAY_SESSION_CACHE_REDIS_LOOKUP_TIMEOUT`
- `GATEWAY_REPLAY_REDIS_KEY_PREFIX`,
`GATEWAY_REPLAY_REDIS_RESERVE_TIMEOUT`
- `GATEWAY_SESSION_EVENTS_REDIS_STREAM`,
`GATEWAY_SESSION_EVENTS_REDIS_READ_BLOCK_TIMEOUT`
- `GATEWAY_CLIENT_EVENTS_REDIS_STREAM`,
`GATEWAY_CLIENT_EVENTS_REDIS_READ_BLOCK_TIMEOUT`
### Retired env vars (hard removal)
The following variables are no longer read or honored:
- `GATEWAY_SESSION_CACHE_REDIS_ADDR` — replaced by
`GATEWAY_REDIS_MASTER_ADDR`.
- `GATEWAY_SESSION_CACHE_REDIS_USERNAME` — Redis ACL not used.
- `GATEWAY_SESSION_CACHE_REDIS_PASSWORD` — replaced by
`GATEWAY_REDIS_PASSWORD`.
- `GATEWAY_SESSION_CACHE_REDIS_DB` — replaced by `GATEWAY_REDIS_DB`.
- `GATEWAY_SESSION_CACHE_REDIS_TLS_ENABLED` — TLS disabled by policy.
`pkg/redisconn.LoadFromEnv` rejects `GATEWAY_REDIS_TLS_ENABLED` and
`GATEWAY_REDIS_USERNAME` at startup with a clear error pointing to
`ARCHITECTURE.md §Persistence Backends`.
> **Compound legacy prefixes (`GATEWAY_SESSION_CACHE_REDIS_USERNAME` etc.)
> are not actively rejected.** `pkg/redisconn`'s deprecated-env detector
> only watches the canonical `GATEWAY_REDIS_*` form. The compound legacy
> vars become silently inert. The architecture rule explicitly accepts this
> ("no backward-compat shim — fresh project, no production deploys to
> migrate"); operators upgrading should remove the variables from their
> deployment manifests.
### Telemetry
`redisconn.Instrument` wires `redisotel.InstrumentTracing` (with
`WithDBStatement(false)`) and `redisotel.InstrumentMetrics`. This is the
first gateway release that emits Redis tracing and connection-pool metrics;
downstream dashboards will start populating without further changes.
## Consequences
- Gateway test code that previously constructed a Redis client per adapter
must now construct one client and pass it to every adapter under test
(see `internal/session/redis_test.go`, `internal/replay/redis_test.go`,
`internal/events/subscriber_test.go`,
`internal/events/client_subscriber_test.go`).
- Operators must set `GATEWAY_REDIS_PASSWORD`. A passwordless local Redis
is still acceptable as long as a placeholder password is supplied to the
binary; Redis without `requirepass` accepts AUTH unconditionally.
- The integration test harness passes `GATEWAY_REDIS_PASSWORD =
"integration"` alongside `GATEWAY_REDIS_MASTER_ADDR` (see
`integration/internal/harness/gatewayservice.go`).
+17 -14
View File
@@ -7,25 +7,28 @@ readiness, shutdown, and push or revoke incidents.
Before starting the process, confirm:
- `GATEWAY_SESSION_CACHE_REDIS_ADDR` points to the Redis deployment used for
session lookup and both internal event streams.
- `GATEWAY_REDIS_MASTER_ADDR` and `GATEWAY_REDIS_PASSWORD` point to the Redis
deployment used for session lookup, replay reservations, session-events
consumption, and client-events fan-out. Optional read replicas may be
listed in `GATEWAY_REDIS_REPLICA_ADDRS` (currently unused; reserved for
future read-routing).
- `GATEWAY_SESSION_EVENTS_REDIS_STREAM` and
`GATEWAY_CLIENT_EVENTS_REDIS_STREAM` reference existing Redis Stream keys or
the names publishers will use.
`GATEWAY_CLIENT_EVENTS_REDIS_STREAM` reference existing Redis Stream keys
or the names publishers will use.
- `GATEWAY_RESPONSE_SIGNER_PRIVATE_KEY_PEM_PATH` points to a readable PKCS#8
PEM-encoded Ed25519 private key.
- the configured Redis ACL, DB, TLS, and key-prefix settings match the target
environment.
- the configured Redis DB and key-prefix settings match the target
environment. Per `ARCHITECTURE.md §Persistence Backends`, Redis traffic is
password-protected and TLS is disabled by policy; the deprecated
`GATEWAY_REDIS_TLS_ENABLED` and `GATEWAY_REDIS_USERNAME` variables are no
longer accepted and cause a hard fail at startup.
At startup the process performs bounded `PING` checks for:
At startup the process opens one shared `*redis.Client` (instrumented via
OpenTelemetry tracing and metrics) and performs one bounded `PING`. The
session cache, replay store, session-events subscriber, and client-events
subscriber all use that client.
- the Redis-backed session cache adapter;
- the replay store;
- the session event subscriber;
- the client event subscriber.
Startup fails fast if any of those checks fail or if the signer key cannot be
loaded.
Startup fails fast if the ping fails or if the signer key cannot be loaded.
Expected listener state after a healthy start:
+13 -8
View File
@@ -1,10 +1,11 @@
module galaxy/gateway
go 1.26.0
go 1.26.1
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1
buf.build/go/protovalidate v1.1.3
galaxy/redisconn v0.0.0-00010101000000-000000000000
github.com/alicebob/miniredis/v2 v2.37.0
github.com/getkin/kin-openapi v0.135.0
github.com/gin-gonic/gin v1.12.0
@@ -61,7 +62,7 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.21 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
@@ -77,6 +78,8 @@ require (
github.com/prometheus/procfs v0.20.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 // indirect
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/woodsbury/decimal128 v1.3.0 // indirect
@@ -86,14 +89,16 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/arch v0.25.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sys v0.43.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace galaxy/redisconn => ../pkg/redisconn
+20 -15
View File
@@ -83,6 +83,7 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -95,8 +96,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -131,6 +132,10 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 h1:QY4nmPHLFAJjtT5O4OMUEOxP8WVaRNOFpcbmxT2NLZU=
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0/go.mod h1:WH8cY/0fT41Bsf341qzo8v4nx0GCE8FykAA23IVbVmo=
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0 h1:2dKdoEYBJ0CZCLPiCdvvc7luz3DPwY6hKdzjL6m1eHE=
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0/go.mod h1:WzkrVG9ro9BwCQD0eJOWn6AGL4Z1CleGflM45w1hu10=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8=
@@ -196,8 +201,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
@@ -206,24 +211,24 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+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 {