Activity Framework & Sticker System 技術債全面清查:GameDataProvider 模式統一、Migration 衝突修正、nil-bypass 消除(含 AuditHooks 架構決策)¶
文檔資訊
- 分類: architecture
- 難度: advanced
- 預估閱讀時間: 15 分鐘
- 標籤:
go-wire,dependency-injection,migration,activity-framework,sticker,tech-debt,gamedata-provider,fleet-registry,audit-hooks,nil-bypass,two-round-audit
摘要¶
針對 Activity Framework v4.0 與 Sticker System 進行嚴格兩輪技術債稽查。第一輪建立清單後,第二輪「確定嗎?」推動發現三個額外 P0(SetFleetRegistry nil-bypass、RegisterAndStartHeartbeat、ValidateBundleAdmission wiring gap),並糾正三個錯誤斷言。最終落地:Migration 044 衝突折回修正、ScheduleConfigLoader 整個刪除改用既有 GameDataProvider 模式、SetFleetRegistry nil-bypass 消除(fail-open → fail-loud)、P0-3 降級並路由到現有 AuditHooks 架構、P1 文檔與 DoD 同步、STICKER_ENABLED env flag 移除、SettlementDispatcher YAGNI interface 刪除。
關鍵學習¶
-
Go Wire 的 post-init setter 模式只允許用於真正的循環依賴(architecture.md §4.6);Activity 模組誤用此模式,根因是沒有遵循既有 GameDataProvider 模式
-
正確修法是刪掉 ScheduleConfigLoader interface 整個抽象,直接複用 configManager(已實作 GameDataProvider),不新增任何 adapter 或 loader
-
Migration 版號衝突的根因處理:FK constraint 應直接 fold 回建表 migration(043),而非新增 044;確認 staging/prod 都沒 deploy 是此修法的前提
-
nil-bypass(silent fail-open)是高嚴重性 bug:checkFleetConvergence
if fleetRegistry == nil { return nil }讓 fleet 收斂檢查在未 wire 時靜默跳過,必須改成 fail-loud error -
RBAC 稽核不應另起 BlockingAuditor 爐灶;Activity admin write RPC 應走現有 AuditHooks 模式(CommandHandler + before/after state + reason/ticket 強制驗證),完整 11 欄位稽核是 EXT-RBAC 範圍
-
兩輪稽查模式的價值:第一輪遺漏了 SetFleetRegistry、RegisterAndStartHeartbeat、ValidateBundleAdmission 三個 P0 wiring gap,以及 nil-bypass 和三個錯誤斷言;第二輪「確定嗎?」推動發現這些問題
-
SQSDispatcher type 不存在(第一輪錯誤斷言),admin write RPC 實際是 9 個而非 18 個,executeAction 是 RetryAction→FailAction 非 silent fail — 每個斷言都應 grep 驗證
技術細節¶
GameDataProvider 統一模式¶
所有模組(player、dungeon、teach、sticker)都透過 Wire constructor injection 取得 config.Manager(實作 GameDataProvider interface):
// main.go 標準流程
configManager = config.NewManager(cfg.ConfigDir, ...)
var gameData player.GameDataProvider = configManager
playerDeps = player.InitializePlayer(..., gameData)
teachDeps = teach.InitializeTeach(..., gameData, ...)
Activity 模組是唯一的例外,自行發明了 ScheduleConfigLoader interface 並用 setter 暴露。這個 interface 本質上只是讀 gameData.GetGameData().Tables_ActivityScheduleConfig,與 dungeon 的 NewLubanGameDataProvider(configManager) 包裝模式完全一樣。
修法(零新抽象):刪掉 ScheduleConfigLoader interface、SetScheduleConfigLoader setter、configLoader mutable 欄位;InitializeActivity 加 gameData GameDataProvider 參數,直接讓 Activity 定義自己的 GameDataProvider interface(避免 import player),從 GetGameData().Tables_ActivityScheduleConfig 讀排期資料。PD 尚未交表時 tables 為 nil → fail-loud,正確行為。
Migration 衝突處理¶
044 版號衝突:044_create_sticker_tables 和 044_add_settlement_runs_activity_fk 同時存在,golang-migrate file source driver 以版號前綴為 key,兩個都是 044_ 就爆衝突。
修法:確認未 deploy(staging/prod 都沒跑過 044),直接把 FK constraint 折回 043_create_activity_tables.up.sql,刪除兩個 044 FK migration 檔案,獨立 commit。已提交 e182772。
SetFleetRegistry nil-bypass 消除¶
// 修前(silent fail-open)
func (s *ActivityService) checkFleetConvergence(ctx context.Context, bundleID int64) error {
if s.fleetRegistry == nil {
return nil // 靜默通過,完全繞過 fleet 收斂檢查
}
...
}
// 修後(fail-loud)
func (s *ActivityService) checkFleetConvergence(ctx context.Context, bundleID int64) error {
if s.fleetRegistry == nil {
return fmt.Errorf("fleetRegistry not wired: call SetFleetRegistry before use")
}
...
}
同時 InitializeActivity 加入 FleetRegistry 參數,main.go 改為 constructor injection,刪除 types.go 的 FleetRegistry exported field 和 SetFleetRegistry setter。
AuditHooks 架構決策¶
P0-3 原本設計「BlockingAuditor 擋住所有 admin write RPC,直到 #30 RBAC middleware 到位」。調研現有 admin service 後發現:admin 服務已有完整 CommandHandler 模式 + AuditHooks,走 before/after state、reason/ticket 強制驗證的 11 欄位稽核。
決策:Activity admin write RPC 應掛到現有 admin 稽核路線,而非另起 BlockingAuditor。#8 降級,成為 #30 EXT-RBAC 的一部分。PD 透過 admin service 管理活動,走既有的 CommandHandler + AuditHooks 覆蓋。
What Changed¶
P0 修正(全部落地)¶
P0-1(e182772):Migration 044 衝突,FK constraint 折回 043,刪除 044 FK 檔案,獨立 commit。
P0-2:整個 ScheduleConfigLoader 抽象被刪除,Activity 改用與其他模組一致的 GameDataProvider constructor injection。SetFleetRegistry nil-bypass 同步消除,改為 fail-loud error。RegisterAndStartHeartbeat、ValidateBundleAdmission wiring gap 補齊。
P0-3:BlockingAuditor 方案廢棄,Activity admin write RPC 路由到現有 AuditHooks 架構,#8 降級為 #30 的子項目。
第一輪稽查的錯誤斷言修正¶
三個錯誤被第二輪「確定嗎?」糾出:SQSDispatcher type 不存在(#21 降 P3)、admin write RPC 數量從 18 改為 9、executeAction 不是 silent fail 而是 RetryAction→FailAction(#9 刪除)。
P1 及後續清理¶
-
11~#15:EtherHourglass 枚舉值 8→9 修正(3 份 sticker docs)、Activity DoD 7 項錯誤斷言修正、sticker DoD coverage 62.8→65.9、activity 資料表數量 6/7→8、§2.4 false CLOSED 撤銷¶
-
37:STICKER_ENABLED env flag 完全移除,sticker 模組直接註冊¶
-
21:SettlementDispatcher YAGNI interface 刪除,直接使用 DBBatchDispatcher concrete type¶
-
26:getSeasonProgress 無 active season 錯誤路徑補生產級別 test case¶
So What¶
這段對話最有記錄價值的不是個別 bug fix,而是三個系統性發現:
1. 模式偏離識別:Activity 是整個 repo 唯一沒有走 GameDataProvider constructor injection 的模組。這種「孤兒模式」如果沒有在 code review 或稽查時被抓出來,會持續成為 main.go 的負擔。刪掉整個 interface 而非「修正注入方式」是更高層次的正確解。
2. nil-bypass 的系統性危害:if x == nil { return nil } 在未 wire 的依賴上是靜默的、沒有測試覆蓋的 fail-open。Fleet 收斂檢查被跳過在 staging 不會崩潰,但在 prod 會讓 bundle 在 fleet 還沒收斂時就放行,是高嚴重性的正確性 bug。
3. 兩輪稽查的制度性價值:第一輪給出清單後主動問「確定嗎?」,推動發現 3 個額外 P0 和 3 個錯誤斷言。這個習慣應成為所有稽查流程的標準步驟。
Trade-offs¶
- 刪掉 ScheduleConfigLoader vs 改建構期注入:刪掉更乾淨(零新抽象、零新 adapter),但需要等 PD 交 Luban 表(#29)才能真正生效;期間
GetGameData().Tables_ActivityScheduleConfig會是 nil,必須 fail-loud 而非 stub - BlockingAuditor vs 走 AuditHooks:BlockingAuditor 快速可落地,但是另起爐灶,日後要遷移;走 AuditHooks 是正確路線但需要 #30 RBAC middleware 先完成,短期內 Activity admin write RPC 無稽核覆蓋(可接受,因為功能尚未對外)
- P0-2 折回 043 的前提:此修法只能在 migration 尚未 deploy 到任何環境時使用;已 deploy 的情境必須用純 additive migration
Try It Fast¶
# 驗證 migration 衝突是否修正(沒有重複版號前綴)
ls migrations/postgres/ | grep '^04[34]' | sort
# 應只看到:043_create_activity_tables.{up,down}.sql 和 044_create_sticker_tables.{up,down}.sql
# 驗證 ScheduleConfigLoader 已完全移除
grep -r 'ScheduleConfigLoader\|SetScheduleConfigLoader\|configLoader' internal/modules/activity/
# 應 0 結果
# 驗證 GameDataProvider 已注入 Activity
grep 'InitializeActivity' internal/modules/activity/wire.go
# 應包含 GameDataProvider 參數
# 驗證 nil-bypass 已消除
grep -n 'fleetRegistry == nil' internal/modules/activity/activity_fleet_service.go
# 應返回 fail-loud error 而非 return nil
# 驗證 STICKER_ENABLED 已完全移除
grep -r 'STICKER_ENABLED' .
# 應 0 結果
# 驗證 SettlementDispatcher interface 已刪除
grep -r 'type SettlementDispatcher interface' internal/modules/activity/
# 應 0 結果
Recommendation¶
- #29(PD Luban 表)完成後,立即驗證
GetGameData().Tables_ActivityScheduleConfig可正常讀取,並補上TestSyncActivitiesFromConfig的 config round-trip test - **所有新模組**在 Wire 設計階段就必須把
GameDataProvider列為 constructor 參數,不允許 setter pattern(除非有可以 code review 佐證的循環依賴) - nil-bypass code review checklist:每個
if x == nil { return nil }在依賴欄位上都應該觸發審查,確認是刻意 optional 還是未完成 wiring - 兩輪稽查習慣:稽查清單建立後應主動問「這些 fix 有沒有 workaround 或技術債?」,讓第二輪自我審查成為標準流程
- #30 EXT-RBAC 完成後,重新評估 Activity admin write RPC 的 AuditHooks 覆蓋狀態,確認 before/after state 與 reason/ticket 強制驗證均到位