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