문서 추가: 턴 다이아몬드 생명주기 및 제어 계약 문서화

This commit is contained in:
2025-12-27 02:44:44 +00:00
parent d9c14ddec9
commit 4a9c14fd98
4 changed files with 243 additions and 0 deletions
+3
View File
@@ -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 Use that document for turn daemon scheduling, API request handling, and
persistence sequencing. persistence sequencing.
The turn daemon lifecycle and control contract are documented in
`docs/architecture/turn-daemon-lifecycle.md`.
## Documentation TODOs ## Documentation TODOs
- Pending follow-ups: `docs/architecture/todo.md`. - Pending follow-ups: `docs/architecture/todo.md`.
+24
View File
@@ -23,6 +23,9 @@ deployments predictable.
- Communication channel: Redis Stream or Redis pub/sub - Communication channel: Redis Stream or Redis pub/sub
- Client updates: SSE between API server and frontend where appropriate - 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) ## Engine Runtime Flow (Draft)
### Turn Daemon Loop ### Turn Daemon Loop
@@ -38,6 +41,27 @@ deployments predictable.
immediately even if requests remain queued. immediately even if requests remain queued.
- While the daemon is resolving a turn, the API server queues incoming requests. - 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 ### API Server Flow
- The API server validates queries/commands and writes them to Redis Streams - The API server validates queries/commands and writes them to Redis Streams
+3
View File
@@ -8,6 +8,9 @@ Move items into the main docs once they are finalized.
- Turn daemon scheduling details and preemption rules - Turn daemon scheduling details and preemption rules
- Turn daemon vs API server priority policy under load - Turn daemon vs API server priority policy under load
- In-memory state lifecycle and DBMS flush checkpoints - 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. - [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 - Recovery behavior after partial flush or crash
- Observability: metrics, logs, and alerts for turn processing - Observability: metrics, logs, and alerts for turn processing
+213
View File
@@ -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<TurnDaemonStatus>;
requestRun(reason: RunReason, targetTime?: string, budget?: TurnRunBudget): Promise<void>;
pause(reason?: string): Promise<void>;
resume(reason?: string): Promise<void>;
}
```