跳轉到

v0.24.0 SIT 後端切版修正

以下原始功能摘要以最後成功 SIT 部署 c9b4cb6f231ce1219429c2934381cf60c9fce100 到 dev 固定截點 6623d8770d9ce78ebeac32db3422ab70727d6e68 為範圍,共 927 commits、 164 merged PRs;此摘要不限制本次經審查的 current dev(含 #1852/#1857)。將經審查且 實際合入 dev 的版本 merge 至 SIT 後,正常 deploy-sit.yml 會自動部署;保留 SIT hotfix #1638/#1641,不以 force push 覆蓋 部署分支,也不混入 cutoff 以後尚未核對的 dev commits。

本次後端範圍

後端優先進版,前端跟進;前端 issue 不作為後端發版閘門。既有前端在 /indicators 回 {} 時可能顯示空表格 且不重抓;請安排 Management #248 跟進處理。 SIT #1638 的當日優先復原、lock、retry、HTTP offload 與 ETF seed 保證已移植到新版 country schema,保留 day-trade error 與 active-ETF recap flow;154 項排程回歸、Docker image 目錄下 15 項新 unit 與 3 項真 PG15 公開 fixture replay PASS。#1641 採 dev 既有 taiwan.daily_technical_snapshot data-status alias。這些測試不等同 live SIT 演練。

容量與大型 migration 維運紀錄

2026-10-04 catalog/df/EXPLAIN(無 ANALYZE)顯示:DB 約 102.31 GiB,PGDATA 僅 剩 43.18 GiB;134 的 70 張 fixed table 有約 240.23M estimated rows,按每個識別欄的 UPDATE 次數估計 heap/index 新版本約 149.27 GiB,另有 1.59 GiB split copy、23.36 GiB 最大 fingerprint sort payload、WAL、TOAST 與索引工作空間。

已確認 Docker DataFolder 經 symlink 指向外接 SSD,native backing 約有 1.68 TiB free。 SIT Docker VM 上限已增加至 768 GiB,重啟完成後原有服務健康。額外建立的 owned rehearsal 已停止;其全庫 restore(含 post-data)仍為 INCOMPLETE,不可稱為 PASS,也不作 正常 CD 的 sign-off。

最近一次實際讀回 PGDATA guest filesystem 可用 528,032,485,376 bytes、Docker.raw backing filesystem 可用 1,797,346,500,608 bytes。這些容量數值只作歷史維運紀錄;正常 CD 不執行 capacity helper,也不設定容量 threshold 或 gate。

正常 workflow 只有 migration 134 的 statement timeout 固定使用 0(不設 statement timeout),同批及其他部署的其餘 migration 檔各使用 600s。workflow 不設定 job timeout override,沿用 Actions 既有 6 小時預設上限。缺少舊的 rehearsal SHA/timeout Variables 不會阻止正常部署;人工 full-size rehearsal 可另作維運診斷。

Migration 與會員資料

  • 原地套用 migration 128–164,共 40 檔;151_* 四檔、164_* 三檔,無 157/158。 workflow 使用 checkout 內的 SQL 與 DB ledger,保留 40 檔數量上限及既有 country ledger-hole 防護;正常 CD 不等待額外的人工 SHA manifest sign-off。
  • 134 把股票與部分會員相關表搬入 country schema,ticker 改存裸碼+listing type; 128 改會員 FK 的刪除行為。財務交易值與會員帳號應保留,碰到 collision、orphan、 無法推導的 listing 身分或 row/hash 不符時,由 migration preflight fail closed。
  • 使用一般 deploy-sit.yml,不執行 rebase-sit-db.yml、baseline adoption 或 seed-through。 不重設會員、subscription、entitlement、user group 或 refresh token。
  • 本次首次含 134 的部署須建立 fresh backup:先人工停止 API/scheduler 及其他 writer,再讀回 pg_stat_activity;備份期間持續確認 source cluster、container identity 與停止狀態。 人工 worker、backfill、promotion 等也必須停止,不只停止兩個命名容器。此一次性操作 須在觸發該次自動部署前完成,但不是 workflow gate 或 PR merge 條件。

已有備份的實際驗證範圍

2026-10-03 17:08 建立的 full archive 為 6,683,522,054 bytes,包含全部 schema/data, 另有 roles、PG/API/scheduler immutable images 與私有 runtime config。全部存於外接 SSD 的 AES-256 加密映像,key 留在 SIT 主機 0600 檔;最後已卸載並 readonly reopen 驗證 SHA-256 通過。原備份不會被本次 cutover 覆寫或 prune。

核心 18 表已獨立還原,逐表 row count/全列內容 SHA-256 PASS,涵蓋會員、登入、 訂閱權限、券商、交易平倉與觀察清單。全庫 post-data/索引還原為 INCOMPLETE, 因此尚未證明整庫恢復時間及所有索引能在此主機完成。本紀錄不把 subset restore 當成 full restore PASS;restore verification 的範圍與限制列在本文件。

一次性部署操作:切版前建立 fresh backup

本次首次套用 134 前須使用既有備份工具與人工驗證。程式碼可正常合入 dev;操作者須 在觸發會自動部署的 dev → SIT merge/push 前完成以下步驟,不得先觸發部署再補備份。 workflow 不含自動備份/receipt gate,也不要求 SIT_BACKUP_STATE_DIR/ SIT_BACKUP_VOLUME_UUID 或其他新 repo Variables;這項一次性操作不是 GitHub PR approval/merge check。後續例行部署沿用既有備份制度。

  1. 核對部署目標、原 PG cluster/volume 與 PG/API/scheduler immutable image IDs, 停止全部 writer 並讀回 API/scheduler 停止及 DB 外部 client 清空。保持停止直到部署; 不在備份後恢復舊 stack 再推版。若不能確認停止或資料一致性,停止發版。
  2. 用已核對的加密映像與 SIT 主機的 0600 key 掛載,建立新的私有切版目錄; 不沿用舊 snapshot 目錄。讀回映像實際可用容量、外接 backing 可用容量及預期 archive/image 空間。映像內 filesystem UUID 與外接 SSD UUID 不同;adapter 的 --volume-uuid 必須使用備份路徑所在的已掛載加密 filesystem UUID。
  3. 以既有 db_backup_host.py --full 呼叫完整 weekly path,例如下列命令。 此選項也會產生 daily dump;recovery 必須選同次 weekly/*_full_*.dump,不能使用 排除 derived tables 的 daily dump。--attempts 1 避免人工切版失敗後長時間自行重試。 CUTOVER_BACKUP_DIR 為新目錄,ENCRYPTED_FS_UUID、PG_SYSTEM_IDENTIFIER 與 BACKUP_SIDECAR 均須由本次 read-back 設定,不從範例推測。
/usr/bin/python3 investment-agent/backend/scripts/db_backup_host.py \
  --backup-dir "$CUTOVER_BACKUP_DIR" --volume-uuid "$ENCRYPTED_FS_UUID" \
  --container cortex-sit-postgres --database cortex_investment --user cortex \
  --system-identifier "$PG_SYSTEM_IDENTIFIER" --disabled-sidecar "$BACKUP_SIDECAR" \
  --full --attempts 1
  1. 在同次加密目錄保存完整 archive、pg_restore --list TOC、roles、schema/ledger、 canonical 會員表 row/hash、PG/API/scheduler image tar 與私有 runtime config。 對每個 payload 計算 SHA-256;卸載後 readonly reopen 再驗,並核對 source identity、 row/hash 與 writer 停止狀態。驗證失敗時保持停止,不執行 migration。
  2. 保存精確 payload path、image IDs 與驗證證據供 recovery 使用。本次一次性程序 與部署相鄰執行,事先停住服務會令 workflow 的自動 image fallback unavailable;取消/前置失敗時保持停止,依人工 核對的 DB+image 配對復原,不把舊容器存在當成相容證明。

stdout/Actions log 不輸出 DB 密碼、unlock key、使用者列或完整 container env; 私有備份證據不作 GitHub artifact。這些步驟只驗證新 archive bytes 與來源一致性, 不代表全庫 restore/40 檔 migration 演練 PASS。fresh backup 是本次首次 134 部署的 必要 recovery source,但不建立可重用的 workflow gate;人工 rehearsal 仍不作正常 CD 的 promotion gate。

失敗與配對 recovery

statement_timeout=0 只取消 134 的 SQL 時限,不取消 Actions 既有 6 小時 job 上限。 job timeout/取消也不保證容器內 SQL 已停止;134 的 COMMIT 與 runner 寫 ledger 是兩次 操作,可能出現 schema 已切換但 134 未登錄。遇到此情況保持 writers 停止,先核對 資料庫活動、實際 schema 與 ledger,再依配對 recovery 處理;不得盲目重跑或補登錄。

quiesce 前先核對持久 DB ledger 與 SIT 主機的最後成功啟用點 $HOME/.cortex/sit/activated-migration。任何完整檔名依 COLLATE "C" 排序晚於 該啟用點,即標記 SIT_MIGRATION_STARTED=true,包含尚未成功啟用的 partial progress 與零 pending 重試。只有檔案不存在的首次切版才以 127_story_strategy_source_revision.sql 為起點;壞檔、不可讀、擁有者/0600 權限不符、檔名不在 checkout 或啟用點不在目前 DB ledger,都停止於 quiesce 前。查詢失敗或無法辨識結果同樣拒絕切版。 有 pending SQL 時,migration actor 呼叫前也會標記 true;任何檔案執行失敗都可能已 commit 一部分 SQL, 不能用「尚無 ledger row」推論 schema 沒變。fallback image 只有在開始 capture 時 API 與 scheduler 都正在運行、immutable image IDs 完整有效時才保存;這也防止 SQL 已 commit 但 ledger INSERT 失敗的前次切版留下 stopped 舊容器後,被重跑復活。

  • 正式 quiesce 未開始:前置檢查失敗/取消不額外停止原 stack。既有 legacy runtime-state 搬移分支可能事先停止服務;若該分支被觸發仍須人工讀回/復原, 不能假設原 stack 保持運行。
  • marker 為 false 且 quiesce 已開始:只有完整運行的原 stack 所保存的 image 可以 fallback。成功啟用後的純程式碼部署若為零 pending、ledger 未超過啟用點,marker 保持 false;quiesce 或新 image 的 recreate/health 失敗可恢復原 image,deploy 仍失敗/取消。fallback unavailable 或恢復 失敗時停止部分啟動的服務;不替前次已變更 schema 的切版重啟舊 app。
  • marker 後、新 stack 通過穩定 health 驗證前:停止 API/scheduler,保留 failed DB 與 所有 backup,不 retag/start 舊 app,不自動 restore/DROP/rebase。 正常失敗及可處理的 TERM/INT 都會停止 writers; SIGKILL、runner 失聯或主機中斷仍需要人工確認實際 container/DB client 狀態。
  • 穩定 health 通過才標記 SIT_CUTOVER_ACTIVATED=true;下一個獨立步驟讀取實際 MAX(filename COLLATE "C"),以同目錄暫存檔加原子替換、權限 0600 寫入啟用點。 寫入失敗保留健康的新 stack,deploy 仍失敗;未更新的啟用點讓後續 migration 重試 保持保守判斷。之後 SES catch-up 失敗或取消, 保留健康的新 stack,deploy 仍失敗或取消,未完成的 catch-up watermark 不寫入。 啟用後的 cleanup 不切回舊 image;本 run 的 migration marker 保持原值。

啟用點只證明上次穩定健康的 stack 與 ledger 配對,不可手動提前寫入或刪除來放行 回退。人工 DB recovery 須一併核對該檔與配對 images;還原後啟用點不在 ledger 時, 先人工確認 schema 與健康服務再處理記錄,不能繞過 guard 直接重跑。

若正常 workflow 的 image fallback 不可用,使用本次部署前建立且已驗證的 fresh backup, 依下列順序人工配對 recovery;不得拿歷史 snapshot 假冒同次備份:

  1. 先讀回 SIT target、container IDs、DB cluster、schema ledger、所有 client,以及 部署 run 的核准 SHA/同次私有備份記錄的舊 image IDs;確認 writers 停止。若新 stack 曾上線, 先保留 failed DB 並盤點 cutover 後新增的會員/交易資料,避免回到備份點時遺失新寫入。
  2. 解鎖同次 backup encrypted image,驗證 payload bytes、SHA、TOC 與 roles/image/config 清單。不能只取 17:08 的舊 snapshot 代替 fresh cutover dump。
  3. 在隔離、明確命名的 candidate DB/volume 完整 restore fresh archive(含 post-data), 保留目前 failed DB;驗證原 schema、ledger、備份記錄中全部核心表 row/hash、FK、roles 與全部 index。 全庫 restore 若未完成,保持停機,不能只還原核心表就宣布恢復。
  4. 用備份記錄中的舊 PG/API/scheduler immutable image 與相同 runtime config,在隔離 candidate 驗證 API health、登入、觀察清單、持股/交易和 scheduler。
  5. 提交精確 target/切換命令及 read-back 證據,取得該 recovery 動作授權後才切換 SIT。 DB 與 image 必須成對切回;不在新 schema 上手動清除 marker 來繞過 image guard。
  6. 切換後重驗 API、scheduler、schema 和會員資料;失敗 DB、candidate 及 backup 不自動 刪除,清理須另列精確名稱與授權。

部署後驗收

確認 Deploy SIT run 成功、核准 commit 與 runtime image 對應、PG/API health、scheduler running、40 檔全在 ledger,以及 country schema verifier/OAuth gate 通過。再以登入、 watchlist、transaction 與新即時報價 API smoke 檢查代表資料;歷史 OHLC/technical/CHIP 重建工具的合入不代表已完成歷史重建,發版不額外手動執行這些大型 backfill。