lease 상실 데몬을 종료하고 관리자에게 즉시 오류 기록
This commit is contained in:
@@ -90,7 +90,20 @@ export class TurnDaemonLifecycle {
|
||||
|
||||
start(): Promise<void> {
|
||||
if (!this.loopPromise) {
|
||||
this.loopPromise = this.runLoop();
|
||||
this.loopPromise = this.runLoop().catch(async (error: unknown) => {
|
||||
// 초기화와 pause gate 실패도 관리자에게 즉시 남긴다. lease를
|
||||
// 잃은 runtime은 대기 상태로 살아 있으면 안전하게 재개할 수 없다.
|
||||
this.status.state = 'stopping';
|
||||
this.status.running = false;
|
||||
this.status.paused = true;
|
||||
this.status.lastError = error instanceof Error ? error.message : 'Unknown lifecycle error.';
|
||||
try {
|
||||
await this.hooks?.onRunError?.(error);
|
||||
} catch {
|
||||
// 장애 기록 실패가 원래 종료 원인을 덮어쓰지 않게 한다.
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
return this.loopPromise;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,11 @@ import {
|
||||
createResetOfficerLockHandler,
|
||||
} from './monthlyCoreEventAction.js';
|
||||
import { buildCommandEnv } from './reservedTurnCommands.js';
|
||||
import { DatabaseTurnDaemonLease, TurnDaemonLeaseUnavailableError } from '../lifecycle/databaseTurnDaemonLease.js';
|
||||
import {
|
||||
DatabaseTurnDaemonLease,
|
||||
TurnDaemonLeaseLostError,
|
||||
TurnDaemonLeaseUnavailableError,
|
||||
} from '../lifecycle/databaseTurnDaemonLease.js';
|
||||
import { EngineStateManager } from './engineStateManager.js';
|
||||
import { applyRuntimeClockShift } from './runtimeClockShift.js';
|
||||
import { applyRuntimeGameSettings } from './runtimeGameSettings.js';
|
||||
@@ -1060,7 +1064,9 @@ const createTurnDaemonRuntimeWithLease = async (
|
||||
hooks,
|
||||
pauseGate: async () => {
|
||||
if (turnDaemonLease?.isLost()) {
|
||||
return true;
|
||||
// 만료된 owner는 재개 명령도 처리할 수 없다. 현재 runtime을
|
||||
// 끝내 PM2가 새 owner와 DB snapshot으로 시작하도록 한다.
|
||||
throw new TurnDaemonLeaseLostError(options.profileName ?? options.profile);
|
||||
}
|
||||
const gatewayPaused = (await pauseGate?.()) ?? false;
|
||||
const phase = world.getGameClockState().phase;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ManualClock } from '@sammo-ts/common';
|
||||
import { TurnDaemonLeaseLostError } from '../src/lifecycle/databaseTurnDaemonLease.js';
|
||||
|
||||
import {
|
||||
InMemoryControlQueue,
|
||||
@@ -14,6 +15,39 @@ import {
|
||||
const addMinutes = (time: Date, minutes: number): Date => new Date(time.getTime() + minutes * 60_000);
|
||||
|
||||
describe('TurnDaemonLifecycle', () => {
|
||||
it.each([false, true])('reports a fatal lease gate and exits even if reporting fails (%s)', async (reportFails) => {
|
||||
const now = new Date('2026-09-09T17:30:00Z');
|
||||
const error = new TurnDaemonLeaseLostError('che:default');
|
||||
const processor = { run: vi.fn() };
|
||||
const onRunError = vi.fn(async () => {
|
||||
if (reportFails) throw new Error('gateway unavailable');
|
||||
});
|
||||
const lifecycle = new TurnDaemonLifecycle(
|
||||
{
|
||||
clock: new ManualClock(now.getTime()),
|
||||
controlQueue: new InMemoryControlQueue(),
|
||||
processor,
|
||||
getNextTickTime: (value) => addMinutes(value, 5),
|
||||
stateStore: {
|
||||
loadLastTurnTime: async () => now,
|
||||
loadNextGeneralTurnTime: async () => now,
|
||||
saveLastTurnTime: async () => {},
|
||||
loadCheckpoint: async () => undefined,
|
||||
saveCheckpoint: async () => {},
|
||||
},
|
||||
hooks: { onRunError },
|
||||
pauseGate: async () => {
|
||||
throw error;
|
||||
},
|
||||
},
|
||||
{ profile: 'che', defaultBudget: { budgetMs: 100, maxGenerals: 10, catchUpCap: 1 } }
|
||||
);
|
||||
await expect(lifecycle.start()).rejects.toBe(error);
|
||||
expect(onRunError).toHaveBeenCalledExactlyOnceWith(error);
|
||||
expect(processor.run).not.toHaveBeenCalled();
|
||||
expect(lifecycle.getStatus()).toMatchObject({ state: 'stopping', paused: true, lastError: error.message });
|
||||
});
|
||||
|
||||
it.each(['PREOPEN', 'SUSPENDED', 'RECONCILING', 'COMPLETED'] as const)(
|
||||
'does not dispatch an explicit run while the clock phase is %s',
|
||||
async (phase) => {
|
||||
|
||||
@@ -306,6 +306,12 @@ process 복구를 시도합니다. 관리자 화면의 오류와 PM2 process 상
|
||||
뒤 원인을 해결하고 실패한 작업을 재시도해 주세요. 재시도는 처음 고정된 commit을
|
||||
사용합니다.
|
||||
|
||||
VM 중단이나 DB 연결 장애로 turn-daemon lease가 만료되면 기존 owner는 턴과
|
||||
관리자 mutation을 처리할 수 없습니다. Lifecycle은 이를 즉시 `lastError`와
|
||||
`PAUSED`로 기록하고 종료합니다. PM2가 새 runtime과 DB snapshot으로 시작한 뒤
|
||||
관리자가 `재개`를 요청합니다. 이전 owner의 lease를 연장하거나 fencing 검증을
|
||||
우회하지 않습니다. 초기화·pause gate 실패도 같은 오류 기록 경로를 사용합니다.
|
||||
|
||||
Turn daemon의 DB persistence interactive transaction은 기본 30초입니다. 정상 turn
|
||||
budget과 같은 Prisma 기본 5초를 그대로 쓰면 populated season의 flush가 경계에서
|
||||
rollback되고 profile이 `PAUSED`로 전환될 수 있습니다. timeout을 늘려도 한
|
||||
|
||||
Reference in New Issue
Block a user