11import express , { Express , Request , Response , NextFunction } from "express" ;
2+ import type { Server } from "node:http" ;
23import cors from "cors" ;
34import cookieParser from "cookie-parser" ;
45import crypto from "crypto" ;
56import dotenv from "dotenv" ;
6- import { prisma , connectWithRetry , startPoolHealthCheck } from "./config/db" ;
7+ import {
8+ connectWithRetry ,
9+ drainDatabasePool ,
10+ pool ,
11+ startPoolHealthCheck ,
12+ stopPoolHealthCheck ,
13+ } from "./config/db" ;
714import { trace } from "./config/tracing" ;
815import { intakeRateLimit } from "./middleware/intakeRateLimit" ;
916import { sqlInjectionGuard } from "./middleware/sanitize" ;
1017import { tracingMiddleware } from "./utils/tracing" ;
1118import { metricsMiddleware } from "./middleware/metrics" ;
1219import { createMetricsRouter , updatePoolMetrics } from "./utils/metrics" ;
20+ import { closeHttpServer , createGracefulShutdownHandler } from "./utils/graceful-shutdown" ;
1321import authRoutes from "./routes/auth" ;
1422import jobsRoutes from "./routes/jobs" ;
1523import disputesRoutes from "./routes/disputes" ;
@@ -20,7 +28,6 @@ import uploadsRoutes from "./routes/uploads";
2028import bulkRoutes from "./routes/bulk" ;
2129import poolRoutes from "./routes/pool" ;
2230import stateRoutes from "./routes/state" ;
23- import { pool } from "./config/db" ;
2431import { startStorageCleanup , stopStorageCleanup } from "./utils/storage-cleanup" ;
2532import { startNonceCleanup , stopNonceCleanup } from "./utils/nonce-cleanup" ;
2633
@@ -31,6 +38,16 @@ const port = process.env.PORT || 3001;
3138const logger = trace . getLogger ( "server" ) ;
3239const isProduction = process . env . NODE_ENV === "production" ;
3340const CSRF_COOKIE_NAME = "lance-csrf-token" ;
41+ let isShuttingDown = false ;
42+ let server : Server | null = null ;
43+ let poolMetricsInterval : NodeJS . Timeout | null = null ;
44+
45+ function positiveIntEnv ( name : string , fallback : number ) : number {
46+ const parsed = Number . parseInt ( process . env [ name ] || String ( fallback ) , 10 ) ;
47+ return Number . isFinite ( parsed ) && parsed > 0 ? parsed : fallback ;
48+ }
49+
50+ const SHUTDOWN_TIMEOUT_MS = positiveIntEnv ( "SHUTDOWN_TIMEOUT_MS" , 10_000 ) ;
3451
3552// Enable CORS for frontend requests with credentials support
3653const FRONTEND_URL = process . env . FRONTEND_URL || "http://localhost:3000" ;
@@ -76,6 +93,19 @@ app.get("/api/v1/auth/csrf", (req: Request, res: Response) => {
7693 res . json ( { csrfToken } ) ;
7794} ) ;
7895
96+ app . use ( ( req : Request , res : Response , next : NextFunction ) => {
97+ if ( ! isShuttingDown ) {
98+ return next ( ) ;
99+ }
100+
101+ logger . warn ( "Request rejected during graceful shutdown" , {
102+ method : req . method ,
103+ path : req . path ,
104+ } ) ;
105+ res . setHeader ( "Connection" , "close" ) ;
106+ return res . status ( 503 ) . json ( { error : "Server is shutting down" } ) ;
107+ } ) ;
108+
79109app . use ( csrfMiddleware ) ;
80110app . use ( tracingMiddleware ) ; // Global request tracing and diagnostics
81111app . use ( intakeRateLimit ) ;
@@ -148,23 +178,34 @@ app.get("/health", async (req: Request, res: Response) => {
148178 }
149179} ) ;
150180
151- // Graceful shutdown handler
152- process . on ( "SIGTERM" , async ( ) => {
153- logger . info ( "SIGTERM received, shutting down gracefully" ) ;
154- stopStorageCleanup ( ) ;
155- stopNonceCleanup ( ) ;
156- try {
157- await prisma . $disconnect ( ) ;
158- logger . info ( "Database connection closed" ) ;
159- process . exit ( 0 ) ;
160- } catch ( error ) {
161- logger . error ( "Error during shutdown" , {
162- error : error instanceof Error ? error . message : String ( error ) ,
163- } ) ;
164- process . exit ( 1 ) ;
165- }
181+ const shutdown = createGracefulShutdownHandler ( {
182+ logger,
183+ timeoutMs : SHUTDOWN_TIMEOUT_MS ,
184+ markShuttingDown : ( ) => {
185+ isShuttingDown = true ;
186+ } ,
187+ closeServer : ( ) => closeHttpServer ( server ) ,
188+ stopBackgroundTasks : [
189+ ( ) => {
190+ stopStorageCleanup ( ) ;
191+ stopNonceCleanup ( ) ;
192+ stopPoolHealthCheck ( ) ;
193+ if ( poolMetricsInterval ) {
194+ clearInterval ( poolMetricsInterval ) ;
195+ poolMetricsInterval = null ;
196+ }
197+ } ,
198+ ] ,
199+ drainDatabase : drainDatabasePool ,
200+ exit : ( code ) => process . exit ( code ) ,
166201} ) ;
167202
203+ for ( const signal of [ "SIGINT" , "SIGTERM" ] as NodeJS . Signals [ ] ) {
204+ process . once ( signal , ( ) => {
205+ void shutdown ( signal ) ;
206+ } ) ;
207+ }
208+
168209// ---------------------------------------------------------------------------
169210// Start the server — validate the DB connection with retry backoff first,
170211// then kick off background pool health-checking.
@@ -175,10 +216,10 @@ async function bootstrap(): Promise<void> {
175216 startPoolHealthCheck ( ) ;
176217 startStorageCleanup ( ) ;
177218 startNonceCleanup ( ) ;
178- app . listen ( port , ( ) => {
219+ server = app . listen ( port , ( ) => {
179220 console . log ( `⚡️[server]: Server is running at http://localhost:${ port } ` ) ;
180221 // Update pool metrics periodically so the Prometheus scrape has fresh data
181- setInterval ( ( ) => {
222+ poolMetricsInterval = setInterval ( ( ) => {
182223 updatePoolMetrics ( pool . totalCount , pool . idleCount , pool . waitingCount ) ;
183224 } , 15_000 ) . unref ( ) ;
184225 } ) ;
0 commit comments