跳轉到

#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 執行順序

  1. 依 rehearsal 相同方式建立 cutover snapshot dump,立即記錄 SHA-256、bytes, 並在 throwaway PostgreSQL 完成一次 restore read-back。
  2. 停止全部 runtime writer 與 API 流量;以 pg_stat_activity、scheduler/task state 與 container/process read-back 證明沒有殘留 writer。
  3. 以 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 後資料重建順序

  1. 在 taiwan/us 新 schema 執行 OHLC rebuild;FinMind 優先,Yahoo fallback。
  2. 驗收 raw、adjusted、corporate-action reconstruction 與 promotion。任何 blocking error 都先停止,修正 PR merge/deploy 後才 resume。
  3. 以完整 code-level history 重算 Indicator V2。
  4. 重生 signal、event、story、latest SES、SESI 與 Strategy dependencies。
  5. 同時做正向與反向證據驗算:Indicator → signal → event → story,且 story 必須 能反查 event、signal 與 indicator evidence。

本 PR 不更動 Indicator 公式,也不重新接回 SZO;它只解除 OHLC、Indicator 與 SESI 重建的 identity/schema 阻塞。