diff --git a/internal/db/db.go b/internal/db/db.go index c696df4..acb5120 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -5,6 +5,7 @@ import ( "embed" "fmt" "io/fs" + "log" "sort" "strings" "time" @@ -15,6 +16,19 @@ import ( //go:embed migrations var migrationsFS embed.FS +// pingAttempts and pingRetryDelay bound the retry on the first connection. +// This pod's own IP can reach the Postgres pod's node before that node's +// NetworkPolicy enforcement (kube-router, reacting to the pod's creation +// event) has added it to the allowed-source set, which fails the ping with +// "connection refused" rather than a timeout. That race resolves within +// several seconds in practice; five attempts two seconds apart give it +// comfortable room without turning a genuinely absent database into a long +// hang. +const ( + pingAttempts = 5 + pingRetryDelay = 2 * time.Second +) + // Open connects to Postgres. dsn is a libpq connection string or URL, e.g. // postgres://terdut:secret@localhost:5432/terdut?sslmode=disable. // @@ -33,11 +47,19 @@ func Open(dsn string) (*sql.DB, error) { db.SetMaxOpenConns(10) db.SetMaxIdleConns(5) db.SetConnMaxLifetime(time.Hour) - if err := db.Ping(); err != nil { - db.Close() - return nil, fmt.Errorf("ping: %w", err) + + for attempt := 1; ; attempt++ { + err = db.Ping() + if err == nil { + return db, nil + } + if attempt == pingAttempts { + db.Close() + return nil, fmt.Errorf("ping: %w", err) + } + log.Printf("open db: ping attempt %d/%d failed, retrying in %s: %v", attempt, pingAttempts, pingRetryDelay, err) + time.Sleep(pingRetryDelay) } - return db, nil } // Migrate applies every embedded migration that has not been applied yet, in