mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 15:09:10 +00:00
refactor: improve shutdown cleanup
This commit is contained in:
committed by
Carlos Valente
parent
871a6b46c8
commit
4408bdb0d3
@@ -1,3 +1,3 @@
|
|||||||
export interface IAdapter {
|
export interface IAdapter {
|
||||||
shutdown: () => void;
|
shutdown: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,10 +72,17 @@ class OscServer implements IAdapter {
|
|||||||
});
|
});
|
||||||
this.udpSocket.bind(port);
|
this.udpSocket.bind(port);
|
||||||
}
|
}
|
||||||
shutdown() {
|
shutdown(): Promise<void> {
|
||||||
logger.info(LogOrigin.Rx, 'OSC: Closing server');
|
logger.info(LogOrigin.Rx, 'OSC: Closing server');
|
||||||
this.udpSocket?.close();
|
const socket = this.udpSocket;
|
||||||
this.udpSocket = null;
|
this.udpSocket = null;
|
||||||
|
if (!socket) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
socket.close(() => resolve());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -235,8 +235,25 @@ class SocketServer implements IAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shutdown() {
|
shutdown(): Promise<void> {
|
||||||
this.wss?.close();
|
const wss = this.wss;
|
||||||
|
if (!wss) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// Notify clients first so they can reconnect gracefully
|
||||||
|
for (const client of wss.clients) {
|
||||||
|
if (client.readyState === WebSocket.OPEN || client.readyState === WebSocket.CONNECTING) {
|
||||||
|
client.close(1001, 'Server shutting down');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wss.close(() => {
|
||||||
|
this.wss = null;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+91
-25
@@ -42,6 +42,7 @@ import { consoleError, consoleHighlight, consoleSuccess } from './utils/console.
|
|||||||
import { generateCrashReport } from './utils/generateCrashReport.js';
|
import { generateCrashReport } from './utils/generateCrashReport.js';
|
||||||
import { getNetworkInterfaces } from './utils/network.js';
|
import { getNetworkInterfaces } from './utils/network.js';
|
||||||
import { clearUploadfolder } from './utils/upload.js';
|
import { clearUploadfolder } from './utils/upload.js';
|
||||||
|
import { withTimeout } from './utils/withTimeout.js';
|
||||||
|
|
||||||
console.log('\n');
|
console.log('\n');
|
||||||
consoleHighlight(`Starting Ontime version ${ONTIME_VERSION}`);
|
consoleHighlight(`Starting Ontime version ${ONTIME_VERSION}`);
|
||||||
@@ -64,6 +65,7 @@ const prefix = updateRouterPrefix();
|
|||||||
|
|
||||||
// Create express APP
|
// Create express APP
|
||||||
const app = express();
|
const app = express();
|
||||||
|
let isShuttingDown = false;
|
||||||
if (!isProduction) {
|
if (!isProduction) {
|
||||||
// log server timings to requests
|
// log server timings to requests
|
||||||
app.use(serverTiming());
|
app.use(serverTiming());
|
||||||
@@ -85,6 +87,15 @@ app.get(`${prefix}/health`, (_req, res) => {
|
|||||||
res.status(200).send('OK');
|
res.status(200).send('OK');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// readiness probe route for orchestrators (e.g. kubernetes)
|
||||||
|
app.get(`${prefix}/ready`, (_req, res) => {
|
||||||
|
if (isShuttingDown) {
|
||||||
|
res.status(503).send('SHUTTING_DOWN');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).send('READY');
|
||||||
|
});
|
||||||
|
|
||||||
// Implement route endpoints
|
// Implement route endpoints
|
||||||
app.use(`${prefix}/login`, loginRouter); // router for login flow
|
app.use(`${prefix}/login`, loginRouter); // router for login flow
|
||||||
app.use(`${prefix}/data`, authenticate, appRouter); // router for application data
|
app.use(`${prefix}/data`, authenticate, appRouter); // router for application data
|
||||||
@@ -136,6 +147,7 @@ enum OntimeStartOrder {
|
|||||||
|
|
||||||
let step = OntimeStartOrder.InitAssets;
|
let step = OntimeStartOrder.InitAssets;
|
||||||
let expressServer: Server | null = null;
|
let expressServer: Server | null = null;
|
||||||
|
let shutdownPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
const checkStart = (currentState: OntimeStartOrder) => {
|
const checkStart = (currentState: OntimeStartOrder) => {
|
||||||
if (step !== currentState) {
|
if (step !== currentState) {
|
||||||
@@ -250,34 +262,88 @@ export const startIntegrations = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description clean shutdown app services
|
* Clean shutdown app services
|
||||||
* @param {number} exitCode
|
* - it avoid concurrency issues with deduplication of request to shutdown
|
||||||
* @return {Promise<void>}
|
* - extracts exit code to modify cleanup behaviour
|
||||||
*/
|
*/
|
||||||
export const shutdown = async (exitCode = 0) => {
|
export async function shutdown(exitCode = 0): Promise<void> {
|
||||||
consoleHighlight(`Ontime shutting down with code ${exitCode}`);
|
if (shutdownPromise) {
|
||||||
|
return shutdownPromise;
|
||||||
await flushPendingWrites().catch((_error) => {
|
|
||||||
/** nothing do to here */
|
|
||||||
});
|
|
||||||
|
|
||||||
// clear the restore file if it was a normal exit
|
|
||||||
// 0 means it was a SIGNAL
|
|
||||||
// 1 means crash -> keep the file
|
|
||||||
// 2 means dev crash -> do nothing
|
|
||||||
// 3 means container shutdown -> keep the file
|
|
||||||
// 99 means there was a shutdown request from the UI
|
|
||||||
if (exitCode === 0 || exitCode === 99) {
|
|
||||||
await restoreService.clear();
|
|
||||||
await portManager.shutdown();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expressServer?.close();
|
shutdownPromise = performShutdown(exitCode);
|
||||||
runtimeService.shutdown();
|
return shutdownPromise;
|
||||||
logger.shutdown();
|
};
|
||||||
oscServer.shutdown();
|
|
||||||
socket.shutdown();
|
const closeHttpServer = async (server: Server | null): Promise<void> => {
|
||||||
process.exit(exitCode);
|
if (!server) return;
|
||||||
|
|
||||||
|
const closePromise = new Promise<void>((resolve, reject) => {
|
||||||
|
server.close((error) => {
|
||||||
|
if (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code === 'ERR_SERVER_NOT_RUNNING') {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.closeIdleConnections();
|
||||||
|
server.closeAllConnections();
|
||||||
|
|
||||||
|
await closePromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shutdownGlobalTimeout = 10_000; // 10 seconds
|
||||||
|
const shutdownTimeout = 4_000; // 4 seconds
|
||||||
|
|
||||||
|
async function performShutdown(exitCode: number): Promise<void> {
|
||||||
|
isShuttingDown = true;
|
||||||
|
consoleHighlight(`Ontime shutting down with code ${exitCode}`);
|
||||||
|
|
||||||
|
// if shutdown takes longer than 10 seconds, force exit to avoid hanging processes
|
||||||
|
const forceExitTimer = setTimeout(() => {
|
||||||
|
consoleError('Forced shutdown after timeout');
|
||||||
|
process.exit(exitCode);
|
||||||
|
}, shutdownGlobalTimeout);
|
||||||
|
|
||||||
|
try {
|
||||||
|
runtimeService.shutdown();
|
||||||
|
|
||||||
|
// Block for at most 4 seconds on each shutdown segment
|
||||||
|
await withTimeout(
|
||||||
|
flushPendingWrites().catch((_error) => {
|
||||||
|
/** nothing do to here */
|
||||||
|
}),
|
||||||
|
shutdownTimeout,
|
||||||
|
);
|
||||||
|
|
||||||
|
// clear the restore file if it was a normal exit
|
||||||
|
// 0 means it was a SIGNAL
|
||||||
|
// 1 means crash -> keep the file
|
||||||
|
// 2 means dev crash -> do nothing
|
||||||
|
// 3 means container shutdown -> keep the file
|
||||||
|
// 99 means there was a shutdown request from the UI
|
||||||
|
if (exitCode === 0 || exitCode === 99) {
|
||||||
|
await withTimeout(restoreService.clear(), shutdownTimeout);
|
||||||
|
await withTimeout(portManager.shutdown(), shutdownTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
await withTimeout(
|
||||||
|
Promise.all([closeHttpServer(expressServer), socket.shutdown(), oscServer.shutdown()]),
|
||||||
|
shutdownTimeout,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(LogOrigin.Server, `Shutdown error: ${error}`, false);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(forceExitTimer);
|
||||||
|
logger.shutdown();
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on('exit', (code) => consoleHighlight(`Ontime shutdown with code: ${code}`));
|
process.on('exit', (code) => consoleHighlight(`Ontime shutdown with code: ${code}`));
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Resolves or rejects with the provided promise, but fails if it does not settle within `timeoutMs`.
|
||||||
|
*/
|
||||||
|
export const withTimeout = <T>(promise: Promise<T>, timeoutMs: number): Promise<T> => {
|
||||||
|
let timer: NodeJS.Timeout | null = null;
|
||||||
|
const timeout = new Promise<T>((_, reject) => {
|
||||||
|
timer = setTimeout(() => reject(new Error('Operation timed out')), timeoutMs);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.race([promise, timeout]).finally(() => {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user