package db import ( "context" "log" "os" "strconv" "strings" "time" "github.com/redis/go-redis/v9" ) // Rdb is the shared Redis connection, or nil when Redis is not configured. // // Deliberately the *same* instance the express backend uses. POS presence is // read by the rider app, which talks to that backend, and a second Redis would // mean either cross-service HTTP calls on every board refresh or two copies of // the truth about which tills are alive. // // Key namespaces do not collide: express owns `delivery:*`, `city:*`, // `rider_*`; POS owns `pos:*`. Worth keeping that way — a shared datastore only // stays safe while each writer's keys are obviously its own. var Rdb *redis.Client // RedisCtx is the background context for Redis calls made outside a request. var RedisCtx = context.Background() // InitRedis connects if REDIS_HOST is set, and does nothing if it is not. // // Redis is optional here: without it the POS health board goes dark, but bills // still arrive and commit. That is the right failure — losing presence is an // inconvenience, losing a sale is not — so this never aborts startup. func InitRedis() { host := strings.TrimSpace(os.Getenv("REDIS_HOST")) if host == "" { log.Println("redis: REDIS_HOST not set, POS presence disabled") return } port := getEnv("REDIS_PORT", "6379") dbIndex, err := strconv.Atoi(getEnv("REDIS_DB", "0")) if err != nil { dbIndex = 0 } Rdb = redis.NewClient(&redis.Options{ Addr: host + ":" + port, Username: getEnv("REDIS_USER", "default"), Password: os.Getenv("REDIS_PASSWORD"), DB: dbIndex, // Short on purpose. A degraded Redis must fail fast rather than tie up // a pooled connection for tens of seconds — the express backend learned // this the hard way, where a 10s x 3-retry config let one stuck call // hold a connection for ~35s and exhausted the pool under load. DialTimeout: 5 * time.Second, ReadTimeout: 3 * time.Second, WriteTimeout: 3 * time.Second, PoolTimeout: 4 * time.Second, }) ctx, cancel := context.WithTimeout(RedisCtx, 5*time.Second) defer cancel() if err := Rdb.Ping(ctx).Err(); err != nil { // Logged, not fatal. A broker that cannot be reached is fatal because // bills would silently queue; Redis being down only costs the board. log.Printf("redis: could not reach %s:%s — POS presence will be unavailable: %v", host, port, err) Rdb = nil return } log.Printf("✅ Redis connected at %s:%s (db %d)", host, port, dbIndex) } // CloseRedis releases the pool on shutdown. func CloseRedis() { if Rdb == nil { return } if err := Rdb.Close(); err != nil { log.Printf("redis: close failed: %v", err) } }