#1416 國別股票識別原子切換 Runbook
本 runbook 將舊的混合 public.stocks/public.stock_ohlc 原子切換為
taiwan.* 與 us.*。DB canonical identity 是 bare stock_code;
listing_type 只保存 tw、two 或 NULL,不參與 PK/FK。HTTP ticker
格式不變。
本文件的 full-volume rehearsal 是 Dev 身分切換的驗收門檻。 v0.24.0 將已合入 dev 的程式與 migration 升至既有 SIT,切版備份、失敗停機與 配對回復規則另見
v0.24.0-sit-release.zh-TW.md。 SIT 核心 18 表還原通過不等於全庫索引還原通過,不可據此宣稱 full-volume rehearsal PASS。 下方 Dev-only rehearsal source 規則仍是原 Dev 身分切換的驗收門檻。本次 SIT 則由 正常deploy-sit.yml在 dev → SIT merge 後自動部署:只有 migration 134 的 statement timeout 固定使用0,同批及其他部署的其餘 migration 檔各使用600s。 workflow 不設定 job timeout override,沿用 Actions 既有 6 小時預設上限,也沒有 capacity helper/threshold gate。本次首次 134 須在觸發部署前完成一次 fresh backup, 但它不是 workflow gate 或 PR merge 條件;人工 SIT rehearsal 仍只是可選維運證據。 目前額外 rehearsal 的 full restore 仍為 INCOMPLETE,不可稱為 PASS。
不可逾越的前提
- 只在已核准的 Dev maintenance window 執行;SIT 不作 rehearsal source。
- migration 與新 image 不得分開部署。舊 image 不能連新 schema,新 image 不能連舊 schema。
- API、scheduler、worker、promotion、手動 backfill 等所有 DB writer 必須停機, 並由 DB activity read-back 證明只剩 migration actor。
- 不使用 feature flag、dual write、legacy compatibility column 或
search_path切換。 - migration 前必須有可還原的 consistent logical dump;rollback 是 restore dump, 不是只回退 image。
- 7 組 dual aliases、5236 重疊 OHLC,以及任何 user-owned key collision 都必須 先產出 exact manifest 並取得資料處理授權。migration 的 preflight 非零時必須停止, 不得繞過。
- 結構化 JSON 內若保存的是裸台股代號,migration 必須用該 summary/cache row 的
trade_date、trading_date或period_end_date查找同日 OHLC;同日缺列時,只有 前後最近 OHLC 的listing_type一致才可保存該 date-effective 值。不得以 current master、單側資料或跨板前後值推測;無日期或證據不足時 fail closed。
Full-volume rehearsal hard gate
Rehearsal 使用 Dev consistent dump,在 Dev Mac 的專用 external SSD 上建立 throwaway PostgreSQL。開始前讀回 dump、DB、affected tables 與 filesystem 容量:
required_free = measured_dump_bytes
+ restored_db_bytes
+ affected_shadow_copy_bytes
+ 2 * affected_shadow_copy_bytes
+ 10% filesystem headroom
若未達門檻,禁止以 subset 取代 full-volume rehearsal。完整 rehearsal 必須記錄:
- dump SHA-256、bytes、duration;
- restore 與 migration duration;
- peak disk、WAL、temporary bytes;
- country row counts、deterministic hashes、FK/orphan/duplicate 結果;
- verifier JSON;
- 新 image API/provider/write/idempotency smoke;
- 銷毀 clone 後再次 restore 原 dump,並通過相同 row/hash/FK/API smoke。
任一項 FAIL、BLOCKED 或 UNVERIFIED 都不得排正式 maintenance window。
Maintenance 執行順序
- 依 rehearsal 相同方式建立 cutover snapshot dump,立即記錄 SHA-256、bytes, 並在 throwaway PostgreSQL 完成一次 restore read-back。
- 停止全部 runtime writer 與 API 流量;以
pg_stat_activity、scheduler/task state 與 container/process read-back 證明沒有殘留 writer。 - 以 read-only transaction 重跑本 PR 的 versioned manifest generator:
cd investment-agent/backend
python -m database.listing_schema_manifest --database-url "$CUTOVER_DATABASE_URL"
接著重跑 PR evidence 中的 catalog、collision、orphan、JSON 與 capacity 查詢;
輸出必須和已核准的 exact manifest 一致。.codex/ 是本機工作目錄且不會隨
image 發布,maintenance 不得依賴其中檔案。
4. 依核准的 exact collision manifest 處理資料。若 target、row count 或 hash 漂移,
立即停止並重新核准,不可推測。
5. 以單一 migration actor 執行
investment-agent/backend/database/migrations/134_country_stock_identity.sql。
migration 的 advisory lock、preflight 或任一 DDL/DML 失敗都視為整體失敗。
migration 會在任何 legacy source table/identifier column 被刪除前,按每個
country destination 計算 source-derived canonical row count 與 deterministic
content hash;所有 DDL/DML 完成後、COMMIT 前再對 final destination 計算同一
fingerprint。任一 count/hash 不同會在 transaction 內 fail closed,不得手動跳過。
structured JSON 亦以 deep-normalized canonical value 納入;global/group/stock catalog
只要原列有 identifier,就必須進 country child,不能用 scope 推測有無 identity。
6. 執行 verifier:
cd investment-agent/backend
python -m database.listing_schema_verifier --database-url "$CUTOVER_DATABASE_URL"
唯一通過輸出是 []。同時讀回 country row/hash、FK/orphan、PK/unique/index、
public.stocks/public.stock_ohlc 不存在,以及 application schemas 的 legacy
identifier/value 為零。
另抽查至少一筆 legacy structured JSON 裸代號,確認 DB 已保存
stock_code + date-effective listing_type,API/LLM read boundary 仍輸出既有 ticker
shape。
7. 只部署包含 migration 134 對應 runtime 的新 image。
8. 執行 OpenAPI diff 與代表 API smoke:stocks、OHLC、indicators、signals、SES、
watchlist、transactions;Taiwan、US、^TWII、5236 都必須覆蓋。
9. 執行 provider dry-run 與 first-write/exact-retry/idempotency smoke,確認 FinMind、
Yahoo、TWSE、TPEx boundary 仍投影既有 external ticker,但 DB 只寫 bare identity。
10. 所有驗證通過後才恢復 API 流量與 writers。
Rollback
- migration 尚未 commit:保持 runtime 停止,讓 transaction rollback,驗證舊 schema、 row count 與 hash 後才可重新嘗試。
- migration 已 commit 或新 image smoke 失敗:保持 runtime 停止,移除失敗 DB, restore 已驗證的 pre-cutover dump,再部署舊 image。
- restore 後重跑 row/hash/FK/API smoke;全部通過才恢復服務。
- 禁止只回退 application image並讓它連新 DB,也禁止手工拼回 suffixed ticker。
Cutover 後資料重建順序
- 在
taiwan/us新 schema 執行 OHLC rebuild;FinMind 優先,Yahoo fallback。 - 驗收 raw、adjusted、corporate-action reconstruction 與 promotion。任何 blocking error 都先停止,修正 PR merge/deploy 後才 resume。
- 以完整 code-level history 重算 Indicator V2。
- 重生 signal、event、story、latest SES、SESI 與 Strategy dependencies。
- 同時做正向與反向證據驗算:Indicator → signal → event → story,且 story 必須 能反查 event、signal 與 indicator evidence。
本 PR 不更動 Indicator 公式,也不重新接回 SZO;它只解除 OHLC、Indicator 與 SESI 重建的 identity/schema 阻塞。