feat: use postgres
This commit is contained in:
@@ -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
@@ -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
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user