Files
backend_fiesta/db/redis.go
Suriyakumarvijayanayagam 4474479735 Load .env.<APP_ENV>, validate config at boot, keep secrets out of the image
`main.go` only ever loaded `.env`; the `APP_ENV` switch described in
`.env.local` / `.env.production` did not exist, and a missing variable
surfaced one restart at a time as a log.Fatalf inside db.Connect.

config.Load now picks `.env.<APP_ENV>` (default local) then `.env`, with
real environment winning, reads every setting into one typed Config and
reports everything missing in one message. Production insists on a POS
signing secret; local warns when DB_HOST is not a local address. db,
redis and the image store take the Config instead of reading env
themselves.

Also:
- livehub read MQTT_USERNAME while everything else uses MQTT_USER, so the
  console stream connected to the broker unauthenticated. Both accepted.
- .dockerignore: `COPY . .` was baking .env.production into the image.
  Dockerfile sets APP_ENV=production.
- Drop utils/config.go (dead viper loader) and create_table.go (unused,
  hardcoded production DSN); go mod tidy removes viper.
- .env.example lists every variable the code reads; docs/ENVIRONMENT.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 17:04:33 +05:30

79 lines
2.5 KiB
Go

package db
import (
"context"
"log"
"nearle/config"
"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(c config.RedisConfig) {
if !c.Enabled() {
log.Println("redis: REDIS_HOST not set, POS presence disabled")
return
}
host, port, dbIndex := c.Host, c.Port, c.DB
Rdb = redis.NewClient(&redis.Options{
Addr: host + ":" + port,
Username: c.User,
Password: c.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)
}
}