오류 정지 중 가입 턴을 고정하고 화면 이동 실패 복구 지원

This commit is contained in:
2026-09-12 02:02:31 +00:00
parent 5041fc365e
commit 21d10d5dfc
15 changed files with 448 additions and 26 deletions
+19 -18
View File
@@ -1,4 +1,5 @@
import { randomUUID } from 'node:crypto';
import { createRuntimePauseGate } from './runtimePauseGate.js';
import { loadActionModuleBundle, type TurnCommandProfile, type TurnSchedule } from '@sammo-ts/logic';
import {
@@ -716,6 +717,7 @@ const createTurnDaemonRuntimeWithLease = async (
let stopClockProjectionWorker = () => {};
let applyClockProjection: DatabaseTurnHooks['applyClockProjection'] | undefined;
let synchronizeClockAuthority: DatabaseTurnHooks['synchronizeClockAuthority'] | undefined;
let prepareClockRecovery: DatabaseTurnHooks['prepareRealtimeRecovery'] | undefined;
const nationTraits = await loadNationTraitModules([...NATION_TRAIT_KEYS], new NationTraitLoader());
const nationTraitMap = new Map(nationTraits.map((module) => [module.key, module]));
const monthlyActionModules = await loadActionModuleBundle(
@@ -941,11 +943,16 @@ const createTurnDaemonRuntimeWithLease = async (
onRunError: async (error) => {
await dbHooks.hooks.onRunError?.(error);
await gatewayGate?.markPaused(error);
if (!turnDaemonLease?.isLost() && world.getGameClockState().phase === 'RUNNING') {
// 같은 command batch의 다음 가입도 정지된 시각을 보게 한다.
await dbHooks.prepareRealtimeRecovery({ paused: true });
}
},
};
takeCommittedReadModelChangeReceipt = dbHooks.takeCommittedReadModelChangeReceipt;
applyClockProjection = dbHooks.applyClockProjection;
synchronizeClockAuthority = dbHooks.synchronizeClockAuthority;
prepareClockRecovery = dbHooks.prepareRealtimeRecovery;
close = async () => {
if (auctionBidder) {
await auctionBidder.close();
@@ -1060,7 +1067,6 @@ const createTurnDaemonRuntimeWithLease = async (
maxGenerals: 200,
catchUpCap: 1,
};
let lastObservedGatewayPause: boolean | null = null;
const lifecycle = new TurnDaemonLifecycle(
{
@@ -1071,23 +1077,18 @@ const createTurnDaemonRuntimeWithLease = async (
stateStore,
processor,
hooks,
pauseGate: async () => {
if (turnDaemonLease?.isLost()) {
// 만료된 owner는 재개 명령도 처리할 수 없다. 현재 runtime을
// 끝내 PM2가 새 owner와 DB snapshot으로 시작하도록 한다.
throw turnDaemonLease.getLossError();
}
const gatewayPaused = (await pauseGate?.()) ?? false;
const phase = world.getGameClockState().phase;
const phaseNeedsSync = gatewayPaused
? phase !== 'SUSPENDED'
: phase === 'SUSPENDED' || phase === 'RECONCILING';
if (synchronizeClockAuthority && (lastObservedGatewayPause !== gatewayPaused || phaseNeedsSync)) {
await synchronizeClockAuthority();
}
lastObservedGatewayPause = gatewayPaused;
return gatewayPaused;
},
pauseGate: createRuntimePauseGate({
assertLease: () => {
if (turnDaemonLease?.isLost()) throw turnDaemonLease.getLossError();
},
shouldPause: async () => (await pauseGate?.()) ?? false,
getPhase: () => world.getGameClockState().phase,
isExplicitlyPaused: () => gatewayGate?.isExplicitlyPaused() ?? false,
prepareRecovery: async (recoveryOptions) => {
await prepareClockRecovery?.(recoveryOptions);
},
synchronize: async () => synchronizeClockAuthority?.(),
}),
commandHandler,
commandResponder: options.controlQueue ? undefined : (databaseCommandQueue ?? undefined),
// The exclusive fixture runner aborts the entire in-memory runtime