merge: 훼 밀린 턴 처리와 데몬 메모리 설정 반영
This commit is contained in:
@@ -183,11 +183,15 @@ export class TurnDaemonLifecycle {
|
|||||||
const gameNowMs = gameClock?.now.getTime() ?? nowMs;
|
const gameNowMs = gameClock?.now.getTime() ?? nowMs;
|
||||||
const nextTurnMs = nextRunTime.getTime();
|
const nextTurnMs = nextRunTime.getTime();
|
||||||
if (gameNowMs >= nextTurnMs) {
|
if (gameNowMs >= nextTurnMs) {
|
||||||
// Ref checkDelay() executes every turn due at the observed
|
// Preserve Ref's chronological boundary even when realtime has
|
||||||
// wall-clock time in one snapshot. Using only the oldest due
|
// fallen multiple months behind. Draining every general through
|
||||||
// timestamp lets generals created by that batch run before a
|
// gameNow before advancing the first pending month labels and
|
||||||
// monthly boundary, although Ref defers them to the next pass.
|
// resolves all of those future commands in the stale month and
|
||||||
await this.runOnce({ reason: 'schedule', targetTime: new Date(gameNowMs) });
|
// can build an unbounded same-month catch-up backlog. Within the current
|
||||||
|
// month we still observe every due general through gameNow; once
|
||||||
|
// a month boundary is overdue, process only through that boundary.
|
||||||
|
const nextMonthMs = this.getNextTickTime(new Date(this.status.lastTurnTime!)).getTime();
|
||||||
|
await this.runOnce({ reason: 'schedule', targetTime: new Date(Math.min(gameNowMs, nextMonthMs)) });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -447,11 +447,11 @@ describe('TurnDaemonLifecycle', () => {
|
|||||||
await loop;
|
await loop;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('catches up through the observed clock with queue and checkpoint context', async () => {
|
it('limits realtime backlog to the next month with queue and checkpoint context', async () => {
|
||||||
const turnTermMinutes = 10;
|
const turnTermMinutes = 10;
|
||||||
const lastTurnTime = new Date(2026, 0, 2, 2, 0, 0, 0);
|
const lastTurnTime = new Date(2026, 0, 2, 2, 0, 0, 0);
|
||||||
const generalTurnQueue = [addMinutes(lastTurnTime, 5), addMinutes(lastTurnTime, 20)];
|
const generalTurnQueue = [addMinutes(lastTurnTime, 5), addMinutes(lastTurnTime, 20)];
|
||||||
const expectedRunTimeMs = addMinutes(lastTurnTime, 30).getTime();
|
const expectedRunTimeMs = addMinutes(lastTurnTime, turnTermMinutes).getTime();
|
||||||
const checkpoint = {
|
const checkpoint = {
|
||||||
turnTime: lastTurnTime.toISOString(),
|
turnTime: lastTurnTime.toISOString(),
|
||||||
generalId: 101,
|
generalId: 101,
|
||||||
@@ -524,11 +524,11 @@ describe('TurnDaemonLifecycle', () => {
|
|||||||
await loop;
|
await loop;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('catches up through the observed clock when tick boundary precedes the queue front', async () => {
|
it('limits realtime backlog to the next month when the boundary precedes the queue front', async () => {
|
||||||
const turnTermMinutes = 10;
|
const turnTermMinutes = 10;
|
||||||
const lastTurnTime = new Date(2026, 0, 2, 2, 0, 0, 0);
|
const lastTurnTime = new Date(2026, 0, 2, 2, 0, 0, 0);
|
||||||
const generalTurnQueue = [addMinutes(lastTurnTime, 15), addMinutes(lastTurnTime, 30)];
|
const generalTurnQueue = [addMinutes(lastTurnTime, 15), addMinutes(lastTurnTime, 30)];
|
||||||
const expectedRunTimeMs = addMinutes(lastTurnTime, 30).getTime();
|
const expectedRunTimeMs = addMinutes(lastTurnTime, turnTermMinutes).getTime();
|
||||||
const checkpoint = {
|
const checkpoint = {
|
||||||
turnTime: lastTurnTime.toISOString(),
|
turnTime: lastTurnTime.toISOString(),
|
||||||
generalId: 102,
|
generalId: 102,
|
||||||
|
|||||||
@@ -393,6 +393,7 @@ export const buildProcessDefinitions = (
|
|||||||
const daemonCwd = path.join(runtimeWorkspace, 'app', 'game-engine');
|
const daemonCwd = path.join(runtimeWorkspace, 'app', 'game-engine');
|
||||||
const apiScript = path.join(apiCwd, 'dist', 'index.js');
|
const apiScript = path.join(apiCwd, 'dist', 'index.js');
|
||||||
const daemonScript = path.join(daemonCwd, 'dist', 'index.js');
|
const daemonScript = path.join(daemonCwd, 'dist', 'index.js');
|
||||||
|
const turnDaemonNodeOptions = baseEnv.TURN_DAEMON_NODE_OPTIONS?.trim();
|
||||||
const apiEnv = {
|
const apiEnv = {
|
||||||
...baseEnv,
|
...baseEnv,
|
||||||
GAME_API_ROLE: 'server',
|
GAME_API_ROLE: 'server',
|
||||||
@@ -409,6 +410,7 @@ export const buildProcessDefinitions = (
|
|||||||
};
|
};
|
||||||
const daemonEnv = {
|
const daemonEnv = {
|
||||||
...baseEnv,
|
...baseEnv,
|
||||||
|
...(turnDaemonNodeOptions ? { NODE_OPTIONS: turnDaemonNodeOptions } : {}),
|
||||||
GAME_ENGINE_ROLE: 'turn-daemon',
|
GAME_ENGINE_ROLE: 'turn-daemon',
|
||||||
TURN_PROFILE: profile.profile,
|
TURN_PROFILE: profile.profile,
|
||||||
PROFILE: profile.profile,
|
PROFILE: profile.profile,
|
||||||
|
|||||||
@@ -233,6 +233,23 @@ describe('buildProcessDefinitions', () => {
|
|||||||
}
|
}
|
||||||
expect(definitions.frontend.env.VITE_APP_BASE_PATH).toBe('/che');
|
expect(definitions.frontend.env.VITE_APP_BASE_PATH).toBe('/che');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('applies a dedicated Node heap option only to the turn daemon', () => {
|
||||||
|
const definitions = buildProcessDefinitions(buildProfile(), {
|
||||||
|
...processConfig,
|
||||||
|
baseEnv: {
|
||||||
|
NODE_OPTIONS: '--max-old-space-size=1536',
|
||||||
|
TURN_DAEMON_NODE_OPTIONS: '--max-old-space-size=3072',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(definitions.daemon.env.NODE_OPTIONS).toBe('--max-old-space-size=3072');
|
||||||
|
expect(definitions.frontend.env.NODE_OPTIONS).toBe('--max-old-space-size=1536');
|
||||||
|
expect(definitions.api.env.NODE_OPTIONS).toBe('--max-old-space-size=1536');
|
||||||
|
expect(definitions.auction.env.NODE_OPTIONS).toBe('--max-old-space-size=1536');
|
||||||
|
expect(definitions.battleSim.env.NODE_OPTIONS).toBe('--max-old-space-size=1536');
|
||||||
|
expect(definitions.tournament.env.NODE_OPTIONS).toBe('--max-old-space-size=1536');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sanitizeManagedProcessEnv', () => {
|
describe('sanitizeManagedProcessEnv', () => {
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ profile 범위 권한과 별개인 전역 `admin.releases.manage` 권한이 필
|
|||||||
API URL이 다른 frontend artifact를 cache hit로 잘못 복원하지 않습니다.
|
API URL이 다른 frontend artifact를 cache hit로 잘못 복원하지 않습니다.
|
||||||
`NODE_OPTIONS`와 `RAYON_NUM_THREADS`는 출력에는 영향을 주지 않는 resource 제한으로
|
`NODE_OPTIONS`와 `RAYON_NUM_THREADS`는 출력에는 영향을 주지 않는 resource 제한으로
|
||||||
build child에 전달됩니다.
|
build child에 전달됩니다.
|
||||||
|
- `TURN_DAEMON_NODE_OPTIONS`가 설정되어 있으면 Gateway orchestrator는 그 값을
|
||||||
|
turn-daemon PM2 process의 `NODE_OPTIONS`로만 덮어씁니다. API·frontend·worker와
|
||||||
|
build는 공용 `NODE_OPTIONS`를 계속 사용합니다. 전용 heap을 늘릴 때는 runtime
|
||||||
|
container hard limit과 전체 process RSS를 먼저 확인합니다.
|
||||||
- migration 이후 이전 애플리케이션으로 돌아갈 때 schema 하위 호환성이
|
- migration 이후 이전 애플리케이션으로 돌아갈 때 schema 하위 호환성이
|
||||||
유지됩니다.
|
유지됩니다.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user