66 "log/slog"
77 "os"
88 "runtime"
9+ "sync"
910 "time"
1011
1112 "github.com/jackc/pgx/v5/pgxpool"
@@ -54,7 +55,38 @@ func NewAPIV1Router(srv api.Server,
5455 assetVersionRepository shared.AssetVersionRepository ,
5556 artifactRepository shared.ArtifactRepository ,
5657) APIV1Router {
58+ if pool == nil {
59+ panic ("NewAPIV1Router: pool must not be nil" )
60+ }
61+
5762 apiV1Router := srv .Echo .Group ("/api/v1" )
63+
64+ var healthConn * pgxpool.Conn
65+ var healthConnMu sync.Mutex
66+ pingDedicatedHealthConn := func (ctx context.Context ) error {
67+ healthConnMu .Lock ()
68+ defer healthConnMu .Unlock ()
69+
70+ if healthConn == nil {
71+ conn , err := pool .Acquire (ctx )
72+ if err != nil {
73+ return err
74+ }
75+ healthConn = conn
76+ slog .Info ("reserved dedicated health-check database connection" )
77+ }
78+
79+ if err := healthConn .Ping (ctx ); err != nil {
80+ // If the context was not canceled, drop the connection and force a fresh acquire next time.
81+ if ctx .Err () == nil {
82+ healthConn .Release ()
83+ healthConn = nil
84+ }
85+ return err
86+ }
87+
88+ return nil
89+ }
5890 // this makes the third party integrations available to all controllers
5991 apiV1Router .Use (func (next echo.HandlerFunc ) echo.HandlerFunc {
6092 return func (ctx shared.Context ) error {
@@ -71,6 +103,12 @@ func NewAPIV1Router(srv api.Server,
71103 }
72104 })
73105
106+ initCtx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
107+ defer cancel ()
108+ if err := pingDedicatedHealthConn (initCtx ); err != nil {
109+ panic ("NewAPIV1Router: could not initialize dedicated health-check connection: " + err .Error ())
110+ }
111+
74112 apiV1Router .GET ("/info/" , func (c echo.Context ) error {
75113 var mem runtime.MemStats
76114 runtime .ReadMemStats (& mem )
@@ -129,23 +167,18 @@ func NewAPIV1Router(srv api.Server,
129167 dbInfo .Status = "healthy"
130168
131169 // Prefer runtime stats from the underlying pgx pool which backs the sql.DB
132- if pool != nil {
133- stats := pool .Stat ()
134- // Map pgx pool stats to the DBStats fields
135- dbInfo .OpenConnections = int (stats .TotalConns ())
136- dbInfo .InUse = int (stats .AcquiredConns ())
137- dbInfo .Idle = int (stats .IdleConns ())
138- dbInfo .MaxOpenConnections = int (stats .MaxConns ())
139-
140- // Expose the same values in the Pool info structure below
141- poolInfo .TotalConns = int (stats .TotalConns ())
142- poolInfo .IdleConns = int (stats .IdleConns ())
143- poolInfo .AcquiredConns = int (stats .AcquiredConns ())
144- poolInfo .MaxConns = int (stats .MaxConns ())
145- } else {
146- // Fallback to sql DB stats if pool isn't available
147- dbInfo .DBStats = sqlDB .Stats ()
148- }
170+ stats := pool .Stat ()
171+ // Map pgx pool stats to the DBStats fields
172+ dbInfo .OpenConnections = int (stats .TotalConns ())
173+ dbInfo .InUse = int (stats .AcquiredConns ())
174+ dbInfo .Idle = int (stats .IdleConns ())
175+ dbInfo .MaxOpenConnections = int (stats .MaxConns ())
176+
177+ // Expose the same values in the Pool info structure below
178+ poolInfo .TotalConns = int (stats .TotalConns ())
179+ poolInfo .IdleConns = int (stats .IdleConns ())
180+ poolInfo .AcquiredConns = int (stats .AcquiredConns ())
181+ poolInfo .MaxConns = int (stats .MaxConns ())
149182
150183 if ver , dirty , err := database .GetMigrationVersionWithDB (); err == nil {
151184 v := ver
@@ -190,10 +223,36 @@ func NewAPIV1Router(srv api.Server,
190223
191224 ctxWithTimeout , cancel := context .WithTimeout (ctx .Request ().Context (), 5 * time .Second )
192225 defer cancel ()
226+ pingStart := time .Now ()
227+
228+ pingErr := pingDedicatedHealthConn (ctxWithTimeout )
229+
230+ if pingErr != nil {
231+ sqlStats := sqlDB .Stats ()
232+ logArgs := []any {
233+ "error" , pingErr ,
234+ "usingDedicatedHealthConn" , true ,
235+ "pingDuration" , time .Since (pingStart ),
236+ "requestContextErr" , ctx .Request ().Context ().Err (),
237+ "pingContextErr" , ctxWithTimeout .Err (),
238+ "sqlOpenConnections" , sqlStats .OpenConnections ,
239+ "sqlInUse" , sqlStats .InUse ,
240+ "sqlIdle" , sqlStats .Idle ,
241+ "sqlWaitCount" , sqlStats .WaitCount ,
242+ "sqlWaitDuration" , sqlStats .WaitDuration ,
243+ }
244+
245+ pgxStats := pool .Stat ()
246+ logArgs = append (logArgs ,
247+ "pgxTotalConns" , pgxStats .TotalConns (),
248+ "pgxAcquiredConns" , pgxStats .AcquiredConns (),
249+ "pgxIdleConns" , pgxStats .IdleConns (),
250+ "pgxMaxConns" , pgxStats .MaxConns (),
251+ "pgxAcquireCount" , pgxStats .AcquireCount (),
252+ "pgxAcquireDuration" , pgxStats .AcquireDuration (),
253+ )
193254
194- if err := sqlDB .PingContext (ctxWithTimeout ); err != nil {
195- stats := sqlDB .Stats () // log stats for diagnostics
196- slog .Info ("database ping failed" , "error" , err , "openConnections" , stats .OpenConnections , "inUse" , stats .InUse , "idle" , stats .Idle )
255+ slog .Info ("database ping failed" , logArgs ... )
197256 return ctx .JSON (503 , map [string ]string {
198257 "status" : "unhealthy" ,
199258 "error" : "database ping failed" ,
0 commit comments