docs: consolidate current architecture documentation
This commit is contained in:
@@ -1,99 +1,95 @@
|
||||
# 요청·턴·저장 흐름
|
||||
|
||||
## 조회 요청
|
||||
|
||||
일반 query는 다음 경로를 따릅니다.
|
||||
## 조회
|
||||
|
||||
```text
|
||||
Vue view/store
|
||||
-> tRPC client
|
||||
-> game-api router
|
||||
-> session actor + 입력 validation
|
||||
-> Prisma query
|
||||
-> 권한에 맞춘 DTO/redaction
|
||||
-> 화면 상태
|
||||
browser -> tRPC -> procedure auth -> Prisma/Redis read -> DTO/redaction -> browser
|
||||
```
|
||||
|
||||
조회는 engine의 in-memory object를 직접 공유하지 않습니다. 따라서 daemon이 transaction을 commit하기 전의
|
||||
중간 계산은 API query에 노출되지 않습니다.
|
||||
Public, own, same-nation, foreign, NPC와 role별 응답은 router가 server-side
|
||||
actor와 resource relation으로 결정합니다. Prisma row를 그대로 반환하지 않고
|
||||
endpoint DTO에서 공개 field를 선택합니다.
|
||||
|
||||
## 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
|
||||
## API transaction mutation
|
||||
|
||||
```text
|
||||
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 신호
|
||||
request
|
||||
-> requestId와 input 검증
|
||||
-> session actor·권한 검사
|
||||
-> InputEvent(target=API, PROCESSING)
|
||||
-> Prisma transaction
|
||||
-> domain row mutation
|
||||
-> InputEvent(SUCCEEDED)
|
||||
-> commit
|
||||
-> notification
|
||||
```
|
||||
|
||||
계산이나 DB 쓰기가 실패하면 `EngineStateManager`가 등록된 in-memory participant를 savepoint로 되돌립니다.
|
||||
DB transaction도 commit되지 않아 메모리와 DB의 부분 진행을 피합니다. lease를 잃은 worker는 stale 결과를
|
||||
commit할 수 없어야 합니다.
|
||||
`app/game-api/src/inputEventBoundary.ts`의 `executeInputEvent()`가 이 경계를
|
||||
제공합니다. 중복 request ID는 완료·처리 중 event를 다시 실행하지 않으며,
|
||||
실패 event는 claim 조건을 만족할 때 attempts를 증가시킵니다.
|
||||
|
||||
## 한 tick의 처리
|
||||
## Daemon mutation
|
||||
|
||||
`TurnDaemonLifecycle`은 clock과 schedule에서 다음 실행 시점을 구합니다. 턴을 시작하면
|
||||
`InMemoryTurnProcessor`와 `InMemoryTurnWorld`가 `turnTime`, 그다음 `general.id` 순서로 실행 대상을
|
||||
결정합니다. checkpoint는 재시작 시 이미 처리한 동일 시점의 장수를 건너뛰는 기준입니다.
|
||||
```text
|
||||
request
|
||||
-> actor·input 검증
|
||||
-> InputEvent(target=DAEMON)
|
||||
-> daemon transport
|
||||
-> lease owner claim
|
||||
-> in-memory world mutation
|
||||
-> EngineStateManager transaction flush
|
||||
-> event 결과와 checkpoint commit
|
||||
-> SSE/realtime
|
||||
```
|
||||
|
||||
장수 한 명의 예약 명령은 대략 다음 순서입니다.
|
||||
명령은 API 수락 시점과 daemon 실행 시점에 필요한 조건을 각각 검사합니다.
|
||||
예약 뒤 world가 바뀔 수 있으므로 실행 constraint를 생략하지 않습니다.
|
||||
|
||||
1. `InMemoryReservedTurnStore`에서 첫 예약 명령을 읽습니다.
|
||||
2. 명령 key를 `GeneralTurnCommandLoader` 또는 `NationTurnCommandLoader`로 불러옵니다.
|
||||
3. `actionContextBuilder`가 대상 도시·국가·장수, map, unit set, 외교, 시간과 RNG를 구성합니다.
|
||||
4. permission/min/full constraint를 목적에 맞게 평가합니다.
|
||||
5. 선행 턴이 있으면 진행 상태를 쌓고, 완성된 시점에 `resolve()`를 실행합니다.
|
||||
6. effect와 직접 변경을 world에 반영하고 로그·메시지·후속 턴 시간을 기록합니다.
|
||||
7. 실행된 queue를 당기고 끝에 기본 `휴식`을 채웁니다.
|
||||
## Tick
|
||||
|
||||
예약 시 통과와 실행 시 성공은 같지 않습니다. 그 사이 자원, 도시 소유, 외교, 직책이 바뀔 수 있으므로 full
|
||||
constraint는 실행 순간 다시 평가됩니다.
|
||||
`TurnDaemonLifecycle`은 다음 장수 turn time과 tick 경계 중 빠른 시각을
|
||||
선택합니다. 한 run은 budget 안에서 due command를 처리하고 calendar 경계를
|
||||
진행합니다.
|
||||
|
||||
## 월 변경 경계
|
||||
1. lease와 fencing token을 확인합니다.
|
||||
2. 예약 턴을 revision/lease로 claim합니다.
|
||||
3. command args와 실행 constraint를 평가합니다.
|
||||
4. action module과 command handler가 state patch, log, message를 만듭니다.
|
||||
5. world에 patch를 적용하고 dirty entity를 기록합니다.
|
||||
6. 월 경계를 지났으면 scenario event action을 정해진 순서로 실행합니다.
|
||||
7. transaction에서 dirty state, turn queue, log, event와 checkpoint를 flush합니다.
|
||||
|
||||
`InMemoryTurnWorld.advanceMonth()`는 다음 순서를 보존합니다.
|
||||
Transaction 실패 시 `EngineStateManager`가 in-memory snapshot을 복원합니다.
|
||||
Lease를 잃은 process는 fencing 검사에서 commit하지 못합니다.
|
||||
|
||||
1. 다음 연·월을 계산합니다.
|
||||
2. `beforeMonthChanged` handler를 등록 순서대로 실행합니다.
|
||||
3. world의 현재 연·월을 바꿉니다.
|
||||
4. `onMonthChanged` handler를 등록 순서대로 실행합니다.
|
||||
5. 연도가 바뀌었으면 `onYearChanged`를 실행합니다.
|
||||
## 저장 위치
|
||||
|
||||
`turnDaemon.ts`의 `composeCalendarHandlers()` 순서에는 월간 event, 수입, 연감, PRE_MONTH 상태 정리,
|
||||
국가 명령, 국가 통계, 외교, 전쟁 설정, 방랑, 국가 수, 통일, 토너먼트, 경매, 전선 상태가 포함됩니다.
|
||||
이 순서는 ref의 관찰 가능한 결과와 RNG·persistence에 영향을 주므로 리팩터링 시 단순 정렬하지 않습니다.
|
||||
| 상태 | 기준 |
|
||||
| ----------------------------------- | ------------------------------------ |
|
||||
| world, 장수, 국가, 도시, 외교, 부대 | game Prisma model |
|
||||
| 예약 명령과 revision | `GeneralTurn*`, `NationTurn*` |
|
||||
| 내구성 입력 | `InputEvent` |
|
||||
| daemon 소유권 | `TurnDaemonLease` |
|
||||
| checkpoint와 calendar meta | `WorldState` |
|
||||
| 사용자 출력 | `LogEntry`, message·board 관련 model |
|
||||
| fan-out | Redis/SSE |
|
||||
|
||||
## RNG 경계
|
||||
Redis notification 실패는 이미 commit된 PostgreSQL mutation을 되돌리지
|
||||
않습니다. 재연결 client는 DB 조회로 상태를 복구합니다.
|
||||
|
||||
게임 결과에 영향을 주는 난수는 `LiteHashDRBG`와 `RandUtil`을 사용합니다. seed에는 hidden seed와
|
||||
action/month/general 같은 context가 직렬화됩니다. main RNG의 소비 순서를 유지해야 하는 로직과 독립된
|
||||
재현 가능 substream을 써야 하는 fallback을 구분합니다. authoritative path에 `Math.random()`을 넣지
|
||||
않습니다.
|
||||
## RNG
|
||||
|
||||
## 장애를 추적할 위치
|
||||
RNG instance는 command와 월간 handler context로 전달합니다. 판정 순서,
|
||||
후보 정렬과 소비 호출 수를 변경하지 않습니다. Main stream과 관계없는
|
||||
fallback 무작위성은 seed가 있는 별도 substream을 사용합니다.
|
||||
|
||||
| 증상 | 우선 확인 |
|
||||
| ----------------------------- | --------------------------------------------------------------------- |
|
||||
| 같은 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 순서 |
|
||||
## 추적 지점
|
||||
|
||||
- request acceptance: `app/game-api/src/inputEventBoundary.ts`
|
||||
- daemon transport: `app/game-api/src/daemon/`
|
||||
- lifecycle: `app/game-engine/src/lifecycle/turnDaemonLifecycle.ts`
|
||||
- runtime composition: `app/game-engine/src/turn/turnDaemon.ts`
|
||||
- world: `inMemoryWorld.ts`, `worldLoader.ts`
|
||||
- transaction: `engineStateManager.ts`, `databaseHooks.ts`
|
||||
- queue: `reservedTurnStore.ts`, `reservedTurnHandler.ts`
|
||||
- schema: `packages/infra/prisma/game.prisma`
|
||||
|
||||
Reference in New Issue
Block a user