feat: 게임 시계 reconciliation 권위 기반 추가

This commit is contained in:
2026-09-03 08:44:43 +00:00
parent b91dcbcaaa
commit ae7d55ef47
29 changed files with 1265 additions and 81 deletions
+107 -29
View File
@@ -63,21 +63,23 @@ enum InputEventTarget {
}
model InputEvent {
sequence BigInt @id @default(autoincrement())
requestId String @unique @map("request_id")
target InputEventTarget
eventType String @map("event_type")
payload Json @default(dbgenerated("'{}'::jsonb"))
actorUserId String? @map("actor_user_id")
status InputEventStatus @default(PENDING)
result Json?
error String?
attempts Int @default(0)
lockedBy String? @map("locked_by")
leaseUntil DateTime? @map("lease_until")
createdAt DateTime @default(now()) @map("created_at")
processingAt DateTime? @map("processing_at")
completedAt DateTime? @map("completed_at")
sequence BigInt @id @default(autoincrement())
requestId String @unique @map("request_id")
target InputEventTarget
eventType String @map("event_type")
payload Json @default(dbgenerated("'{}'::jsonb"))
actorUserId String? @map("actor_user_id")
acceptedGameTick BigInt? @map("accepted_game_tick")
acceptedClockRevision BigInt? @map("accepted_clock_revision")
status InputEventStatus @default(PENDING)
result Json?
error String?
attempts Int @default(0)
lockedBy String? @map("locked_by")
leaseUntil DateTime? @map("lease_until")
createdAt DateTime @default(now()) @map("created_at")
processingAt DateTime? @map("processing_at")
completedAt DateTime? @map("completed_at")
@@index([target, status, sequence])
@@map("input_event")
@@ -145,25 +147,101 @@ model TurnDaemonLease {
}
model WorldState {
id Int @id @default(autoincrement())
scenarioCode String @map("scenario_code")
currentYear Int @map("current_year")
currentMonth Int @map("current_month")
tickSeconds Int @map("tick_seconds")
clockBaseTime DateTime? @map("clock_base_time")
clockTick BigInt? @map("clock_tick")
clockMode String @default("realtime") @map("clock_mode")
clockWallAnchor DateTime? @map("clock_wall_anchor")
lastTurnTick BigInt? @map("last_turn_tick")
config Json @default(dbgenerated("'{}'::jsonb"))
meta Json @default(dbgenerated("'{}'::jsonb"))
updatedAt DateTime @updatedAt @map("updated_at")
id Int @id @default(autoincrement())
scenarioCode String @map("scenario_code")
currentYear Int @map("current_year")
currentMonth Int @map("current_month")
tickSeconds Int @map("tick_seconds")
clockBaseTime DateTime? @map("clock_base_time")
clockTick BigInt? @map("clock_tick")
clockMode String @default("realtime") @map("clock_mode")
clockWallAnchor DateTime? @map("clock_wall_anchor")
lastTurnTick BigInt? @map("last_turn_tick")
clockPhase String @default("RUNNING") @map("clock_phase")
clockRevision BigInt @default(1) @map("clock_revision")
deadlineGeneration BigInt @default(1) @map("deadline_generation")
config Json @default(dbgenerated("'{}'::jsonb"))
meta Json @default(dbgenerated("'{}'::jsonb"))
updatedAt DateTime @updatedAt @map("updated_at")
trafficPeriods TrafficPeriod[]
trafficPeriods TrafficPeriod[]
clockSuspensions ClockSuspension[]
clockProjectionOutbox ClockProjectionOutbox[]
@@map("world_state")
}
model ClockSuspension {
id String @id @db.VarChar(64)
worldStateId Int @map("world_state_id")
source String
policy String
status String @default("SUSPENDED")
sourceRevision BigInt @map("source_revision")
targetRevision BigInt @map("target_revision")
cutTick BigInt @map("cut_tick")
cutWallAt DateTime @map("cut_wall_at") @db.Timestamp(3)
resumeWallAt DateTime? @map("resume_wall_at") @db.Timestamp(3)
rateTicksPerSecond Int @map("rate_ticks_per_second")
catchUpTicks BigInt @default(0) @map("catch_up_ticks")
gapTicks BigInt? @map("gap_ticks")
shiftTicks BigInt? @map("shift_ticks")
alignedTick BigInt? @map("aligned_tick")
participantChecksumBefore String? @map("participant_checksum_before")
participantChecksumAfter String? @map("participant_checksum_after")
detail Json @default(dbgenerated("'{}'::jsonb"))
createdAt DateTime @default(dbgenerated("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')")) @map("created_at") @db.Timestamp(3)
updatedAt DateTime @default(dbgenerated("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')")) @updatedAt @map("updated_at") @db.Timestamp(3)
worldState WorldState @relation(fields: [worldStateId], references: [id], onDelete: Cascade)
participants ClockReconciliationParticipant[]
projectionOutbox ClockProjectionOutbox[]
@@unique([worldStateId, targetRevision])
@@index([status, createdAt])
@@map("clock_suspension")
}
model ClockReconciliationParticipant {
suspensionId String @map("suspension_id") @db.VarChar(64)
participantKey String @map("participant_key") @db.VarChar(96)
policy String
beforeChecksum String @map("before_checksum") @db.VarChar(128)
afterChecksum String @map("after_checksum") @db.VarChar(128)
affectedCount Int @default(0) @map("affected_count")
detail Json @default(dbgenerated("'{}'::jsonb"))
suspension ClockSuspension @relation(fields: [suspensionId], references: [id], onDelete: Cascade)
@@id([suspensionId, participantKey])
@@map("clock_reconciliation_participant")
}
model ClockProjectionOutbox {
id BigInt @id @default(autoincrement())
worldStateId Int @map("world_state_id")
suspensionId String? @map("suspension_id") @db.VarChar(64)
targetRevision BigInt @map("target_revision")
status String @default("PENDING")
payload Json @default(dbgenerated("'{}'::jsonb"))
checksum String @db.VarChar(128)
attempts Int @default(0)
availableAt DateTime @default(dbgenerated("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')")) @map("available_at") @db.Timestamp(3)
lockedAt DateTime? @map("locked_at") @db.Timestamp(3)
lockedBy String? @map("locked_by") @db.VarChar(128)
appliedAt DateTime? @map("applied_at") @db.Timestamp(3)
lastError String? @map("last_error")
createdAt DateTime @default(dbgenerated("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')")) @map("created_at") @db.Timestamp(3)
updatedAt DateTime @default(dbgenerated("(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')")) @updatedAt @map("updated_at") @db.Timestamp(3)
worldState WorldState @relation(fields: [worldStateId], references: [id], onDelete: Cascade)
suspension ClockSuspension? @relation(fields: [suspensionId], references: [id], onDelete: Cascade)
@@unique([worldStateId, targetRevision])
@@index([status, availableAt, id])
@@map("clock_projection_outbox")
}
model Nation {
id Int @id
name String
@@ -0,0 +1,81 @@
ALTER TABLE world_state
ADD COLUMN clock_phase TEXT NOT NULL DEFAULT 'RUNNING',
ADD COLUMN clock_revision BIGINT NOT NULL DEFAULT 1,
ADD COLUMN deadline_generation BIGINT NOT NULL DEFAULT 1;
UPDATE world_state
SET clock_phase = CASE
WHEN clock_mode = 'manual' THEN 'MANUAL'
WHEN clock_wall_anchor IS NOT NULL AND clock_wall_anchor > CURRENT_TIMESTAMP THEN 'PREOPEN'
ELSE 'RUNNING'
END;
ALTER TABLE input_event
ADD COLUMN accepted_game_tick BIGINT,
ADD COLUMN accepted_clock_revision BIGINT;
CREATE TABLE clock_suspension (
id VARCHAR(64) PRIMARY KEY,
world_state_id INTEGER NOT NULL REFERENCES world_state(id) ON DELETE CASCADE,
source TEXT NOT NULL,
policy TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'SUSPENDED',
source_revision BIGINT NOT NULL,
target_revision BIGINT NOT NULL,
cut_tick BIGINT NOT NULL,
cut_wall_at TIMESTAMP(3) NOT NULL,
resume_wall_at TIMESTAMP(3),
rate_ticks_per_second INTEGER NOT NULL,
catch_up_ticks BIGINT NOT NULL DEFAULT 0,
gap_ticks BIGINT,
shift_ticks BIGINT,
aligned_tick BIGINT,
participant_checksum_before VARCHAR(128),
participant_checksum_after VARCHAR(128),
detail JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMP(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
updated_at TIMESTAMP(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
CONSTRAINT clock_suspension_world_revision_key UNIQUE (world_state_id, target_revision),
CONSTRAINT clock_suspension_revision_step CHECK (target_revision = source_revision + 1),
CONSTRAINT clock_suspension_nonnegative_catchup CHECK (catch_up_ticks >= 0),
CONSTRAINT clock_suspension_positive_rate CHECK (rate_ticks_per_second > 0)
);
CREATE INDEX clock_suspension_status_created_at_idx ON clock_suspension(status, created_at);
CREATE TABLE clock_reconciliation_participant (
suspension_id VARCHAR(64) NOT NULL REFERENCES clock_suspension(id) ON DELETE CASCADE,
participant_key VARCHAR(96) NOT NULL,
policy TEXT NOT NULL,
before_checksum VARCHAR(128) NOT NULL,
after_checksum VARCHAR(128) NOT NULL,
affected_count INTEGER NOT NULL DEFAULT 0,
detail JSONB NOT NULL DEFAULT '{}'::jsonb,
PRIMARY KEY (suspension_id, participant_key),
CONSTRAINT clock_reconciliation_participant_policy_check CHECK (policy IN ('SHIFT', 'KEEP', 'REBUILD', 'FORBID')),
CONSTRAINT clock_reconciliation_participant_count_check CHECK (affected_count >= 0)
);
CREATE TABLE clock_projection_outbox (
id BIGSERIAL PRIMARY KEY,
world_state_id INTEGER NOT NULL REFERENCES world_state(id) ON DELETE CASCADE,
suspension_id VARCHAR(64) REFERENCES clock_suspension(id) ON DELETE CASCADE,
target_revision BIGINT NOT NULL,
status TEXT NOT NULL DEFAULT 'PENDING',
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
checksum VARCHAR(128) NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
available_at TIMESTAMP(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
locked_at TIMESTAMP(3),
locked_by VARCHAR(128),
applied_at TIMESTAMP(3),
last_error TEXT,
created_at TIMESTAMP(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
updated_at TIMESTAMP(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),
CONSTRAINT clock_projection_outbox_world_revision_key UNIQUE (world_state_id, target_revision),
CONSTRAINT clock_projection_outbox_status_check CHECK (status IN ('PENDING', 'APPLYING', 'APPLIED', 'FAILED')),
CONSTRAINT clock_projection_outbox_attempts_check CHECK (attempts >= 0)
);
CREATE INDEX clock_projection_outbox_status_available_at_id_idx
ON clock_projection_outbox(status, available_at, id);