fix(gateway): make process shutdown idempotent

This commit is contained in:
2026-07-31 16:55:57 +00:00
parent 1dd1610cb8
commit 81e58a21bc
5 changed files with 170 additions and 12 deletions
@@ -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');
};
+19 -3
View File
@@ -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;
}
};