fix(runtime): bound PM2 process restarts and identity
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
import { isRecord } from '@sammo-ts/common';
|
||||
|
||||
import type { BuildCommand, BuildRunner } from './buildRunner.js';
|
||||
import type { ProcessManager } from './processManager.js';
|
||||
import { sanitizeManagedProcessEnv, type ProcessManager } from './processManager.js';
|
||||
import type {
|
||||
GatewayClaimedProfileUpdate,
|
||||
GatewayOperationRecord,
|
||||
@@ -368,7 +368,7 @@ export const buildProcessDefinitions = (
|
||||
battleSim: { name: string; script: string; cwd: string; env: Record<string, string> };
|
||||
tournament: { name: string; script: string; cwd: string; env: Record<string, string> };
|
||||
} => {
|
||||
const baseEnv = { ...(config.baseEnv ?? {}) };
|
||||
const baseEnv = sanitizeManagedProcessEnv(config.baseEnv ?? {});
|
||||
const frontendName = buildProcessName(profile.profileName, 'frontend');
|
||||
const apiName = buildProcessName(profile.profileName, 'api');
|
||||
const daemonName = buildProcessName(profile.profileName, 'daemon');
|
||||
@@ -1453,11 +1453,18 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
buildWorkspace: workspace.root,
|
||||
};
|
||||
const started = await this.startProfile(builtProfile, assertLease);
|
||||
if (!started) {
|
||||
await updateClaimedProfile({ status: 'STOPPED', lastError: 'Failed to start profile processes.' }, () =>
|
||||
const ready = started && (await this.waitForProfileReadiness(builtProfile, assertLease));
|
||||
if (!ready) {
|
||||
if (started) {
|
||||
await this.stopProfile(builtProfile, assertLease);
|
||||
}
|
||||
const detail = started
|
||||
? 'reset completed but profile processes failed readiness'
|
||||
: 'reset completed but profile processes failed to start';
|
||||
await updateClaimedProfile({ status: 'STOPPED', lastError: detail }, () =>
|
||||
this.repository.updateStatus(profile.profileName, 'STOPPED')
|
||||
);
|
||||
return { status: 'FAILED', detail: 'reset completed but profile processes failed to start' };
|
||||
return { status: 'FAILED', detail };
|
||||
}
|
||||
await updateClaimedProfile({ lastError: null }, async () => {
|
||||
await this.repository.updateLastError(profile.profileName, null);
|
||||
@@ -1723,6 +1730,17 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
];
|
||||
const attemptedNames: string[] = [];
|
||||
try {
|
||||
const expectedNames = new Set(orderedDefinitions.map((definition) => definition.name));
|
||||
const existingNames = new Set(
|
||||
(await this.processManager.list())
|
||||
.filter((process) => expectedNames.has(process.name))
|
||||
.map((process) => process.name)
|
||||
);
|
||||
for (const name of existingNames) {
|
||||
await assertLease?.();
|
||||
await this.processManager.delete(name);
|
||||
await assertLease?.();
|
||||
}
|
||||
for (const definition of orderedDefinitions) {
|
||||
await assertLease?.();
|
||||
attemptedNames.push(definition.name);
|
||||
@@ -1774,12 +1792,17 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
this.fetchImpl(frontendUrl),
|
||||
this.processManager.list(),
|
||||
]);
|
||||
const online = new Set(
|
||||
processes
|
||||
.filter((process) => process.status.toLowerCase() === 'online')
|
||||
.map((process) => process.name)
|
||||
const expectedProcesses = processes.filter((process) => expectedNames.includes(process.name));
|
||||
const safeProcesses = expectedProcesses.filter(
|
||||
(process) => process.status.toLowerCase() === 'online' && (process.restartCount ?? 0) === 0
|
||||
);
|
||||
if (api.ok && frontend.ok && expectedNames.every((name) => online.has(name))) {
|
||||
if (
|
||||
api.ok &&
|
||||
frontend.ok &&
|
||||
expectedProcesses.length === expectedNames.length &&
|
||||
safeProcesses.length === expectedNames.length &&
|
||||
new Set(safeProcesses.map((process) => process.name)).size === expectedNames.length
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -4,13 +4,13 @@ import type { GatewayOrchestratorConfig } from '../config.js';
|
||||
import { createGatewayProfileRepository } from './profileRepository.js';
|
||||
import { GatewayOrchestrator } from './gatewayOrchestrator.js';
|
||||
import { Pm2ProcessManager } from './pm2ProcessManager.js';
|
||||
import { sanitizeManagedProcessEnv } from './processManager.js';
|
||||
import { PnpmBuildRunner } from './buildRunner.js';
|
||||
import { resolveWorkspaceRoot } from './workspaceRoot.js';
|
||||
import { GitWorkspaceManager } from './workspaceManager.js';
|
||||
|
||||
export const buildEnvMap = (env: NodeJS.ProcessEnv): Record<string, string> => {
|
||||
const entries = Object.entries(env).filter((entry): entry is [string, string] => typeof entry[1] === 'string');
|
||||
return Object.fromEntries(entries);
|
||||
return sanitizeManagedProcessEnv(env);
|
||||
};
|
||||
|
||||
export const createGatewayOrchestrator = (
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import type * as Pm2 from 'pm2';
|
||||
import type { ProcessManager, ManagedProcessInfo, ProcessDefinition } from './processManager.js';
|
||||
import {
|
||||
sanitizePm2IdentityEnv,
|
||||
type ProcessManager,
|
||||
type ManagedProcessInfo,
|
||||
type ProcessDefinition,
|
||||
} from './processManager.js';
|
||||
|
||||
type Pm2Module = typeof Pm2;
|
||||
|
||||
@@ -27,6 +32,20 @@ const withPm2 = async <T>(handler: (pm2: Pm2Module) => Promise<T>): Promise<T> =
|
||||
}
|
||||
};
|
||||
|
||||
export const buildPm2StartOptions = (definition: ProcessDefinition) => ({
|
||||
name: definition.name,
|
||||
script: definition.script,
|
||||
cwd: definition.cwd,
|
||||
args: definition.args,
|
||||
env: sanitizePm2IdentityEnv(definition.env ?? {}),
|
||||
autorestart: true,
|
||||
max_restarts: 5,
|
||||
min_uptime: 10_000,
|
||||
restart_delay: 2_000,
|
||||
kill_timeout: 15_000,
|
||||
time: true,
|
||||
});
|
||||
|
||||
export class Pm2ProcessManager implements ProcessManager {
|
||||
async list(): Promise<ManagedProcessInfo[]> {
|
||||
return withPm2(
|
||||
@@ -44,6 +63,7 @@ export class Pm2ProcessManager implements ProcessManager {
|
||||
pid: item.pid ?? undefined,
|
||||
cwd: item.pm2_env?.pm_cwd ?? undefined,
|
||||
script: item.pm2_env?.pm_exec_path ?? undefined,
|
||||
restartCount: item.pm2_env?.restart_time ?? 0,
|
||||
})) ?? [];
|
||||
resolve(normalized);
|
||||
});
|
||||
@@ -55,24 +75,26 @@ export class Pm2ProcessManager implements ProcessManager {
|
||||
await withPm2(
|
||||
(pm2) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
pm2.start(
|
||||
{
|
||||
name: definition.name,
|
||||
script: definition.script,
|
||||
cwd: definition.cwd,
|
||||
args: definition.args,
|
||||
env: definition.env,
|
||||
autorestart: true,
|
||||
time: true,
|
||||
},
|
||||
(error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
pm2.list((listError, list) => {
|
||||
if (listError) {
|
||||
reject(listError);
|
||||
return;
|
||||
}
|
||||
);
|
||||
if (list?.some((item) => item.name === definition.name)) {
|
||||
reject(new Error(`PM2 process name already exists: ${definition.name}`));
|
||||
return;
|
||||
}
|
||||
pm2.start(
|
||||
buildPm2StartOptions(definition),
|
||||
(error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface ManagedProcessInfo {
|
||||
pid?: number;
|
||||
cwd?: string;
|
||||
script?: string;
|
||||
restartCount?: number;
|
||||
}
|
||||
|
||||
export interface ProcessDefinition {
|
||||
@@ -20,3 +21,52 @@ export interface ProcessManager {
|
||||
stop(name: string): Promise<void>;
|
||||
delete(name: string): Promise<void>;
|
||||
}
|
||||
|
||||
const PM2_INTERNAL_ENV_KEYS = new Set([
|
||||
'NODE_APP_INSTANCE',
|
||||
'autorestart',
|
||||
'autostart',
|
||||
'created_at',
|
||||
'exec_interpreter',
|
||||
'exec_mode',
|
||||
'exit_code',
|
||||
'instance_var',
|
||||
'instances',
|
||||
'merge_logs',
|
||||
'name',
|
||||
'namespace',
|
||||
'node_args',
|
||||
'node_version',
|
||||
'pm_cwd',
|
||||
'pm_err_log_path',
|
||||
'pm_exec_path',
|
||||
'pm_id',
|
||||
'pm_out_log_path',
|
||||
'pm_pid_path',
|
||||
'pm_uptime',
|
||||
'restart_time',
|
||||
'status',
|
||||
'unstable_restarts',
|
||||
'version',
|
||||
'vizion',
|
||||
'watch',
|
||||
]);
|
||||
|
||||
export const sanitizePm2IdentityEnv = (env: NodeJS.ProcessEnv | Record<string, string>): Record<string, string> =>
|
||||
Object.fromEntries(
|
||||
Object.entries(env).filter(
|
||||
(entry): entry is [string, string] =>
|
||||
typeof entry[1] === 'string' &&
|
||||
!entry[0].startsWith('axm_') &&
|
||||
!entry[0].startsWith('pm_') &&
|
||||
!PM2_INTERNAL_ENV_KEYS.has(entry[0])
|
||||
)
|
||||
);
|
||||
|
||||
export const sanitizeManagedProcessEnv = (env: NodeJS.ProcessEnv | Record<string, string>): Record<string, string> => {
|
||||
const sanitized = sanitizePm2IdentityEnv(env);
|
||||
delete sanitized.GATEWAY_ROLE;
|
||||
delete sanitized.GAME_API_ROLE;
|
||||
delete sanitized.GAME_ENGINE_ROLE;
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user