Files
core2026/docs/developer/request-turn-persistence.md
T

5.1 KiB

요청·턴·저장 흐름

조회 요청

일반 query는 다음 경로를 따릅니다.

Vue view/store
  -> tRPC client
  -> game-api router
  -> session actor + 입력 validation
  -> Prisma query
  -> 권한에 맞춘 DTO/redaction
  -> 화면 상태

조회는 engine의 in-memory object를 직접 공유하지 않습니다. 따라서 daemon이 transaction을 commit하기 전의 중간 계산은 API query에 노출되지 않습니다.

API가 직접 끝내는 mutation

예약 턴, 메시지처럼 API가 DB에서 완결할 수 있는 변경도 input_event를 사용합니다.

  1. Idempotency-Key와 tRPC path로 요청 identity를 만듭니다.
  2. app/game-api/src/inputEventBoundary.ts가 중복·처리 상태를 확인합니다.
  3. 같은 PostgreSQL transaction에서 대상 row와 input event 결과를 저장합니다.
  4. commit 뒤 응답하고 필요한 realtime 갱신을 알립니다.

동일 revision을 전제로 한 예약 턴 수정은 다른 탭이나 요청이 먼저 갱신했으면 충돌합니다. frontend는 최신 목록을 다시 불러와 사용자의 변경을 덮어쓰지 않게 해야 합니다.

engine이 처리하는 mutation

game-api mutation
  -> input_event PENDING
  -> daemon claim (FOR UPDATE SKIP LOCKED)
  -> lease/fencing 확인
  -> EngineStateManager savepoint
  -> command/turn/monthly handler가 InMemoryTurnWorld 변경
  -> world + 예약 턴 + log + message + event 결과 flush
  -> input_event COMPLETED를 같은 DB transaction으로 commit
  -> commit 이후 realtime 신호

계산이나 DB 쓰기가 실패하면 EngineStateManager가 등록된 in-memory participant를 savepoint로 되돌립니다. DB transaction도 commit되지 않아 메모리와 DB의 부분 진행을 피합니다. lease를 잃은 worker는 stale 결과를 commit할 수 없어야 합니다.

한 tick의 처리

TurnDaemonLifecycle은 clock과 schedule에서 다음 실행 시점을 구합니다. 턴을 시작하면 InMemoryTurnProcessorInMemoryTurnWorldturnTime, 그다음 general.id 순서로 실행 대상을 결정합니다. checkpoint는 재시작 시 이미 처리한 동일 시점의 장수를 건너뛰는 기준입니다.

장수 한 명의 예약 명령은 대략 다음 순서입니다.

  1. InMemoryReservedTurnStore에서 첫 예약 명령을 읽습니다.
  2. 명령 key를 GeneralTurnCommandLoader 또는 NationTurnCommandLoader로 불러옵니다.
  3. actionContextBuilder가 대상 도시·국가·장수, map, unit set, 외교, 시간과 RNG를 구성합니다.
  4. permission/min/full constraint를 목적에 맞게 평가합니다.
  5. 선행 턴이 있으면 진행 상태를 쌓고, 완성된 시점에 resolve()를 실행합니다.
  6. effect와 직접 변경을 world에 반영하고 로그·메시지·후속 턴 시간을 기록합니다.
  7. 실행된 queue를 당기고 끝에 기본 휴식을 채웁니다.

예약 시 통과와 실행 시 성공은 같지 않습니다. 그 사이 자원, 도시 소유, 외교, 직책이 바뀔 수 있으므로 full constraint는 실행 순간 다시 평가됩니다.

월 변경 경계

InMemoryTurnWorld.advanceMonth()는 다음 순서를 보존합니다.

  1. 다음 연·월을 계산합니다.
  2. beforeMonthChanged handler를 등록 순서대로 실행합니다.
  3. world의 현재 연·월을 바꿉니다.
  4. onMonthChanged handler를 등록 순서대로 실행합니다.
  5. 연도가 바뀌었으면 onYearChanged를 실행합니다.

turnDaemon.tscomposeCalendarHandlers() 순서에는 월간 event, 수입, 연감, PRE_MONTH 상태 정리, 국가 명령, 국가 통계, 외교, 전쟁 설정, 방랑, 국가 수, 통일, 토너먼트, 경매, 전선 상태가 포함됩니다. 이 순서는 ref의 관찰 가능한 결과와 RNG·persistence에 영향을 주므로 리팩터링 시 단순 정렬하지 않습니다.

RNG 경계

게임 결과에 영향을 주는 난수는 LiteHashDRBGRandUtil을 사용합니다. seed에는 hidden seed와 action/month/general 같은 context가 직렬화됩니다. main RNG의 소비 순서를 유지해야 하는 로직과 독립된 재현 가능 substream을 써야 하는 fallback을 구분합니다. authoritative path에 Math.random()을 넣지 않습니다.

장애를 추적할 위치

증상 우선 확인
같은 mutation이 두 번 보임 idempotency key, input_event 상태·attempt
요청은 성공했지만 화면이 늦음 DB commit 결과, Redis/SSE fan-out
daemon이 처리하지 않음 profile gate, pause 상태, lease owner, PENDING claim
재시작 뒤 일부 턴 반복 checkpoint와 general turn ordering
DB와 메모리가 다름 EngineStateManager, databaseHooks, flush 대상 누락
ref와 결과가 다름 constraint 순서, action context, RNG trace, rounding, log/effect 순서