feat: use postgres
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package redisconn
|
||||
|
||||
import (
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// NewMasterClient builds the master Redis client from cfg using the
|
||||
// conservative options shared by the existing service publishers
|
||||
// (`Protocol: 2`, `DisableIdentity: true`, dial/read/write timeouts bound to
|
||||
// cfg.OperationTimeout).
|
||||
//
|
||||
// NewMasterClient does not validate cfg; callers are expected to call
|
||||
// Config.Validate (or LoadFromEnv which already does) before invoking this.
|
||||
func NewMasterClient(cfg Config) *redis.Client {
|
||||
return redis.NewClient(buildOptions(cfg.MasterAddr, cfg))
|
||||
}
|
||||
|
||||
// NewReplicaClients builds one Redis client per replica address. It returns
|
||||
// nil when no replicas are configured; callers treat that as "no replicas
|
||||
// wired".
|
||||
func NewReplicaClients(cfg Config) []*redis.Client {
|
||||
if len(cfg.ReplicaAddrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
clients := make([]*redis.Client, 0, len(cfg.ReplicaAddrs))
|
||||
for _, addr := range cfg.ReplicaAddrs {
|
||||
clients = append(clients, redis.NewClient(buildOptions(addr, cfg)))
|
||||
}
|
||||
return clients
|
||||
}
|
||||
|
||||
func buildOptions(addr string, cfg Config) *redis.Options {
|
||||
return &redis.Options{
|
||||
Addr: addr,
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
Protocol: 2,
|
||||
DisableIdentity: true,
|
||||
DialTimeout: cfg.OperationTimeout,
|
||||
ReadTimeout: cfg.OperationTimeout,
|
||||
WriteTimeout: cfg.OperationTimeout,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Package redisconn provides shared helpers for opening, instrumenting and
|
||||
// pinging Redis connections used by Galaxy services.
|
||||
//
|
||||
// The package codifies the steady-state rules captured in `ARCHITECTURE.md`
|
||||
// `§Persistence Backends`: each service connects to one master plus
|
||||
// zero-or-more replicas with a mandatory password, no TLS, and no
|
||||
// `USERNAME`/ACL. The deprecated env vars `*_REDIS_TLS_ENABLED` and
|
||||
// `*_REDIS_USERNAME` are rejected by LoadFromEnv with a clear startup error.
|
||||
package redisconn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Default configuration values applied by DefaultConfig and LoadFromEnv when
|
||||
// the corresponding environment variable is absent.
|
||||
const (
|
||||
DefaultDB = 0
|
||||
DefaultOperationTimeout = 250 * time.Millisecond
|
||||
)
|
||||
|
||||
// Config stores the connection settings for one master plus zero-or-more
|
||||
// replica Redis instances. Stage 1 wires only the master; the replica list is
|
||||
// preserved so future read-routing is a non-breaking change.
|
||||
type Config struct {
|
||||
// MasterAddr stores the Redis network address in host:port form. Required.
|
||||
MasterAddr string
|
||||
|
||||
// ReplicaAddrs stores zero-or-more read-only replica addresses.
|
||||
ReplicaAddrs []string
|
||||
|
||||
// Password is the mandatory connection password. Empty values are rejected
|
||||
// by Validate to enforce the architectural rule that Redis traffic is
|
||||
// password-protected even on the trusted segment.
|
||||
Password string
|
||||
|
||||
// DB selects the logical Redis database index.
|
||||
DB int
|
||||
|
||||
// OperationTimeout bounds individual Redis round trips.
|
||||
OperationTimeout time.Duration
|
||||
}
|
||||
|
||||
// DefaultConfig returns the default tuning. MasterAddr and Password remain
|
||||
// zero-valued and must be supplied by callers (or by LoadFromEnv).
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
DB: DefaultDB,
|
||||
OperationTimeout: DefaultOperationTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate reports whether cfg is usable.
|
||||
func (cfg Config) Validate() error {
|
||||
if strings.TrimSpace(cfg.MasterAddr) == "" {
|
||||
return errors.New("redis master addr must not be empty")
|
||||
}
|
||||
if strings.TrimSpace(cfg.Password) == "" {
|
||||
return errors.New("redis password must not be empty")
|
||||
}
|
||||
for index, addr := range cfg.ReplicaAddrs {
|
||||
if strings.TrimSpace(addr) == "" {
|
||||
return fmt.Errorf("redis replica addr at index %d must not be empty", index)
|
||||
}
|
||||
}
|
||||
if cfg.DB < 0 {
|
||||
return errors.New("redis db must not be negative")
|
||||
}
|
||||
if cfg.OperationTimeout <= 0 {
|
||||
return errors.New("redis operation timeout must be positive")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadFromEnv populates Config from environment variables prefixed with
|
||||
// `<prefix>_REDIS_`. The required variables are
|
||||
// `<prefix>_REDIS_MASTER_ADDR` and `<prefix>_REDIS_PASSWORD`; every other
|
||||
// variable falls back to DefaultConfig values.
|
||||
//
|
||||
// LoadFromEnv hard-fails when either of the deprecated variables
|
||||
// `<prefix>_REDIS_TLS_ENABLED` or `<prefix>_REDIS_USERNAME` is set in the
|
||||
// environment, with an error pointing to ARCHITECTURE.md.
|
||||
func LoadFromEnv(prefix string) (Config, error) {
|
||||
if strings.TrimSpace(prefix) == "" {
|
||||
return Config{}, errors.New("redis env prefix must not be empty")
|
||||
}
|
||||
|
||||
if err := rejectDeprecatedEnv(prefix); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
cfg := DefaultConfig()
|
||||
|
||||
masterName := envName(prefix, "MASTER_ADDR")
|
||||
master, ok := os.LookupEnv(masterName)
|
||||
if !ok || strings.TrimSpace(master) == "" {
|
||||
return Config{}, fmt.Errorf("%s must be set", masterName)
|
||||
}
|
||||
cfg.MasterAddr = strings.TrimSpace(master)
|
||||
|
||||
passwordName := envName(prefix, "PASSWORD")
|
||||
password, ok := os.LookupEnv(passwordName)
|
||||
if !ok || strings.TrimSpace(password) == "" {
|
||||
return Config{}, fmt.Errorf("%s must be set", passwordName)
|
||||
}
|
||||
cfg.Password = strings.TrimSpace(password)
|
||||
|
||||
if raw, ok := os.LookupEnv(envName(prefix, "REPLICA_ADDRS")); ok {
|
||||
cfg.ReplicaAddrs = splitCSV(raw)
|
||||
}
|
||||
|
||||
db, err := loadInt(envName(prefix, "DB"), cfg.DB)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
cfg.DB = db
|
||||
|
||||
timeout, err := loadDuration(envName(prefix, "OPERATION_TIMEOUT"), cfg.OperationTimeout)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
cfg.OperationTimeout = timeout
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func rejectDeprecatedEnv(prefix string) error {
|
||||
for _, suffix := range []string{"TLS_ENABLED", "USERNAME"} {
|
||||
name := envName(prefix, suffix)
|
||||
if _, ok := os.LookupEnv(name); ok {
|
||||
return fmt.Errorf("%s is no longer supported (see ARCHITECTURE.md §Persistence Backends); unset it before starting the service", name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func envName(prefix, suffix string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(prefix)) + "_REDIS_" + suffix
|
||||
}
|
||||
|
||||
func splitCSV(raw string) []string {
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, trimmed)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func loadDuration(name string, fallback time.Duration) (time.Duration, error) {
|
||||
raw, ok := os.LookupEnv(name)
|
||||
if !ok {
|
||||
return fallback, nil
|
||||
}
|
||||
parsed, err := time.ParseDuration(strings.TrimSpace(raw))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func loadInt(name string, fallback int) (int, error) {
|
||||
raw, ok := os.LookupEnv(name)
|
||||
if !ok {
|
||||
return fallback, nil
|
||||
}
|
||||
parsed, err := strconv.Atoi(strings.TrimSpace(raw))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
module galaxy/redisconn
|
||||
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/alicebob/miniredis/v2 v2.37.0
|
||||
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0
|
||||
github.com/redis/go-redis/v9 v9.18.0
|
||||
go.opentelemetry.io/otel/metric v1.43.0
|
||||
go.opentelemetry.io/otel/trace v1.43.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 // indirect
|
||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68=
|
||||
github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,31 @@
|
||||
package redisconn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// Ping bounds client.Ping under timeout and returns a wrapped error so
|
||||
// startup failures are easy to spot in service logs.
|
||||
//
|
||||
// timeout is typically taken from Config.OperationTimeout.
|
||||
func Ping(ctx context.Context, client *redis.Client, timeout time.Duration) error {
|
||||
if client == nil {
|
||||
return errors.New("ping redis: nil client")
|
||||
}
|
||||
if timeout <= 0 {
|
||||
return errors.New("ping redis: timeout must be positive")
|
||||
}
|
||||
|
||||
pingCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
if err := client.Ping(pingCtx).Err(); err != nil {
|
||||
return fmt.Errorf("ping redis: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package redisconn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/redis/go-redis/extra/redisotel/v9"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Option configures the OpenTelemetry providers attached to a client by
|
||||
// Instrument. Unset providers fall back to the OpenTelemetry global
|
||||
// providers (matching `redisotel` defaults).
|
||||
type Option func(*options)
|
||||
|
||||
type options struct {
|
||||
tracerProvider trace.TracerProvider
|
||||
meterProvider metric.MeterProvider
|
||||
}
|
||||
|
||||
// WithTracerProvider sets the tracer provider used for Redis command spans.
|
||||
func WithTracerProvider(tp trace.TracerProvider) Option {
|
||||
return func(o *options) {
|
||||
o.tracerProvider = tp
|
||||
}
|
||||
}
|
||||
|
||||
// WithMeterProvider sets the meter provider used for Redis client metrics.
|
||||
func WithMeterProvider(mp metric.MeterProvider) Option {
|
||||
return func(o *options) {
|
||||
o.meterProvider = mp
|
||||
}
|
||||
}
|
||||
|
||||
func evalOptions(opts []Option) options {
|
||||
var resolved options
|
||||
for _, opt := range opts {
|
||||
if opt == nil {
|
||||
continue
|
||||
}
|
||||
opt(&resolved)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
// Instrument attaches Redis tracing and metrics to client. Tracing is
|
||||
// configured with `WithDBStatement(false)` so that only the command name is
|
||||
// captured, matching the existing instrumentation in the user, lobby, and
|
||||
// notification services.
|
||||
func Instrument(client *redis.Client, opts ...Option) error {
|
||||
if client == nil {
|
||||
return errors.New("instrument redis client: nil client")
|
||||
}
|
||||
|
||||
resolved := evalOptions(opts)
|
||||
|
||||
traceOpts := []redisotel.TracingOption{redisotel.WithDBStatement(false)}
|
||||
if resolved.tracerProvider != nil {
|
||||
traceOpts = append(traceOpts, redisotel.WithTracerProvider(resolved.tracerProvider))
|
||||
}
|
||||
if err := redisotel.InstrumentTracing(client, traceOpts...); err != nil {
|
||||
return fmt.Errorf("instrument redis client tracing: %w", err)
|
||||
}
|
||||
|
||||
metricOpts := []redisotel.MetricsOption{}
|
||||
if resolved.meterProvider != nil {
|
||||
metricOpts = append(metricOpts, redisotel.WithMeterProvider(resolved.meterProvider))
|
||||
}
|
||||
if err := redisotel.InstrumentMetrics(client, metricOpts...); err != nil {
|
||||
return fmt.Errorf("instrument redis client metrics: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package redisconn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"galaxy/redisconn"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"go.opentelemetry.io/otel/metric/noop"
|
||||
tracenoop "go.opentelemetry.io/otel/trace/noop"
|
||||
)
|
||||
|
||||
func TestDefaultConfigReturnsExpectedTuning(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := redisconn.DefaultConfig()
|
||||
if cfg.OperationTimeout != redisconn.DefaultOperationTimeout {
|
||||
t.Fatalf("operation timeout = %v, want %v", cfg.OperationTimeout, redisconn.DefaultOperationTimeout)
|
||||
}
|
||||
if cfg.DB != redisconn.DefaultDB {
|
||||
t.Fatalf("db = %d, want %d", cfg.DB, redisconn.DefaultDB)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidateRejectsInvalidValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*redisconn.Config)
|
||||
wantSub string
|
||||
}{
|
||||
{
|
||||
name: "missing master",
|
||||
mutate: func(c *redisconn.Config) {
|
||||
c.MasterAddr = ""
|
||||
},
|
||||
wantSub: "master addr",
|
||||
},
|
||||
{
|
||||
name: "missing password",
|
||||
mutate: func(c *redisconn.Config) {
|
||||
c.Password = ""
|
||||
},
|
||||
wantSub: "password",
|
||||
},
|
||||
{
|
||||
name: "blank replica entry",
|
||||
mutate: func(c *redisconn.Config) {
|
||||
c.ReplicaAddrs = []string{" "}
|
||||
},
|
||||
wantSub: "replica addr",
|
||||
},
|
||||
{
|
||||
name: "negative db",
|
||||
mutate: func(c *redisconn.Config) {
|
||||
c.DB = -1
|
||||
},
|
||||
wantSub: "db must not be negative",
|
||||
},
|
||||
{
|
||||
name: "non-positive timeout",
|
||||
mutate: func(c *redisconn.Config) {
|
||||
c.OperationTimeout = 0
|
||||
},
|
||||
wantSub: "operation timeout",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := redisconn.DefaultConfig()
|
||||
cfg.MasterAddr = "127.0.0.1:6379"
|
||||
cfg.Password = "secret"
|
||||
tt.mutate(&cfg)
|
||||
|
||||
err := cfg.Validate()
|
||||
if err == nil {
|
||||
t.Fatalf("expected validate error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.wantSub) {
|
||||
t.Fatalf("error %q does not contain %q", err, tt.wantSub)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromEnvHappyPath(t *testing.T) {
|
||||
const prefix = "TESTSVC"
|
||||
t.Setenv(prefix+"_REDIS_MASTER_ADDR", "127.0.0.1:6379")
|
||||
t.Setenv(prefix+"_REDIS_REPLICA_ADDRS", "127.0.0.1:6380, 127.0.0.1:6381 ,")
|
||||
t.Setenv(prefix+"_REDIS_PASSWORD", "secret")
|
||||
t.Setenv(prefix+"_REDIS_DB", "3")
|
||||
t.Setenv(prefix+"_REDIS_OPERATION_TIMEOUT", "500ms")
|
||||
|
||||
cfg, err := redisconn.LoadFromEnv(prefix)
|
||||
if err != nil {
|
||||
t.Fatalf("load from env: %v", err)
|
||||
}
|
||||
if cfg.MasterAddr != "127.0.0.1:6379" {
|
||||
t.Fatalf("master addr = %q", cfg.MasterAddr)
|
||||
}
|
||||
if cfg.Password != "secret" {
|
||||
t.Fatalf("password = %q", cfg.Password)
|
||||
}
|
||||
if got, want := cfg.DB, 3; got != want {
|
||||
t.Fatalf("db = %d, want %d", got, want)
|
||||
}
|
||||
if got, want := cfg.OperationTimeout, 500*time.Millisecond; got != want {
|
||||
t.Fatalf("operation timeout = %v, want %v", got, want)
|
||||
}
|
||||
if got, want := len(cfg.ReplicaAddrs), 2; got != want {
|
||||
t.Fatalf("replica count = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromEnvRejectsDeprecatedTLSEnabled(t *testing.T) {
|
||||
const prefix = "TESTSVC"
|
||||
t.Setenv(prefix+"_REDIS_MASTER_ADDR", "127.0.0.1:6379")
|
||||
t.Setenv(prefix+"_REDIS_PASSWORD", "secret")
|
||||
t.Setenv(prefix+"_REDIS_TLS_ENABLED", "true")
|
||||
|
||||
_, err := redisconn.LoadFromEnv(prefix)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when TLS_ENABLED is set")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "TLS_ENABLED") {
|
||||
t.Fatalf("error %q should name TLS_ENABLED", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "ARCHITECTURE.md") {
|
||||
t.Fatalf("error %q should reference ARCHITECTURE.md", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromEnvRejectsDeprecatedUsername(t *testing.T) {
|
||||
const prefix = "TESTSVC"
|
||||
t.Setenv(prefix+"_REDIS_MASTER_ADDR", "127.0.0.1:6379")
|
||||
t.Setenv(prefix+"_REDIS_PASSWORD", "secret")
|
||||
t.Setenv(prefix+"_REDIS_USERNAME", "anything")
|
||||
|
||||
_, err := redisconn.LoadFromEnv(prefix)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when USERNAME is set")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "USERNAME") {
|
||||
t.Fatalf("error %q should name USERNAME", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromEnvRequiresPassword(t *testing.T) {
|
||||
const prefix = "TESTSVC"
|
||||
t.Setenv(prefix+"_REDIS_MASTER_ADDR", "127.0.0.1:6379")
|
||||
t.Setenv(prefix+"_REDIS_PASSWORD", "")
|
||||
|
||||
if _, err := redisconn.LoadFromEnv(prefix); err == nil {
|
||||
t.Fatal("expected error when password is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewMasterClientPingsMiniredis(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := miniredis.RunT(t)
|
||||
server.RequireAuth("secret")
|
||||
|
||||
cfg := redisconn.DefaultConfig()
|
||||
cfg.MasterAddr = server.Addr()
|
||||
cfg.Password = "secret"
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("validate: %v", err)
|
||||
}
|
||||
|
||||
client := redisconn.NewMasterClient(cfg)
|
||||
t.Cleanup(func() {
|
||||
_ = client.Close()
|
||||
})
|
||||
|
||||
if err := redisconn.Ping(context.Background(), client, cfg.OperationTimeout); err != nil {
|
||||
t.Fatalf("ping miniredis: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewReplicaClientsReturnsExpectedLength(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server1 := miniredis.RunT(t)
|
||||
server2 := miniredis.RunT(t)
|
||||
|
||||
cfg := redisconn.DefaultConfig()
|
||||
cfg.MasterAddr = "ignored:6379"
|
||||
cfg.Password = "secret"
|
||||
cfg.ReplicaAddrs = []string{server1.Addr(), server2.Addr()}
|
||||
|
||||
clients := redisconn.NewReplicaClients(cfg)
|
||||
t.Cleanup(func() {
|
||||
for _, client := range clients {
|
||||
_ = client.Close()
|
||||
}
|
||||
})
|
||||
|
||||
if got, want := len(clients), 2; got != want {
|
||||
t.Fatalf("client count = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewReplicaClientsReturnsNilWhenUnconfigured(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := redisconn.DefaultConfig()
|
||||
cfg.MasterAddr = "ignored:6379"
|
||||
cfg.Password = "secret"
|
||||
|
||||
if clients := redisconn.NewReplicaClients(cfg); clients != nil {
|
||||
t.Fatalf("clients = %v, want nil", clients)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstrumentAcceptsNoopProviders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := miniredis.RunT(t)
|
||||
server.RequireAuth("secret")
|
||||
|
||||
cfg := redisconn.DefaultConfig()
|
||||
cfg.MasterAddr = server.Addr()
|
||||
cfg.Password = "secret"
|
||||
|
||||
client := redisconn.NewMasterClient(cfg)
|
||||
t.Cleanup(func() {
|
||||
_ = client.Close()
|
||||
})
|
||||
|
||||
err := redisconn.Instrument(
|
||||
client,
|
||||
redisconn.WithTracerProvider(tracenoop.NewTracerProvider()),
|
||||
redisconn.WithMeterProvider(noop.NewMeterProvider()),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("instrument: %v", err)
|
||||
}
|
||||
|
||||
if err := redisconn.Ping(context.Background(), client, cfg.OperationTimeout); err != nil {
|
||||
t.Fatalf("ping after instrument: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstrumentRejectsNilClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if err := redisconn.Instrument(nil); err == nil {
|
||||
t.Fatal("expected error for nil client")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user