feat: 게임 시계 reconciliation 권위 기반 추가
This commit is contained in:
@@ -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
|
||||
|
||||
+81
@@ -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);
|
||||
Reference in New Issue
Block a user