feat: 게임 시계 reconciliation 권위 기반 추가
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# Game clock reconciliation
|
||||
|
||||
## Product contract
|
||||
|
||||
Gameplay time is an integer `GameTick`; one turn is permanently `36,000,000`
|
||||
ticks. Wall time is an observation and operational-control input, never the
|
||||
authority for gameplay ordering. A long suspension advances the observed game
|
||||
coordinate to the resume wall instant without replaying skipped turns, monthly
|
||||
events, RNG, auctions, or tournaments. Every movable future schedule is shifted
|
||||
by the same exact tick delta, including the sub-turn remainder.
|
||||
|
||||
The clock state is stored in `world_state`:
|
||||
|
||||
- `clock_phase` gates gameplay commits.
|
||||
- `clock_revision` identifies the coordinate conversion generation.
|
||||
- `deadline_generation` fences worker deadlines rebuilt from that generation.
|
||||
- `clock_tick` and `clock_wall_anchor` form the durable observed-time snapshot.
|
||||
- `last_turn_tick` is the execution cursor and is independent from occurrence
|
||||
history.
|
||||
|
||||
The phases are `PREOPEN`, `RUNNING`, `SUSPENDED`, `RECONCILING`, `MANUAL`, and
|
||||
`COMPLETED`. `PREOPEN` alone permits signed negative observed ticks and floors
|
||||
executable schedules at zero. `RUNNING` never projects below its durable tick
|
||||
when wall time moves backward. `SUSPENDED`, `RECONCILING`, and `COMPLETED` do not
|
||||
permit turn or monthly commits. `MANUAL` moves only through explicit engine
|
||||
progression.
|
||||
|
||||
## Durable operation
|
||||
|
||||
A suspension begins under the turn-daemon fence and schema-scoped clock lock.
|
||||
It records the cut tick, database wall instant, rate, source revision, and
|
||||
participant checksum in `clock_suspension`. Resume reads the database wall
|
||||
instant and builds an exact plan:
|
||||
|
||||
```text
|
||||
gapTicks = max(0, ticksBetween(cutWall, resumeWall, rateAtCut))
|
||||
shiftTicks = gapTicks - catchUpTicks
|
||||
alignedTick = cutTick + gapTicks
|
||||
deadlineAfter = deadlineBefore + shiftTicks
|
||||
```
|
||||
|
||||
Planned maintenance, delayed opening, and unification wait use zero catch-up.
|
||||
The compatibility-only complete-turn behavior is named
|
||||
`LEGACY_COMPLETE_TURNS`; it is not the exact policy.
|
||||
|
||||
Every participant writes its `SHIFT`, `KEEP`, `REBUILD`, or `FORBID` decision,
|
||||
row count, and before/after checksum to `clock_reconciliation_participant`.
|
||||
The authoritative registry is
|
||||
[`game-clock-participants.json`](./game-clock-participants.json). The
|
||||
architecture gate rejects a new tick/revision field that is absent from that
|
||||
inventory.
|
||||
|
||||
## DB to Redis boundary
|
||||
|
||||
The database transaction leaves the phase `RECONCILING` and creates exactly one
|
||||
`clock_projection_outbox` row for the target revision. An outbox worker rebuilds
|
||||
auction and tournament projections and writes
|
||||
`sammo:{profile}:clock:active-revision` last. Only after checksum verification
|
||||
may the database transition to `RUNNING` for the same target revision and
|
||||
deadline generation.
|
||||
|
||||
Workers must compare DB revision, Redis active revision, phase, and deadline
|
||||
generation before dequeue and again in their final database transaction. Due
|
||||
pop is one Redis operation: verify revision/phase, read `-inf..nowTick`, and
|
||||
remove the claimed members. A failed Redis rebuild therefore leaves the game in
|
||||
`RECONCILING`; process liveness alone is not readiness.
|
||||
|
||||
## Lock order
|
||||
|
||||
All mutation paths use this order:
|
||||
|
||||
```text
|
||||
turn-daemon fencing row
|
||||
-> game-clock:operation advisory transaction lock
|
||||
-> general-access:persistence advisory transaction lock (only if needed)
|
||||
-> world_state FOR UPDATE
|
||||
-> participant rows/tables in registry order
|
||||
-> DB commit
|
||||
-> Redis outbox projection
|
||||
```
|
||||
|
||||
The ordinary turn flush already validates phase, revision, and deadline
|
||||
generation after taking this lock prefix. Clock operation participants will be
|
||||
added without changing that prefix.
|
||||
|
||||
## Opening invariant
|
||||
|
||||
Both production and direct seeding use the same scenario seeder. It stores
|
||||
`clock_tick = 0`, `last_turn_tick = 0`, and the scheduled opening as
|
||||
`clock_wall_anchor`. The metadata names `seededAtWall`, `scheduledOpenAtWall`,
|
||||
`projectedGameDateAtOpening`, and `calendarStart` separately. Precreated general
|
||||
turn ticks are calculated from zero and therefore cannot be negative. At the
|
||||
wall anchor the in-memory phase promotion refuses any PREOPEN clock whose stored
|
||||
tick is not exactly zero.
|
||||
|
||||
## Compatibility and migration
|
||||
|
||||
This branch begins with dual-read defaults for callers and fixtures built before
|
||||
the new columns. Database migration backfills manual profiles as `MANUAL`,
|
||||
future anchored realtime profiles as `PREOPEN`, and other profiles as
|
||||
`RUNNING`. Existing DateTime columns remain projections while tick columns are
|
||||
authoritative.
|
||||
|
||||
Exact reconciliation stays disabled while any registry participant is
|
||||
`FORBID`. In particular, Redis-only tournament dates and unification wait must
|
||||
be moved to durable tick/revision contracts before the operation can reach
|
||||
`RUNNING`. Removing these guards to make a partial operation pass is prohibited.
|
||||
Reference in New Issue
Block a user