Skip to content

Commit 6b6d2a1

Browse files
committed
using dedicated health check connection
1 parent 0ba6c45 commit 6b6d2a1

1 file changed

Lines changed: 79 additions & 20 deletions

File tree

router/apiv1_router.go

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
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

Comments
 (0)