`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>
79 lines
2.5 KiB
Go
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)
|
|
}
|
|
}
|