Skip to content

Commit 16fb438

Browse files
Merge pull request #107 from edehvictor/feature/graceful-shutdown
feat: graceful shutdown (#58)
2 parents 82e42b5 + a733b1b commit 16fb438

2 files changed

Lines changed: 64 additions & 7 deletions

File tree

src/db/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,8 @@ export async function initializeDb() {
6060
}
6161

6262
// Graceful shutdown
63-
process.on('SIGINT', () => {
63+
// Export close function for graceful shutdown
64+
export function closeDb() {
6465
sqlite.close();
65-
process.exit(0);
66-
});
67-
66+
}
6867
export { schema };

src/index.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import express from 'express';
22
import webhookRouter from './webhooks/webhook.routes.js';
33
import { calloraEvents } from './events/event.emitter.js';
44
import helmet from 'helmet';
5-
import { db, initializeDb, schema } from './db/index.js';
5+
import { db, initializeDb, schema, closeDb } from './db/index.js';
66
import { eq, desc, and, type SQL } from 'drizzle-orm';
77
import { requireAuth, type AuthenticatedLocals } from './middleware/requireAuth.js';
88
import { errorHandler } from './middleware/errorHandler.js';
99
import { BadRequestError, NotFoundError, UnauthorizedError, ForbiddenError } from './errors/index.js';
1010
import * as developerRepository from './repositories/developerRepository.js';
1111
import type { Response } from 'express';
12+
import type { Socket } from 'net';
1213

1314
import { createDeveloperRouter } from './routes/developerRoutes.js';
1415
import { createGatewayRouter } from './routes/gatewayRoutes.js';
@@ -100,9 +101,66 @@ if (isDirectExecution) {
100101
async function startServer() {
101102
try {
102103
await initializeDb();
103-
app.listen(PORT, () => {
104+
105+
const server = app.listen(PORT, () => {
104106
console.log(`Callora backend listening on http://localhost:${PORT}`);
105107
});
108+
109+
// Track active connections so we can wait for them to finish
110+
const activeConnections = new Set<Socket>();
111+
112+
server.on('connection', (socket: Socket) => {
113+
activeConnections.add(socket);
114+
socket.once('close', () => activeConnections.delete(socket));
115+
});
116+
117+
async function gracefulShutdown(signal: string) {
118+
console.log(`\n[shutdown] Received ${signal}. Starting graceful shutdown...`);
119+
120+
// 1. Stop accepting new requests
121+
server.close(() => {
122+
console.log('[shutdown] HTTP server closed. No new requests accepted.');
123+
});
124+
125+
// 2. Wait for in-flight requests to finish (max 30s)
126+
const TIMEOUT_MS = 30_000;
127+
const deadline = setTimeout(() => {
128+
console.warn('[shutdown] Timeout reached. Forcing exit.');
129+
process.exit(1);
130+
}, TIMEOUT_MS);
131+
deadline.unref();
132+
133+
// 3. Wait until all active connections are gone
134+
await new Promise<void>((resolve) => {
135+
if (activeConnections.size === 0) return resolve();
136+
console.log(`[shutdown] Waiting for ${activeConnections.size} in-flight connection(s)...`);
137+
const interval = setInterval(() => {
138+
if (activeConnections.size === 0) {
139+
clearInterval(interval);
140+
resolve();
141+
}
142+
}, 200);
143+
});
144+
145+
// 4. Close the database
146+
console.log('[shutdown] Closing database...');
147+
try {
148+
closeDb();
149+
console.log('[shutdown] Database closed.');
150+
} catch (err) {
151+
console.error('[shutdown] Error closing database:', err);
152+
}
153+
154+
// 5. Exit cleanly
155+
console.log('[shutdown] Shutdown complete. Exiting.');
156+
clearTimeout(deadline);
157+
process.exit(0);
158+
}
159+
160+
// Register shutdown signals
161+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
162+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
163+
106164
} catch (error) {
107165
console.error('Failed to start server:', error);
108166
process.exit(1);
@@ -112,4 +170,4 @@ if (isDirectExecution) {
112170
startServer();
113171
}
114172

115-
export default app;
173+
export default app;

0 commit comments

Comments
 (0)