refactor: 턴 경계와 12턴 묶음을 보존하는 2배속 복구 구현

This commit is contained in:
2026-09-06 10:04:33 +00:00
parent 63da0921cb
commit d3443ec7f5
53 changed files with 1467 additions and 160 deletions
+4
View File
@@ -145,6 +145,7 @@ model TurnDaemonLease {
ownerId String @map("owner_id")
leaseUntil DateTime @map("lease_until")
fencingEpoch BigInt @default(1) @map("fencing_epoch")
clockReady Boolean @default(false) @map("clock_ready")
heartbeatAt DateTime @default(now()) @map("heartbeat_at")
@@map("turn_daemon_lease")
@@ -160,6 +161,9 @@ model WorldState {
clockTick BigInt? @map("clock_tick")
clockMode String @default("realtime") @map("clock_mode")
clockWallAnchor DateTime? @map("clock_wall_anchor")
clockRecoveryStartTick BigInt? @map("clock_recovery_start_tick")
clockRecoveryEndTick BigInt? @map("clock_recovery_end_tick")
clockRecoveryStartWallAt DateTime? @map("clock_recovery_start_wall_at")
lastTurnTick BigInt? @map("last_turn_tick")
clockPhase String @default("RUNNING") @map("clock_phase")
clockRevision BigInt @default(1) @map("clock_revision")
@@ -0,0 +1,18 @@
ALTER TABLE "turn_daemon_lease" ADD COLUMN "clock_ready" BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE "world_state"
ADD COLUMN "clock_recovery_start_tick" BIGINT,
ADD COLUMN "clock_recovery_end_tick" BIGINT,
ADD COLUMN "clock_recovery_start_wall_at" TIMESTAMP(3),
ADD CONSTRAINT "world_state_turn_recovery_window_check" CHECK (
("clock_recovery_start_tick" IS NULL AND "clock_recovery_end_tick" IS NULL AND "clock_recovery_start_wall_at" IS NULL)
OR (
"clock_recovery_start_tick" IS NOT NULL AND "clock_recovery_end_tick" IS NOT NULL AND "clock_recovery_start_wall_at" IS NOT NULL
AND "clock_recovery_start_tick" BETWEEN -9007199254740991 AND 9007199254740991
AND "clock_recovery_end_tick" BETWEEN -9007199254740991 AND 9007199254740991
AND "clock_recovery_start_tick" % 36000000 = 0
AND "clock_recovery_end_tick" % 36000000 = 0
AND "clock_recovery_end_tick" - "clock_recovery_start_tick" BETWEEN 72000000 AND 792000000
AND ("clock_recovery_end_tick" - "clock_recovery_start_tick") % 72000000 = 0
)
);
+26 -2
View File
@@ -1,4 +1,4 @@
import { GameClock, inferClockPhase, parseGameClockPhase } from '@sammo-ts/common';
import { GameClock, readTurnRecovery, inferClockPhase, parseGameClockPhase } from '@sammo-ts/common';
import { GamePrisma, type GamePrismaClient } from './gamePrisma.js';
import { acquireGameSchemaAdvisoryXactLock, CLOCK_OPERATION_PERSISTENCE_LOCK } from './gameSchemaAdvisoryLock.js';
@@ -9,6 +9,21 @@ interface DbWallRow {
wallNow: Date;
}
/** 새 daemon의 시계 복구가 끝나기 전에는 독립 worker가 시간을 진행하지 않는다. */
export const readTurnRuntimeReady = async (
db: Pick<GamePrismaClient, '$queryRaw'>,
revision: bigint
): Promise<boolean> => {
const [row] = await db.$queryRaw<Array<{ ready: boolean }>>(GamePrisma.sql`
SELECT EXISTS (
SELECT 1 FROM turn_daemon_lease, world_state
WHERE clock_ready = TRUE AND lease_until > timezone('UTC', clock_timestamp())
AND clock_revision = ${revision}
) AS ready
`);
return row?.ready === true;
};
export interface InputEventClockCoordinate {
wallAt: Date;
gameAt: Date;
@@ -38,6 +53,9 @@ export const readInputEventClockCoordinate = async (
clockTick: true,
clockMode: true,
clockWallAnchor: true,
clockRecoveryStartTick: true,
clockRecoveryEndTick: true,
clockRecoveryStartWallAt: true,
tickSeconds: true,
clockPhase: true,
clockRevision: true,
@@ -60,11 +78,17 @@ export const readInputEventClockCoordinate = async (
tick,
mode,
wallAnchor: world.clockWallAnchor,
recovery: readTurnRecovery(world),
turnSeconds: world.tickSeconds,
phase,
revision,
});
const observedTick = clock.nowTick(wall.wallNow);
const ready =
phase !== 'RUNNING' ||
mode !== 'realtime' ||
world.clockRecoveryStartTick === undefined ||
(await readTurnRuntimeReady(db, world.clockRevision));
const observedTick = ready ? clock.nowTick(wall.wallNow) : clock.tick;
return {
wallAt: wall.wallNow,
gameAt: clock.tickToDate(observedTick),
+9
View File
@@ -15,6 +15,9 @@ export interface TurnEngineWorldStateRow {
clockTick: bigint | null;
clockMode: string;
clockWallAnchor: Date | null;
clockRecoveryStartTick?: bigint | null;
clockRecoveryEndTick?: bigint | null;
clockRecoveryStartWallAt?: Date | null;
lastTurnTick: bigint | null;
clockPhase: string;
clockRevision: bigint;
@@ -183,6 +186,9 @@ export interface TurnEngineWorldStateUpdateInput {
clockTick: bigint;
clockMode: string;
clockWallAnchor: Date;
clockRecoveryStartTick?: bigint | null;
clockRecoveryEndTick?: bigint | null;
clockRecoveryStartWallAt?: Date | null;
lastTurnTick: bigint;
clockPhase: string;
clockRevision: bigint;
@@ -200,6 +206,9 @@ export interface TurnEngineWorldStateCreateInput {
clockTick: bigint;
clockMode: string;
clockWallAnchor: Date;
clockRecoveryStartTick?: bigint | null;
clockRecoveryEndTick?: bigint | null;
clockRecoveryStartWallAt?: Date | null;
lastTurnTick: bigint;
clockPhase: string;
clockRevision: bigint;