feat: implement input event system with durable command handling

- Introduced InputEvent model with status tracking (PENDING, PROCESSING, SUCCEEDED, FAILED) and unique request IDs.
- Added DatabaseTurnDaemonTransport for sending commands and handling idempotency.
- Implemented executeInputEvent function to manage input event lifecycle and error handling.
- Created DatabaseTurnDaemonCommandQueue for managing command processing and lease recovery.
- Enhanced turn daemon lifecycle to support atomic command execution and error recovery.
- Added tests for input event atomicity, command queuing, and error handling scenarios.
This commit is contained in:
2026-07-25 05:36:34 +00:00
parent c5da507df9
commit 5040691d7c
34 changed files with 1587 additions and 448 deletions
+10 -3
View File
@@ -43,13 +43,14 @@ export interface TurnDaemonStatus {
export type TurnDaemonCommand =
| {
type: 'run';
requestId?: string;
reason: RunReason;
targetTime?: string;
budget?: TurnRunBudget;
}
| { type: 'pause'; reason?: string }
| { type: 'resume'; reason?: string }
| { type: 'shutdown'; reason?: string }
| { type: 'pause'; requestId?: string; reason?: string }
| { type: 'resume'; requestId?: string; reason?: string }
| { type: 'shutdown'; requestId?: string; reason?: string }
| { type: 'getStatus'; requestId?: string }
| { type: 'troopJoin'; requestId?: string; generalId: number; troopId: number }
| { type: 'troopExit'; requestId?: string; generalId: number }
@@ -186,6 +187,12 @@ export type TurnDaemonCommand =
};
export type TurnDaemonCommandResult =
| {
type: 'commandRejected';
ok: false;
commandType: TurnDaemonCommand['type'];
reason: string;
}
| {
type: 'auctionFinalize';
ok: true;
+33
View File
@@ -44,6 +44,39 @@ enum DiplomacyLetterState {
REPLACED
}
enum InputEventStatus {
PENDING
PROCESSING
SUCCEEDED
FAILED
}
enum InputEventTarget {
API
ENGINE
}
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")
@@index([target, status, sequence])
@@map("input_event")
}
model WorldState {
id Int @id @default(autoincrement())
scenarioCode String @map("scenario_code")
@@ -0,0 +1,25 @@
CREATE TYPE "InputEventStatus" AS ENUM ('PENDING', 'PROCESSING', 'SUCCEEDED', 'FAILED');
CREATE TYPE "InputEventTarget" AS ENUM ('API', 'ENGINE');
CREATE TABLE "input_event" (
"sequence" BIGSERIAL NOT NULL,
"request_id" TEXT NOT NULL,
"target" "InputEventTarget" NOT NULL,
"event_type" TEXT NOT NULL,
"payload" JSONB NOT NULL DEFAULT '{}'::jsonb,
"actor_user_id" TEXT,
"status" "InputEventStatus" NOT NULL DEFAULT 'PENDING',
"result" JSONB,
"error" TEXT,
"attempts" INTEGER NOT NULL DEFAULT 0,
"locked_by" TEXT,
"lease_until" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"processing_at" TIMESTAMP(3),
"completed_at" TIMESTAMP(3),
CONSTRAINT "input_event_pkey" PRIMARY KEY ("sequence")
);
CREATE UNIQUE INDEX "input_event_request_id_key" ON "input_event"("request_id");
CREATE INDEX "input_event_target_status_sequence_idx" ON "input_event"("target", "status", "sequence");
+1
View File
@@ -25,4 +25,5 @@ export interface DatabaseClient {
inheritanceLog: GamePrisma.InheritanceLogDelegate;
inheritanceResult: GamePrisma.InheritanceResultDelegate;
inheritanceUserState: GamePrisma.InheritanceUserStateDelegate;
inputEvent: GamePrisma.InputEventDelegate;
}