fix(gateway): make process shutdown idempotent
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
export type GatewayShutdownReason = 'SIGINT' | 'SIGTERM' | string;
|
||||
|
||||
export type GatewayShutdownController = {
|
||||
stop(reason: GatewayShutdownReason): Promise<void>;
|
||||
dispose(): void;
|
||||
};
|
||||
|
||||
type GatewayShutdownOptions = {
|
||||
close(reason: GatewayShutdownReason): void | Promise<void>;
|
||||
onStopping?: (reason: GatewayShutdownReason) => void;
|
||||
onError?: (error: unknown, reason: GatewayShutdownReason) => void;
|
||||
};
|
||||
|
||||
/** Installs one idempotent owner for process signals and resource shutdown. */
|
||||
export const installGatewayShutdownController = (options: GatewayShutdownOptions): GatewayShutdownController => {
|
||||
let stopPromise: Promise<void> | undefined;
|
||||
let stopReason: GatewayShutdownReason | undefined;
|
||||
let failureReported = false;
|
||||
let disposed = false;
|
||||
|
||||
const dispose = (): void => {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
process.off('SIGINT', handleSigint);
|
||||
process.off('SIGTERM', handleSigterm);
|
||||
};
|
||||
|
||||
const stop = (reason: GatewayShutdownReason): Promise<void> => {
|
||||
if (stopPromise) return stopPromise;
|
||||
stopReason = reason;
|
||||
options.onStopping?.(reason);
|
||||
stopPromise = Promise.resolve()
|
||||
.then(() => options.close(reason))
|
||||
.finally(dispose);
|
||||
return stopPromise;
|
||||
};
|
||||
|
||||
const requestStop = (reason: GatewayShutdownReason): void => {
|
||||
void stop(reason).catch((error: unknown) => {
|
||||
if (failureReported) return;
|
||||
failureReported = true;
|
||||
options.onError?.(error, stopReason ?? reason);
|
||||
});
|
||||
};
|
||||
function handleSigint(): void {
|
||||
requestStop('SIGINT');
|
||||
}
|
||||
function handleSigterm(): void {
|
||||
requestStop('SIGTERM');
|
||||
}
|
||||
|
||||
process.on('SIGINT', handleSigint);
|
||||
process.on('SIGTERM', handleSigterm);
|
||||
|
||||
return { stop, dispose };
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
|
||||
import { resolveGatewayOrchestratorConfigFromEnv } from '../config.js';
|
||||
import { createGatewayOrchestrator } from './orchestratorFactory.js';
|
||||
import { installGatewayShutdownController } from '../lifecycle/shutdownController.js';
|
||||
|
||||
export const runGatewayOrchestrator = async (): Promise<void> => {
|
||||
const config = resolveGatewayOrchestratorConfigFromEnv();
|
||||
@@ -14,15 +15,17 @@ export const runGatewayOrchestrator = async (): Promise<void> => {
|
||||
|
||||
const { orchestrator } = createGatewayOrchestrator(postgres.prisma as GatewayPrismaClient, config, process.env);
|
||||
|
||||
const stop = async (reason: string): Promise<void> => {
|
||||
console.info(`[gateway-orchestrator] stopping: ${reason}`);
|
||||
await orchestrator.stop();
|
||||
await postgres.disconnect();
|
||||
};
|
||||
|
||||
process.on('SIGINT', () => void stop('SIGINT'));
|
||||
process.on('SIGTERM', () => void stop('SIGTERM'));
|
||||
|
||||
orchestrator.start();
|
||||
installGatewayShutdownController({
|
||||
close: async () => {
|
||||
await orchestrator.stop();
|
||||
await postgres.disconnect();
|
||||
},
|
||||
onStopping: (reason) => console.info(`[gateway-orchestrator] stopping: ${reason}`),
|
||||
onError: (error, reason) => {
|
||||
console.error(`[gateway-orchestrator] shutdown failed (${reason})`, error);
|
||||
process.exitCode = 1;
|
||||
},
|
||||
});
|
||||
console.info('[gateway-orchestrator] started');
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ import { createGatewayOrchestrator } from './orchestrator/orchestratorFactory.js
|
||||
import { appRouter } from './router.js';
|
||||
import { RepositoryProfileStatusService } from './lobby/profileStatusService.js';
|
||||
import { registerAccountIconInternalRoute } from './auth/accountIconInternalRoute.js';
|
||||
import { installGatewayShutdownController } from './lifecycle/shutdownController.js';
|
||||
|
||||
export const createGatewayApiServer = async () => {
|
||||
const config = resolveGatewayApiConfigFromEnv();
|
||||
@@ -131,8 +132,23 @@ export const createGatewayApiServer = async () => {
|
||||
|
||||
export const runGatewayApiServer = async (): Promise<void> => {
|
||||
const { app, config } = await createGatewayApiServer();
|
||||
await app.listen({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
const shutdown = installGatewayShutdownController({
|
||||
close: () => app.close(),
|
||||
onStopping: (reason) => app.log.info({ reason }, 'gateway API stopping'),
|
||||
onError: (error, reason) => {
|
||||
app.log.error({ err: error, reason }, 'gateway API shutdown failed');
|
||||
process.exitCode = 1;
|
||||
},
|
||||
});
|
||||
app.addHook('onClose', async () => shutdown.dispose());
|
||||
try {
|
||||
await app.listen({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
});
|
||||
} catch (error) {
|
||||
shutdown.dispose();
|
||||
await app.close();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user