From 4a9c14fd980f4dc22cbf2a631b0202a6769cbf59 Mon Sep 17 00:00:00 2001 From: Hide_D Date: Sat, 27 Dec 2025 02:44:44 +0000 Subject: [PATCH] =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80:=20?= =?UTF-8?q?=ED=84=B4=20=EB=8B=A4=EC=9D=B4=EC=95=84=EB=AA=AC=EB=93=9C=20?= =?UTF-8?q?=EC=83=9D=EB=AA=85=EC=A3=BC=EA=B8=B0=20=EB=B0=8F=20=EC=A0=9C?= =?UTF-8?q?=EC=96=B4=20=EA=B3=84=EC=95=BD=20=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture/overview.md | 3 + docs/architecture/runtime.md | 24 +++ docs/architecture/todo.md | 3 + docs/architecture/turn-daemon-lifecycle.md | 213 +++++++++++++++++++++ 4 files changed, 243 insertions(+) create mode 100644 docs/architecture/turn-daemon-lifecycle.md diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 80973e4..9cf754e 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -34,6 +34,9 @@ This document links to detailed runtime behavior in `docs/architecture/runtime.m Use that document for turn daemon scheduling, API request handling, and persistence sequencing. +The turn daemon lifecycle and control contract are documented in +`docs/architecture/turn-daemon-lifecycle.md`. + ## Documentation TODOs - Pending follow-ups: `docs/architecture/todo.md`. diff --git a/docs/architecture/runtime.md b/docs/architecture/runtime.md index 56061c4..096b27a 100644 --- a/docs/architecture/runtime.md +++ b/docs/architecture/runtime.md @@ -23,6 +23,9 @@ deployments predictable. - Communication channel: Redis Stream or Redis pub/sub - Client updates: SSE between API server and frontend where appropriate +Detailed lifecycle and control flow are defined in +`docs/architecture/turn-daemon-lifecycle.md`. + ## Engine Runtime Flow (Draft) ### Turn Daemon Loop @@ -38,6 +41,27 @@ deployments predictable. immediately even if requests remain queued. - While the daemon is resolving a turn, the API server queues incoming requests. +### Daemon Control Contract (Draft) + +API server commands are delivered to the daemon over the control channel +(Redis Stream or in-process). The daemon replies with status and run events. + +```ts +export type RunReason = 'schedule' | 'manual' | 'poke'; + +export type DaemonCommand = + | { type: 'run'; reason: RunReason; targetTime?: string; budget?: TurnRunBudget } + | { type: 'pause'; reason?: string } + | { type: 'resume'; reason?: string } + | { type: 'getStatus'; requestId: string }; + +export type DaemonEvent = + | { type: 'status'; requestId?: string; status: TurnDaemonStatus } + | { type: 'runStarted'; at: string; reason: RunReason } + | { type: 'runCompleted'; at: string; result: TurnRunResult } + | { type: 'runFailed'; at: string; error: string }; +``` + ### API Server Flow - The API server validates queries/commands and writes them to Redis Streams diff --git a/docs/architecture/todo.md b/docs/architecture/todo.md index 3eedfa1..79d4600 100644 --- a/docs/architecture/todo.md +++ b/docs/architecture/todo.md @@ -8,6 +8,9 @@ Move items into the main docs once they are finalized. - Turn daemon scheduling details and preemption rules - Turn daemon vs API server priority policy under load - In-memory state lifecycle and DBMS flush checkpoints +- [AI suggestion] Specify the turn daemon main loop (distributed lock, `runUntil(now)` catch-up, checkpoint/resume cursor, release lock) and define a status/health endpoint for ops. +- [AI suggestion] Define tick budget settings (wall time, max generals, catch-up cap) to replace PHP `max_execution_time` behavior and clarify partial progress persistence. +- [AI suggestion] Define admin controls (pause/resume, manual run, force-catch-up) and their interaction with the lock/state. - [AI suggestion] Define a constraint evaluation contract with explicit data requirements and a tri-state result (allow/deny/unknown) to support DB-backed prechecks vs in-memory full checks. - Recovery behavior after partial flush or crash - Observability: metrics, logs, and alerts for turn processing diff --git a/docs/architecture/turn-daemon-lifecycle.md b/docs/architecture/turn-daemon-lifecycle.md new file mode 100644 index 0000000..da47257 --- /dev/null +++ b/docs/architecture/turn-daemon-lifecycle.md @@ -0,0 +1,213 @@ +# Turn Daemon Lifecycle + +This document defines the lifecycle of the turn daemon for the rewrite when a +single daemon instance is responsible for turn resolution and all triggers come +from the API server. + +## Assumptions + +- One daemon process per server profile. +- API server is the only ingress for user/admin requests. +- The daemon is the only component that mutates gameplay state. + +## Responsibilities + +- Maintain authoritative in-memory state. +- Execute turn resolution when scheduled. +- Flush state changes and checkpoints to the DBMS. +- Expose status for ops and admin tooling. + +## Trigger Sources + +- Scheduled tick: next turn time is reached. +- API poke: user/admin request asks the daemon to run now. +- Admin control: pause/resume or manual catch-up. + +## State Model + +``` +Idle -> Running -> Flushing -> Idle + \-> Paused (admin) + \-> Stopping (shutdown) +``` + +- `running` flag prevents re-entrant execution when multiple triggers arrive. +- `pendingReason` tracks why a run was requested (schedule, manual, poke). + +## Main Loop Sketch + +```ts +while (!stopping) { + await waitForNextTrigger(nextTurnTime, wakeSignal); + if (paused || running) { + continue; + } + running = true; + try { + await runUntil(now(), budget); + await flushChanges(); + publishTurnEvents(); + } finally { + running = false; + } +} +``` + +## Run Budget and Checkpoints + +Replace PHP `max_execution_time` with explicit limits to allow partial progress: + +- `budgetMs`: max wall-clock time per run. +- `maxGenerals`: max generals processed per run. +- `catchUpCap`: max turns (or months) processed in one run. + +When a limit is hit, persist a checkpoint so the next run resumes safely. +Minimum checkpoint data: + +- `game_env.turntime` (last fully processed general turn time) +- optional cursor (last processed general id) if you batch within a turntime +- last known year/month if monthly transitions are mid-flight + +## API Server Interaction + +- API server enqueues mutations and pokes the daemon. +- Read-only queries can read from DBMS or a read model. +- During `running`, incoming requests are queued and processed after the run. + +## Status and Admin Controls + +Expose a status endpoint to replace `proc.php` output: + +- `running`, `paused`, `lastRunAt`, `lastDurationMs` +- `lastTurnTime`, `nextTurnTime` +- `pendingReason`, `queueDepth` + +Admin controls should toggle `paused`, trigger manual run, and request catch-up. + +### Suggested Status Endpoint (Draft) + +`GET /admin/turn-daemon/status` + +```json +{ + "profile": "che", + "state": "idle", + "running": false, + "paused": false, + "lastRunAt": "2026-01-01T12:00:00Z", + "lastDurationMs": 842, + "lastTurnTime": "2026-01-01 12:00:00", + "nextTurnTime": "2026-01-01 12:10:00", + "pendingReason": "schedule", + "queueDepth": 3, + "checkpoint": { + "turnTime": "2026-01-01 12:00:00", + "generalId": 1201, + "year": 12, + "month": 3 + } +} +``` + +### Suggested Admin Endpoints (Draft) + +`POST /admin/turn-daemon/run` + +```json +{ + "reason": "manual", + "targetTime": "2026-01-01 12:10:00", + "budgetMs": 2500, + "catchUpCap": 3 +} +``` + +`POST /admin/turn-daemon/pause` + +```json +{ + "reason": "maintenance" +} +``` + +`POST /admin/turn-daemon/resume` + +```json +{ + "reason": "maintenance complete" +} +``` + +### Daemon Control Contract (Draft) + +API server to daemon control messages (Redis Stream or in-process channel): + +```ts +export type RunReason = 'schedule' | 'manual' | 'poke'; + +export type DaemonCommand = + | { type: 'run'; reason: RunReason; targetTime?: string; budget?: TurnRunBudget } + | { type: 'pause'; reason?: string } + | { type: 'resume'; reason?: string } + | { type: 'getStatus'; requestId: string }; + +export type DaemonEvent = + | { type: 'status'; requestId?: string; status: TurnDaemonStatus } + | { type: 'runStarted'; at: string; reason: RunReason } + | { type: 'runCompleted'; at: string; result: TurnRunResult } + | { type: 'runFailed'; at: string; error: string }; +``` + +## Recovery and Shutdown + +- On startup, load `game_env.turntime` and catch up to `now`. +- On shutdown, stop accepting new triggers, finish the current run, flush, then + exit cleanly. + +## TypeScript Sketch (Draft) + +```ts +export type TurnDaemonState = 'idle' | 'running' | 'flushing' | 'paused' | 'stopping'; + +export interface TurnRunBudget { + budgetMs: number; + maxGenerals: number; + catchUpCap: number; +} + +export interface TurnCheckpoint { + turnTime: string; + generalId?: number; + year: number; + month: number; +} + +export interface TurnRunResult { + lastTurnTime: string; + processedGenerals: number; + processedTurns: number; + durationMs: number; + partial: boolean; + checkpoint?: TurnCheckpoint; +} + +export interface TurnDaemonStatus { + state: TurnDaemonState; + running: boolean; + paused: boolean; + lastRunAt?: string; + lastDurationMs?: number; + lastTurnTime?: string; + nextTurnTime?: string; + pendingReason?: RunReason; + queueDepth: number; + checkpoint?: TurnCheckpoint; +} + +export interface TurnRunner { + getStatus(): Promise; + requestRun(reason: RunReason, targetTime?: string, budget?: TurnRunBudget): Promise; + pause(reason?: string): Promise; + resume(reason?: string): Promise; +} +```