Skip to content

Commit 2e1c04c

Browse files
committed
Implement shutdown handling
Fix signal forwarding in the Dockerfile and entry.sh script. Implement sigterm/sigint handlers in the app with shutdown tasks for each service.
1 parent 6734bb1 commit 2e1c04c

5 files changed

Lines changed: 119 additions & 12 deletions

File tree

Dockerfile

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Stage 1: Build the application
2-
FROM node:22.21.1-bookworm-slim AS builder
2+
FROM node:22.21.1-alpine3.23 AS builder
33

44
WORKDIR /app
55

@@ -14,13 +14,9 @@ COPY entry.sh ./
1414
RUN npm run build
1515

1616
# Stage 2: Create the production image
17-
FROM node:22.21.1-bookworm-slim
17+
FROM node:22.21.1-alpine3.23
1818

19-
RUN apt-get update && \
20-
apt-get install -yqq --no-install-recommends wget && \
21-
apt-get autoremove && \
22-
apt-get clean && \
23-
rm -rf /var/lib/apt/lists/*
19+
RUN apk add --no-cache wget
2420

2521
WORKDIR /app
2622

@@ -35,4 +31,5 @@ HEALTHCHECK --interval=30s \
3531

3632
EXPOSE 3000
3733

38-
CMD ["/entry.sh"]
34+
CMD ["npm", "run", "start"]
35+
ENTRYPOINT ["/entry.sh"]

backend/server.js

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const TaskScheduler = require("./classes/task-scheduler-singleton");
3535
// const tasks = require("./tasks/tasks");
3636

3737
// websocket
38-
const { setupWebSocketServer } = require("./ws");
38+
const { setupWebSocketServer, shutdownWebSocketServer } = require("./ws");
3939
const writeEnvVariables = require("./classes/env");
4040

4141
process.env.POSTGRES_USER = process.env.POSTGRES_USER ?? "postgres";
@@ -231,6 +231,7 @@ async function authenticate(req, res, next) {
231231
}
232232

233233
// start server
234+
let server;
234235
try {
235236
createdb.createDatabase().then((result) => {
236237
if (result) {
@@ -240,7 +241,7 @@ try {
240241
}
241242

242243
db.migrate.latest().then(() => {
243-
const server = http.createServer(app);
244+
server = http.createServer(app);
244245

245246
setupWebSocketServer(server, BASE_NAME);
246247
server.listen(PORT, LISTEN_IP, async () => {
@@ -255,3 +256,89 @@ try {
255256
} catch (error) {
256257
console.log("[JELLYSTAT] An error has occured on startup: " + error);
257258
}
259+
260+
function runStep(name, fn) {
261+
return (async () => {
262+
console.log(`[JELLYSTAT] Beginnging shutdown step ${name}`);
263+
try {
264+
await fn();
265+
console.log(`[JELLYSTAT] Shutdown step ${name} complete.`);
266+
} catch (err) {
267+
console.error(`[JELLYSTAT] Shutdown step ${name} failed.`, err);
268+
throw err;
269+
}
270+
})();
271+
}
272+
273+
function shutdownKnex() {
274+
if (db && typeof db.destroy === "function") {
275+
console.info("[JELLYSTAT] Destroying knex connection.");
276+
return db.destroy();
277+
}
278+
279+
return Promise.resolve();
280+
}
281+
282+
function shutdownHttpServer() {
283+
return new Promise((resolve, reject) => {
284+
// Stop new connections
285+
server.close((err) => {
286+
if (err) return reject(err);
287+
resolve();
288+
});
289+
290+
if (typeof server.closeIdleConnections === "function") {
291+
console.info(`[JELLYSTAT] Closing idle HTTP connections`);
292+
server.closeIdleConnections();
293+
}
294+
});
295+
}
296+
297+
function withTimeout(promise, ms, label = "shutdown") {
298+
let timeoutId;
299+
300+
const timeoutPromise = new Promise((_, reject) => {
301+
timeoutId = setTimeout(() => {
302+
reject(new Error(`[JELLYSTAT] ${label} timed out after ${ms}ms`));
303+
}, ms);
304+
});
305+
306+
return Promise.race([promise, timeoutPromise]).finally(() => {
307+
clearTimeout(timeoutId);
308+
});
309+
}
310+
311+
// shutdown server
312+
let shuttingDown = false;
313+
async function shutdown(signal) {
314+
if (shuttingDown) return;
315+
shuttingDown = true;
316+
317+
console.log(`[JELLYSTAT] Recieved ${signal}, beginning shutdown process`);
318+
319+
const timeLimit = 9000;
320+
321+
const shutdownTasks = [
322+
runStep("httpServer", async () => shutdownHttpServer(signal)),
323+
runStep("webSocketServer", async () => shutdownWebSocketServer(signal)),
324+
runStep("database", async () => shutdownKnex()),
325+
];
326+
327+
try {
328+
await withTimeout(
329+
Promise.allSettled(shutdownTasks),
330+
timeLimit,
331+
"graceful shutdown"
332+
);
333+
334+
console.log("[JELLYSTAT] Graceful shutdown complete");
335+
process.exit(0);
336+
} catch (err) {
337+
console.error(`[JELLYSTAT] Error occurred while attempting to gracefully shut down.`, err);
338+
process.exit(1);
339+
}
340+
}
341+
342+
// register signal handlers
343+
process.on("SIGTERM", shutdown);
344+
process.on("SIGINT", shutdown);

backend/socket-io-client.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ class SocketIoClient {
1111
this.client = io(this.serverUrl, this.options); // Pass options to io()
1212
}
1313

14+
disconnect() {
15+
this.client?.disconnect();
16+
}
17+
1418
waitForConnection() {
1519
return new Promise((resolve) => {
1620
if (this.client && this.client.connected) {

backend/ws.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const jwt = require("jsonwebtoken");
77
const token = jwt.sign({ user: "internal" }, process.env.JWT_SECRET);
88
const socketClient = new SocketIoClient("http://127.0.0.1:3000", { auth: { token } });
99
let io; // Store the socket.io server instance
10+
let shuttingDown = false;
1011
const JWT_SECRET = process.env.JWT_SECRET;
1112

1213
const setupWebSocketServer = (server, namespacePath) => {
@@ -46,6 +47,24 @@ const setupWebSocketServer = (server, namespacePath) => {
4647
webSocketServerSingleton.setInstance(io);
4748
};
4849

50+
const shutdownWebSocketServer = async (signal) => {
51+
if (!io) return;
52+
53+
try {
54+
io.emit("server_shutdown", { reason: signal });
55+
56+
socketClient?.disconnect();
57+
58+
return new Promise((resolve) => {
59+
io.close(() => {
60+
resolve();
61+
});
62+
});
63+
} catch (err) {
64+
console.error("[JELLYSTAT] WebSocket shutdown error. ", err);
65+
}
66+
}
67+
4968
const sendToAllClients = (message) => {
5069
const ioInstance = webSocketServerSingleton.getInstance();
5170
if (ioInstance) {
@@ -71,4 +90,4 @@ const sendUpdate = async (tag, message) => {
7190
}
7291
};
7392

74-
module.exports = { setupWebSocketServer, sendToAllClients, sendUpdate };
93+
module.exports = { setupWebSocketServer, sendToAllClients, sendUpdate, shutdownWebSocketServer };

entry.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ load_secrets() {
3131
# Load secrets
3232
load_secrets
3333
# Launch Jellystat
34-
npm run start
34+
exec "$@"

0 commit comments

Comments
 (0)