Half of this binary's js.Publish calls were bound to no stream at all.
The streams were declared only by an external Python script on another
machine (Birock/doormile-bookings/setup_jetstream.py) and had drifted
from the code: booking.cancelled, booking.outcome and
booking.assignment_failed had no stream, and CHAT declared the literal
"chat.room.closed" while chat.go publishes "chat.room.closed.<id>",
which it does not match. Every publish site is best-effort
(`if db.Js != nil` + warn-log), so those events were failing and being
dropped silently — every cancellation, delivery outcome and assignment
failure since the streams were created.
db.EnsureStreams now declares the streams at startup from a map that
sits next to the code that publishes, so the contract cannot drift
again. It only ever adds: existing streams keep their storage type,
retention, limits and every subject they already have. Nothing is
deleted. Losing the create race against a sibling replica is expected
and reconciles rather than erroring.
Alongside that, /internal/miler/* ingests rider telemetry still arriving
over the jupiter NATS chain. The forwarding worker holds no rider JWT —
the rider app is still jupiter-shaped — so LegacyMilerIdentity resolves
an identity from a header into c.Locals("userid") behind the existing
X-Internal-Key guard. That lets the routes reuse the miler handlers
unchanged instead of growing a parallel set that would drift.
Identity comes from a header, never the body: the telemetry handlers
overwrite a body-supplied userid precisely so one rider cannot write
another's GPS trail, and reading it from the body here would reopen that
from behind the internal key. MilerProfile.Legacyuserid (nullable,
indexed) maps a jupiter userid to a Doormile one.
Only fire-and-forget telemetry is exposed. Transactional actions stay
synchronous — a rider needs a real answer from pickup-complete, which a
queue in front of it cannot give.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
178 lines
3.8 KiB
Go
178 lines
3.8 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"doormile/config"
|
|
"doormile/utils"
|
|
|
|
nats "github.com/nats-io/nats.go"
|
|
"github.com/redis/go-redis/v9"
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
var (
|
|
DB *gorm.DB
|
|
Rdb *redis.Client
|
|
Ctx = context.Background()
|
|
Nc *nats.Conn
|
|
Js nats.JetStreamContext
|
|
)
|
|
|
|
func Connect(cfg *config.Config) {
|
|
dsn := fmt.Sprintf(
|
|
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Kolkata",
|
|
cfg.DBHost,
|
|
cfg.DBUser,
|
|
cfg.DBPassword,
|
|
cfg.DBName,
|
|
cfg.DBPort,
|
|
)
|
|
|
|
var err error
|
|
maxRetries := 5
|
|
backoff := 2 * time.Second
|
|
|
|
for i := 1; i <= maxRetries; i++ {
|
|
utils.Info("Connecting to database", "attempt", i, "host", cfg.DBHost, "port", cfg.DBPort)
|
|
|
|
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Error),
|
|
})
|
|
|
|
if err == nil {
|
|
sqlDB, dbErr := DB.DB()
|
|
if dbErr == nil {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
if pingErr := sqlDB.PingContext(ctx); pingErr == nil {
|
|
setupDB(DB)
|
|
utils.Info("✅ Database connected successfully")
|
|
startDBHealthCheck(sqlDB)
|
|
return
|
|
} else {
|
|
err = pingErr
|
|
}
|
|
} else {
|
|
err = dbErr
|
|
}
|
|
}
|
|
|
|
utils.Error("❌ DB connection failed", "attempt", i, "error", err)
|
|
time.Sleep(backoff)
|
|
backoff *= 2
|
|
}
|
|
|
|
utils.Logger.Fatal("☢️ CRITICAL: DB connection failed after retries")
|
|
}
|
|
|
|
func setupDB(database *gorm.DB) {
|
|
sqlDB, err := database.DB()
|
|
if err != nil {
|
|
utils.Error("Failed to get sql.DB", "error", err)
|
|
return
|
|
}
|
|
|
|
sqlDB.SetMaxOpenConns(30)
|
|
sqlDB.SetMaxIdleConns(5)
|
|
sqlDB.SetConnMaxLifetime(5 * time.Minute)
|
|
sqlDB.SetConnMaxIdleTime(2 * time.Minute)
|
|
}
|
|
|
|
func startDBHealthCheck(sqlDB *sql.DB) {
|
|
go func() {
|
|
for {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
err := sqlDB.PingContext(ctx)
|
|
cancel()
|
|
|
|
if err != nil {
|
|
utils.Error("❌ DB connection lost", "error", err)
|
|
}
|
|
|
|
time.Sleep(30 * time.Second)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func CloseDB() {
|
|
if DB == nil {
|
|
return
|
|
}
|
|
sqlDB, err := DB.DB()
|
|
if err != nil {
|
|
utils.Error("Error retrieving sql.DB for close", "error", err)
|
|
return
|
|
}
|
|
utils.Info("Closing DB connection")
|
|
sqlDB.Close()
|
|
}
|
|
|
|
func InitRedis(cfg *config.Config) {
|
|
addr := fmt.Sprintf("%s:%s", cfg.RedisHost, cfg.RedisPort)
|
|
|
|
Rdb = redis.NewClient(&redis.Options{
|
|
Addr: addr,
|
|
Username: cfg.RedisUser,
|
|
Password: cfg.RedisPassword,
|
|
DB: 0,
|
|
DialTimeout: 5 * time.Second,
|
|
ReadTimeout: 3 * time.Second,
|
|
WriteTimeout: 3 * time.Second,
|
|
PoolSize: 10,
|
|
MinIdleConns: 2,
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
|
|
_, err := Rdb.Ping(ctx).Result()
|
|
if err != nil {
|
|
utils.Error("⚠️ Redis connection failed, continuing in fallback mode", "addr", addr, "error", err)
|
|
} else {
|
|
utils.Info("✅ Redis connected successfully", "addr", addr)
|
|
}
|
|
}
|
|
|
|
func InitNATS(cfg *config.Config) {
|
|
var err error
|
|
Nc, err = nats.Connect(
|
|
cfg.NatsURL,
|
|
nats.UserInfo(cfg.NatsUser, cfg.NatsPassword),
|
|
nats.ReconnectWait(2*time.Second),
|
|
nats.MaxReconnects(-1),
|
|
)
|
|
if err != nil {
|
|
utils.Error("⚠️ NATS connection failed, continuing without NATS", "url", cfg.NatsURL, "error", err)
|
|
return
|
|
}
|
|
|
|
Js, err = Nc.JetStream()
|
|
if err != nil {
|
|
utils.Error("⚠️ NATS JetStream init failed", "error", err)
|
|
Nc.Close()
|
|
Nc = nil
|
|
return
|
|
}
|
|
|
|
utils.Info("✅ NATS JetStream connected successfully", "url", cfg.NatsURL)
|
|
|
|
// Declare the streams this binary publishes to, so the subject contract is
|
|
// owned by the code that uses it rather than by an external setup script.
|
|
EnsureStreams()
|
|
}
|
|
|
|
func getEnv(key, fallback string) string {
|
|
if val := os.Getenv(key); val != "" {
|
|
return val
|
|
}
|
|
return fallback
|
|
}
|