fix: 특수 유저 커맨드의 Ref 호환 경계를 보강한다
수뇌 국가 설정과 NPC 정책을 actor-bound ENGINE mutation으로 옮기고, 추방·등용·점령·멸망·아이템 폐기의 특수 분기를 Ref와 맞춘다. 요청 ID를 사용자·프로필별로 격리하고 토너먼트 손상 projection을 fail-closed하며 실제 DB 및 Ref 차등 회귀를 보강한다.
This commit is contained in:
@@ -23,7 +23,7 @@ transaction을 만든다. `engineAuthedProcedure`, `accessEngineAuthedProcedure`
|
||||
- **ENGINE 소유 + 불필요한 API outer**: gameplay durable mutation은 ENGINE이 전부
|
||||
소유하지만 route가 아직 API `input_event`/journal transaction에 감싸여 있다.
|
||||
|
||||
현재 합계는 **ENGINE 전환 26 + 혼합 13 + 기존 ENGINE 9 + 불필요한 API
|
||||
현재 합계는 **ENGINE 전환 36 + 혼합 3 + 기존 ENGINE 9 + 불필요한 API
|
||||
outer 1 = 49**다.
|
||||
|
||||
ENGINE 전환 route의 explicit `requestId`는 각 route가 기존에 사용하던 HTTP 요청
|
||||
@@ -45,19 +45,19 @@ selection-pool create/reselect는 client request ID가 있을 때
|
||||
| `inherit.openUniqueAuction` | `engineAuthedProcedure`; world/general/minimum bid 조기 validation (`app/game-api/src/router/inherit/index.ts:389-428`) | 공통 `auctionOpen` ENGINE handler가 mutation을 소유하고 API는 Redis timer index만 갱신 (`app/game-api/src/auction/open.ts:22-51`) |
|
||||
| `join.getSelectionPool` | `engineAuthedProcedure`; actor와 accepted game time만 전달 (`app/game-api/src/router/join/index.ts:390-406`) | `selectPoolReserve` ENGINE handler가 world/DB 상태에서 예약을 재검증하고 저장 (`app/game-engine/src/turn/worldCommandHandler.ts:401-437`) |
|
||||
| `inherit.buyHiddenBuff`, `inherit.setNextSpecialWar`, `inherit.resetSpecialWar`, `inherit.resetTurnTime`, `inherit.resetStat`, `inherit.buyRandomUnique`, `inherit.checkOwner` | 모두 `engineAuthedProcedure`; 인증 user ID와 action 입력만 전달 (`app/game-api/src/router/inherit/index.ts:107-124`, `:301-388`, `:429-445`) | `inheritanceAction` ENGINE handler가 general/user 소유권, 통일 상태, 잔액, 대상, RNG, general patch, inheritance point/log/message를 한 transaction에서 처리 (`app/game-engine/src/turn/worldCommandHandler.ts:807-818`, `app/game-engine/src/turn/inheritanceActionService.ts:313-669`) |
|
||||
| `nation.setNotice`, `nation.setScoutMsg`, `nation.setSecretLimit`, `nation.setRate`, `nation.setBlockWar`, `nation.setBill`, `nation.setBlockScout` | `engineAuthedProcedure`; API는 인증 actor, 현재 국가와 조기 권한만 읽고 semantic mutation을 전달 (`app/game-api/src/router/nation/endpoints/setNotice.ts`, `setScoutMsg.ts`, `setSecretLimit.ts`, `setRate.ts`, `setBlockWar.ts`, `setBill.ts`, `setBlockScout.ts`) | `setNationSetting`이 실행 시점 owner/nation/직책·permission을 다시 검사한다. 전쟁 설정 잔여 횟수 차감과 임관 잠금 검사를 현재 ENGINE state에서 수행하고, 공지는 logical game time과 author snapshot을 국가 meta와 같은 transaction에 저장한다 (`app/game-engine/src/turn/nationSettingMutation.ts`, `worldCommandHandler.ts`). |
|
||||
| `npc.setNationPolicy`, `npc.setNationPriority`, `npc.setGeneralPriority` | `accessEngineAuthedInputProcedure`; API는 현재 화면값과 unit-set 기반 입력 보조만 수행하고 actor-bound semantic delta와 명시적 nullable revision을 전달 (`app/game-api/src/router/npc/index.ts`) | `setNpcPolicy`가 실행 시점 owner/nation/permission, strict CAS, 현재 troop/city membership, priority와 numeric policy를 검증하고 logical setter snapshot과 delta만 반영한다 (`app/game-engine/src/turn/npcPolicyMutation.ts`, `worldCommandHandler.ts`). |
|
||||
|
||||
합계 **26개 route**다.
|
||||
합계 **36개 route**다.
|
||||
|
||||
## 혼합 또는 validation 이관이 먼저 필요한 route
|
||||
|
||||
| route | 현재 procedure / outer transaction | 보류 근거 |
|
||||
| --- | --- | --- |
|
||||
| `messages.respond` | `authedProcedure`, action별 분기 (`app/game-api/src/router/messages/index.ts:318-372`) | `scout`/`raiseInvader`는 `messageRespond` ENGINE command가 처리하지만 `noAggression`/`cancelNA`/`stopWar`는 API transaction의 `respondToDiplomaticMessage` 경로가 처리한다. route 전체를 ENGINE-owned로 보지 않는다. |
|
||||
| `nation.setNotice`, `nation.setScoutMsg`, `nation.setSecretLimit`, `nation.setRate`, `nation.setBlockWar`, `nation.setBill`, `nation.setBlockScout` | `authedProcedure`, outer 있음 (`app/game-api/src/router/nation/endpoints/setNotice.ts:11`, `setScoutMsg.ts:11`, `setSecretLimit.ts:10`, `setRate.ts:10`, `setBlockWar.ts:10`, `setBill.ts:10`, `setBlockScout.ts:10`) | API가 actor 권한과 nation meta를 읽어 full metadata patch를 합성한다. `setNationMeta`는 `_updatedAt` CAS만 검사하므로 actor/permission과 합성 의미를 ENGINE으로 옮겨야 한다 (`app/game-api/src/router/nation/shared.ts:431-458`, `app/game-engine/src/turn/worldCommandHandler.ts:499-542`). |
|
||||
| `npc.setNationPolicy`, `npc.setNationPriority`, `npc.setGeneralPriority` | `accessAuthedInputProcedure`, outer 있음 (`app/game-api/src/router/npc/index.ts:545-812`) | API가 nation/general/world를 읽어 권한, unit-set 기반 기본값과 full policy object를 합성한 뒤 `setNationMeta` CAS를 사용한다. ENGINE은 권한/합성 의미를 소유하지 않는다. |
|
||||
| `tournament.join`, `tournament.placeBet` | `authedProcedure`, outer 있음 (`app/game-api/src/router/tournament/index.ts:393-458`, `:515-620`) | PostgreSQL ENGINE resource/meta 명령과 Redis-owned participants/bets를 결합하고 실패 시 보상 ENGINE 명령을 보낸다. 하나의 DB transaction이 아니며 durable saga/Redis atomic revision이 필요하다. |
|
||||
|
||||
합계 **13개 route**다.
|
||||
합계 **3개 route**다.
|
||||
|
||||
## 기존에 API outer transaction이 없던 ENGINE route
|
||||
|
||||
@@ -84,6 +84,12 @@ selection-pool create/reselect는 client request ID가 있을 때
|
||||
`$transaction`을 호출하지 않고 기존 형식의 stable ENGINE request ID를 전달한다.
|
||||
- `app/game-api/test/nationPersonnelRouter.test.ts`: nation personnel route의 동일 계약을
|
||||
검증한다.
|
||||
- `app/game-api/test/nationSettingRouter.test.ts`, `nationHtmlRouter.test.ts`,
|
||||
`scoutBlockRouter.test.ts`: 일곱 국가 설정이 API outer transaction 없이 인증 actor와
|
||||
semantic intent를 전달하고 ENGINE 거절을 그대로 매핑하는지 검증한다.
|
||||
- `app/game-api/test/npcPolicyRouter.test.ts`,
|
||||
`app/game-engine/test/npcPolicyLifecycle.test.ts`: NPC policy의 명시적 nullable CAS,
|
||||
실행 시점 권한과 delta merge/replay 경계를 검증한다.
|
||||
- `app/game-api/test/troopRouter.test.ts`: troop mutation의 동일 계약을 검증한다.
|
||||
- `app/game-api/test/auctionRouter.test.ts`: auction mutation이 API transaction 없이
|
||||
daemon command와 Redis timer projection을 완료하는 계약을 검증한다.
|
||||
@@ -94,7 +100,8 @@ selection-pool create/reselect는 client request ID가 있을 때
|
||||
validation과 ENGINE의 vote insert/reward/idempotency 경계를 검증한다. API outer/journal
|
||||
제거는 아직 검증 대상이 아니다.
|
||||
- raw inventory 재검색: `rg -n "requestCommand\\(" app/game-api/src/router`와
|
||||
`openAuctionWithDaemon`, `requestInheritanceAction`, `updateNationMeta`,
|
||||
`openAuctionWithDaemon`, `requestInheritanceAction`, `updateNationSetting`,
|
||||
`requestNpcPolicyMutation`,
|
||||
`adjustAccountIconForUser`, `requestImmediateAction`, `requestJoinCreateCommand`,
|
||||
`requestNpcPossessionCommand` caller 검색을 함께 실행해 helper 경유 route를 놓치지
|
||||
않는다.
|
||||
|
||||
@@ -33,10 +33,10 @@ writer reconciliation을 포함한다. rolling deployment가 끝난 뒤에만
|
||||
|
||||
| 분류 | 수 | route |
|
||||
| --- | ---: | --- |
|
||||
| durable journal | 23 | `betting.bet`; `inherit.checkOwner`; `messages.delete`, `messages.respond`, `messages.send`; `nation.setBill`, `nation.setBlockScout`, `nation.setBlockWar`, `nation.setNotice`, `nation.setRate`, `nation.setScoutMsg`, `nation.setSecretLimit`; `npc.setGeneralPriority`, `npc.setNationPolicy`, `npc.setNationPriority`; `turns.repeatGeneral`, `turns.setGeneral`, `turns.setGeneralBulk`, `turns.shiftGeneral`; `vote.closePoll`, `vote.createPoll`, `vote.submitVote`, `vote.updatePoll` |
|
||||
| durable journal | 13 | `betting.bet`; `inherit.checkOwner`; `messages.delete`, `messages.respond`, `messages.send`; `turns.repeatGeneral`, `turns.setGeneral`, `turns.setGeneralBulk`, `turns.shiftGeneral`; `vote.closePoll`, `vote.createPoll`, `vote.submitVote`, `vote.updatePoll` |
|
||||
| separate access journal | 1 | `public.recordAccess` |
|
||||
| explicit no realtime consumer | 14 | `board.writeArticle`, `board.writeComment`; `diplomacy.destroyLetter`, `diplomacy.respondLetter`, `diplomacy.rollbackLetter`, `diplomacy.sendLetter`; `join.getSelectionPool`, `join.listPossessCandidates`; `messages.readLatest`; `turns.repeatNation`, `turns.setNation`, `turns.setNationBulk`, `turns.shiftNation`; `vote.addComment` |
|
||||
| engine owned | 27 | `auction.bidBuyRice`, `auction.bidSellRice`, `auction.bidUnique`, `auction.openBuyRice`, `auction.openSellRice`, `auction.openUnique`; `general.adjustIcon`, `general.buildNationCandidate`, `general.dieOnPrestart`, `general.dropItem`, `general.ensureDieOnPrestartStatus`, `general.instantRetreat`, `general.setMySetting`, `general.vacation`; `inherit.openUniqueAuction`; `join.createGeneral`, `join.possessGeneral`, `join.reselectPoolGeneral`, `join.selectPoolGeneral`; `nation.appoint`, `nation.changePermission`, `nation.kick`; `troop.create`, `troop.exit`, `troop.join`, `troop.kick`, `troop.rename` |
|
||||
| engine owned | 37 | `auction.bidBuyRice`, `auction.bidSellRice`, `auction.bidUnique`, `auction.openBuyRice`, `auction.openSellRice`, `auction.openUnique`; `general.adjustIcon`, `general.buildNationCandidate`, `general.dieOnPrestart`, `general.dropItem`, `general.ensureDieOnPrestartStatus`, `general.instantRetreat`, `general.setMySetting`, `general.vacation`; `inherit.openUniqueAuction`; `join.createGeneral`, `join.possessGeneral`, `join.reselectPoolGeneral`, `join.selectPoolGeneral`; `nation.appoint`, `nation.changePermission`, `nation.kick`, `nation.setBill`, `nation.setBlockScout`, `nation.setBlockWar`, `nation.setNotice`, `nation.setRate`, `nation.setScoutMsg`, `nation.setSecretLimit`; `npc.setGeneralPriority`, `npc.setNationPolicy`, `npc.setNationPriority`; `troop.create`, `troop.exit`, `troop.join`, `troop.kick`, `troop.rename` |
|
||||
| mixed saga | 9 | `inherit.buyHiddenBuff`, `inherit.buyRandomUnique`, `inherit.resetSpecialWar`, `inherit.resetStat`, `inherit.resetTurnTime`, `inherit.setNextSpecialWar`; `tournament.cancel`, `tournament.join`, `tournament.placeBet` |
|
||||
| Redis projection | 6 | `tournament.patchState`, `tournament.seedParticipants`, `tournament.setBettingEntries`, `tournament.setMatches`, `tournament.setParticipants`, `tournament.setState` |
|
||||
| operational | 3 | `turnDaemon.pause`, `turnDaemon.resume`, `turnDaemon.run` |
|
||||
@@ -55,7 +55,6 @@ writer reconciliation을 포함한다. rolling deployment가 끝난 뒤에만
|
||||
| `messages.send` | 생성된 수신/송신 복사본의 `messages.mailbox:<mailbox>` | 해당 mailbox viewer에게 ID 없는 `messagesInvalidated` | 기존 pre-commit Redis `messageCreated`를 제거했다. outbox publish 뒤에도 browser에는 mailbox/message/sender/time/revision이 노출되지 않는다. |
|
||||
| `messages.delete` | 실제로 만료한 송신/수신 mailbox | 동일 | sender copy만 지우는 수동 외교 메시지는 그 mailbox만 표시한다. |
|
||||
| `messages.respond` | 영향 mailbox, `records.general`, 실제 외교 변경 국가의 `nation.content`, front-state patch 도시의 `city.content`, 필요 시 `map.world`, transitive aggregate용 `dashboard.global` | mailbox boolean 및 해당 dashboard slice | 실패 로그도 commit되면 actor 개인 기록을 표시한다. 외교 수락이 실제 diplomacy/city/nation dependency를 바꿀 때만 broad source key를 표시한다. |
|
||||
| nation metadata 7개 및 NPC policy 3개 | `nation.content:<nation>`, `dashboard.global:0`; notice만 추가로 `front.nation:<nation>` | 해당 국가 context/command/board, notice front status | `updateNationMeta()` 성공 뒤 API 표식을 사용하고, ENGINE 소유 transaction도 nation 변화와 `dashboard.global`을 함께 기록한다. 두 input-event의 업무 원자성은 기존 saga지만 source coverage는 ENGINE commit으로 보존된다. |
|
||||
| general reserved turn 4개 | `reserved.general:<general>`, `dashboard.global:0` | 본인 reserved-turn slice | queue row와 CAS revision을 쓴 같은 API transaction에서 표시한다. global key는 troop leader 첫 예약턴에 의존하는 다른 장수 context를 위한 source-only 표식이다. nation reserved turns는 main SSE consumer가 없어 명시적 no-op이다. |
|
||||
| vote 4개 | 기존 `front.general`/`front.global` | front-status boolean | vote producer 작업에서 pre-commit publish를 journal로 이미 치환했다. 댓글은 active survey 제목을 바꾸지 않아 별도 화면 no-op이다. |
|
||||
| `public.recordAccess` | `access.general:<general>` | 없음 | Ref 순서상 gameplay transaction 밖의 별도 access transaction에 저장한다. |
|
||||
@@ -81,9 +80,9 @@ public dashboard event로 내보내지 않는다. browser wake-up은 정밀 enti
|
||||
|
||||
## 남은 업무 원자성 gap과 coverage 판정
|
||||
|
||||
1. nation metadata/NPC policy의 API input-event와 ENGINE input-event는 여전히 하나의
|
||||
업무 transaction이 아니다. 다만 실제 state 소유 ENGINE commit이 precise entity와
|
||||
`dashboard.global`을 기록하므로 revision-first source coverage는 빠지지 않는다.
|
||||
1. 국가 설정 7개와 NPC policy 3개는 API outer input-event를 제거했다. semantic ENGINE
|
||||
command가 actor 권한, 현재 state와 mutation을 검증하고 국가 저장·ENGINE input-event·
|
||||
`nation.content`/`front.nation`/`dashboard.global` journal을 한 transaction에서 commit한다.
|
||||
2. inheritance/tournament command 일부는 보상 가능한 saga다. tournament payload와
|
||||
profile source revision 자체는 API store, 월 자동 개막, runtime clock shift 모두 공통
|
||||
Lua writer 한 번으로 원자화했다.
|
||||
|
||||
Reference in New Issue
Block a user