docs(clock): record lifecycle recovery boundaries

This commit was merged in pull request #1.
This commit is contained in:
2026-09-03 21:08:48 +00:00
parent b77276f202
commit ffbd51a2ef
5 changed files with 65 additions and 2 deletions
+15 -1
View File
@@ -70,6 +70,13 @@ as the archive, prompts, and final unification state. Only a `raiseInvader`
message response tied to that active suspension may pass the suspended command
queue; all other gameplay remains pending.
If the profile processes are operationally stopped during this wait, Gateway
RESUME starts the runtime without consuming the suspension. The database remains
`SUSPENDED`; the daemon-authorized response is still the only transition that may
reconcile it. Selecting one difficulty resolves every pending `raiseInvader`
alternative created at the same game tick, while preserving each message's wall
envelope as history.
The response transaction verifies daemon authority, performs the exact
alignment, applies all participant shifts, optionally changes the turn rate,
then creates the invader nation, deterministic general IDs/RNG results, first
@@ -77,7 +84,8 @@ turns, and the final target-revision outbox. The optional rate change refreshes
the outbox with the final base/rate before commit. DB remains `RECONCILING`
until the daemon projection worker applies Redis and verifies the target
revision/generation. Games without an invader choice move directly to
`COMPLETED`.
`COMPLETED`. After an invader game reaches `isUnited=3`, `InvaderEnding` also
changes the clock to `COMPLETED` in the terminal monthly transaction.
## DB to Redis boundary
@@ -163,6 +171,12 @@ it never changes the rule to WALL_TIME. A WALL rule likewise never derives an
authority tick. See [`time-domains.md`](./time-domains.md) for the complete
inventory and migration policy.
Migration `20260903183000_turn_daemon_lease_utc_wall` expires ephemeral daemon
leases at deployment and installs UTC wall defaults. Migration
`20260903201500_complete_invader_game_clock` repairs legacy `isUnited=3` worlds
left in a running/manual phase and resolves obsolete unchosen invader actions at
the terminal authoritative game tick.
No active participant remains `FORBID`. Tournament writes carry
tick/revision/generation coordinates and are revision-fenced in Redis.
Unification wait uses the same durable ledger and outbox boundary; the former
+14
View File
@@ -44,6 +44,12 @@ Redis 투영은 DB commit 뒤 action ID로 멱등 적용됩니다.
phase/revision/generation을 다시 잠가 검증합니다. 단계 전환·마감·정산은 실행하지
않으며, 원자적 reconciliation이 진행되는 `RECONCILING`에서는 새 베팅도 받지 않습니다.
daemon lease는 게임 schedule이 아니라 운영 `WALL_TIME`입니다. 게임 DB session이
`Asia/Seoul`이어도 acquire/renew/assert/release는 모두
`CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`를 사용합니다. 따라서 정지한 게임의
lease도 현실 시간에 만료되며, KST session에서 UTC `timestamp without time zone`
column을 9시간 미래로 쓰지 않습니다.
## 중단 후 재개
realtime daemon은 재개할 때 Ref `checkDelay()`와 같은 한도를 적용합니다.
@@ -60,6 +66,14 @@ realtime daemon은 재개할 때 Ref `checkDelay()`와 같은 한도를 적용
장수 턴 tick은 바꾸지 않습니다. 동시에 `clock_wall_anchor`를 작업 실행
시각으로 다시 고정합니다.
통일 후 이민족 선택을 기다리는 `UNIFICATION_WAIT`는 일반 maintenance 재개와
다릅니다. 운영상 STOP된 profile을 RESUME하면 Gateway는 프로세스만 다시 띄우고
게임 clock은 `SUSPENDED`로 유지합니다. daemon이 수신자 소유권과 command fence를
확인한 `raiseInvader` 응답만 suspension을 원자적으로 reconciliation할 수 있습니다.
한 난이도를 수락하면 같은 통일 tick의 다른 선택지는 모두 resolved 처리됩니다.
이민족전이 끝나 `isUnited=3`이 되면 clock phase도 같은 월 transaction에서
`COMPLETED`가 되어 이후 GAME_TIME이 진행하지 않습니다.
DB migration은 GAME 규칙의 기존 DateTime 투영에서 tick을 채웁니다. 새 설치와 migration
재실행은 `prisma:migrate:deploy:game`으로 수행합니다. 메시지의 연도 9999 같은
무기한 호환값은 일반 메시지의 투영일 뿐입니다. actionable deadline은
+11
View File
@@ -222,6 +222,11 @@ claim 가능한 상태에서 attempts를 증가시켜 재처리합니다.
5. `EngineStateManager`가 in-memory mutation과 transaction flush를 묶습니다.
6. `TurnDaemonLifecycle`이 control queue와 schedule을 실행합니다.
`turn_daemon_lease`는 운영 WALL_TIME입니다. 모든 write와 active 비교는 DB
session timezone과 무관하게 UTC wall expression을 사용합니다. Lease migration은
profile process가 멈춘 배포 경계에서 기존 ephemeral row를 만료시켜 구버전
writer가 만든 KST timestamp도 새 daemon의 fresh fencing epoch를 막지 않습니다.
Lifecycle은 가장 빠른 장수 턴과 다음 tick 중 앞선 시각을 선택합니다.
pause gate, 수동 run, shutdown과 budget을 같은 loop에서 처리합니다.
@@ -290,6 +295,12 @@ event catalog, in-memory state, dirty marking, flush와 reload 검증까지
- Worker timeout은 요청 실패로 반환하며 DB commit 여부를 별도로 확인합니다.
- API·daemon process 재시작은 `InputEvent`, lease, checkpoint와 operation
상태에서 이어집니다.
- `UNIFICATION_WAIT` 중 operational STOP/재시작은 profile process만 복구하며
suspension을 일반 RESUME으로 소비하지 않습니다. 선택 응답의 daemon fence가
clock alignment와 이민족 생성을 함께 commit합니다.
- test-only `ManualClock``StepClock`의 sleep은 논리 시간을 즉시 전진한 뒤
event loop에 한 번 양보합니다. terminal/suspended polling이 timer 기반
shutdown과 test timeout을 굶기지 않아야 합니다.
- 운영 process와 외부 Caddy 상태는 local build·mock E2E로 증명되지 않습니다.
## Build와 배포
+10 -1
View File
@@ -40,7 +40,7 @@ representation.
| `input_event.processing_game_tick`, `processing_clock_revision`, `processing_deadline_generation` | actual mutation boundary | GAME metadata | stop | KEEP | effect validation/RNG uses this coordinate |
| `input_event.created_at`, `processing_at`, `completed_at`, `lease_until` | request receipt, processing audit, lease | WALL, DB UTC | advance | excluded | external occurrence and worker lease |
| `read_model_outbox.*_at`, `web_push_outbox.*_at` | availability, claim, delivery, audit | WALL, DB UTC | advance | excluded | retry and notification delivery are operational |
| `turn_daemon_lease.lease_until`, `heartbeat_at` | daemon liveness | WALL, DB UTC | advance | excluded | a paused game must still lose a dead daemon lease |
| `turn_daemon_lease.lease_until`, `heartbeat_at` | daemon liveness | WALL, DB UTC | advance | excluded | acquire/renew/assert/release use `CURRENT_TIMESTAMP AT TIME ZONE 'UTC'`, including KST sessions |
| `general.turn_tick` | next general turn | GAME | stop | SHIFT; `turn_time` projection | determines engine order |
| `general.recent_war_tick` | past battle occurrence | GAME | stop | KEEP; `recent_war_time` projection | historical event does not move |
| `general.meta.next_change_tick` | N-turn reselection cooldown | GAME | stop | SHIFT; `next_change`/`nextChangeAt` projections | expressed in turns; missing tick fails closed |
@@ -164,3 +164,12 @@ Old projection columns remain during rolling deployment, but new code never
chooses a clock by NULL fallback: GAME rules require their tick; WALL rules use
their wall column. The disposable migration verifier covers populated upgrade,
indexes/constraints, replay safety, and a second no-op deploy.
Migration `20260903183000_turn_daemon_lease_utc_wall` treats daemon leases as
ephemeral WALL authority: it expires pre-deployment rows, changes the heartbeat
default to DB UTC, and requires the new daemon to acquire a fresh fencing epoch.
Migration `20260903201500_complete_invader_game_clock` moves historical
`isUnited>=3` worlds from `RUNNING`/`MANUAL` to `COMPLETED` and resolves pending
unchosen invader actions at `max(created_game_tick, world_state.clock_tick)`.
Neither migration shifts a WALL occurrence or turns one rule into a clock
fallback.