CI 故障診斷修復與 Circuit Breaker 重構 PR 拆分策略¶
文檔資訊
- 分類: debugging
- 難度: advanced
- 預估閱讀時間: 12 分鐘
- 標籤:
go,testcontainers,circuit-breaker,ci-debugging,redis,postgresql,pg_cron,pg_partman,api-refactoring,pr-splitting
摘要¶
本對話涵蓋兩個完整的 CI 故障診斷與修復,以及 Circuit Breaker 子系統重構的 PR 拆分架構決策。
第一部分:診斷並修復了兩個 CI 失敗根因——smoke test 因環境變數名稱從 REDIS_ADDR 改為 REDIS_URL 後 docker-compose 未同步更新,導致 Redis 連線失效進而觸發 NoopDistributedLocker 造成 500 錯誤;integration test 因 testcontainers 使用 postgres:16-alpine 缺少 pg_partman/pg_cron 擴充套件,遷移腳本 024 無法執行。
第二部分:深度分析 Circuit Breaker 重構應拆成 PR1(API cleanup)和 PR2(outcome semantics)兩個獨立 PR 的設計原則,並評審了第三方提出的詳細設計稿。
關鍵學習¶
-
環境變數名稱變更後,所有使用端(docker-compose、K8s manifest、CI script)必須同步更新,否則舊名稱會被靜默忽略
-
testcontainers 需要特定 PostgreSQL 擴充套件時,應使用
CustomizeRequest(FromDockerfile)指向包含擴充套件的自定義 Dockerfile,而非原生 alpine 映像 -
API 簽名重構(footgun removal)與行為語義變更(outcome semantics)必須嚴格拆分成不同 PR,防止偷渡語義變更
-
Circuit Breaker 的 tri-state(Success/Failure/Ignored)語義中,
context.Canceled應為 Ignored,context.DeadlineExceeded應為 Failure,這是明確的業務決策而非實作細節 -
PR 描述必須明確標注「compile-time breaking API change」而非誤導性的「zero risk」,讓 reviewer 有正確的審查基準
-
testcontainers
CustomizeRequest中同時指定 Image 和FromDockerfile會導致cannot specify both an Image and Context錯誤,必須選一種方式
技術細節¶
CI 故障 1:Smoke Test 500 錯誤¶
根因追蹤路徑(5 層):
HTTP 500
→ mapError default branch
→ initializeNewPlayer() 失敗
→ NoopDistributedLocker.Lock() 永遠返回錯誤
→ redisClient == nil(因為 REDIS_URL 環境變數未設定)
→ docker-compose.yml 仍使用舊的 REDIS_ADDR 變數名
修復:
# docker-compose.yml - twirpserver service
# 舊(被靜默忽略):
- REDIS_ADDR=redis:6379
# 新:
- REDIS_URL=redis://redis:6379/0
- REDIS_TLS_ENABLED=false
CI 故障 2:Integration Test 缺少 PostgreSQL 擴充套件¶
根因: migration 024_partman_auto_partition_management.up.sql 需要 pg_partman 和 pg_cron,但 testcontainers 使用 postgres:16-alpine 不含這些擴充套件。
修復(internal/publisher/testinfra/testinfra.go):
testcontainers.CustomizeRequest(testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: filepath.Join(projectRoot, "deployments/docker"),
Dockerfile: "postgres.Dockerfile",
KeepImage: false,
},
Cmd: []string{
"postgres",
"-c", "shared_preload_libraries=pg_cron",
"-c", "cron.database_name=publisher_test",
},
},
})
注意: 不能同時指定 Image 和 FromDockerfile,只能選一種。
Circuit Breaker PR 拆分原則¶
PR1(API Cleanup)可以動:
- CircuitBreaker.Execute(ctx, func() error) 改簽名
- memory.go / noop.go 跟著改 callback 形狀
- ProtectedCall / ProtectedVoidCall 改簽名
- 所有 callers 機械式替換
- 對應測試更新(注意語意,不只是字面替換)
PR1 不能動(PR2 範圍):
- IsSuccessful 語義
- defaultIsFailure 分類規則
- context.Canceled / context.DeadlineExceeded 是否算 failure
- memory.go 的 failure counting 行為
PR2 核心設計(tri-state):
- 新增共用 internal/cbcore
- 統一 Success / Failure / Ignored 三態
- context.Canceled → Ignored
- context.DeadlineExceeded → Failure
- half-open 強制 permit gate + open-timeout jitter
- metrics 升為正式 runtime contract
What Changed¶
CI 修復¶
修復了兩個 CI 故障:
-
smoke test 500:
docker-compose.yml中REDIS_ADDR改為REDIS_URL=redis://redis:6379/0,對應 commitd0e3a0c的環境變數改名。 -
integration test FAIL:
testinfra.go的SetupSharedEnv()從直接使用postgres:16-alpine改為透過CustomizeRequest(FromDockerfile)建立包含pg_partman/pg_cron的自定義映像,並在啟動命令中加入shared_preload_libraries=pg_cron。
架構規劃¶
完成 Circuit Breaker 重構的 PR 拆分規劃:
-
PR1:純 API cleanup,移除 footgun(
Execute不再把ctx作為 callback 參數傳入),確保go build通過即可驗證所有 call site 更新完整。 -
PR2:breaker 子系統重構,建立
internal/cbcore共用模組,實作 tri-state outcome semantics,涉及publisherclient的 body lifecycle/retry rewind correctness 修正。
So What¶
為什麼值得記錄¶
**環境變數改名的連鎖效應**是一個高頻陷阱。本案例展示了完整的診斷路徑——從表面的 HTTP 500 一路追蹤到 NoopDistributedLocker,說明靜默忽略未知環境變數是一種危險的降級行為。
testcontainers 使用自定義 Dockerfile 的模式在需要資料庫擴充套件的專案中是必要技巧,且有特定的 API 限制(不能混用 Image 和 FromDockerfile)。
PR 拆分原則(API 簽名 vs 行為語義分離)是一個可複用的架構決策框架,適用於所有涉及底層基礎設施重構的場景。「PR1 可以動 API,但不能動 breaker 判定語義」這條邊界線是核心原則。
Trade-offs¶
- testcontainers 自定義 Dockerfile 建置時間增加 vs 測試環境與生產環境一致性:值得付出這個代價
- PR 拆分增加 review 輪次 vs 確保語義變更不被 API cleanup 偷渡:正確的取捨
ProtectedVoidCall保留 vs 減少 API 面積:語義可讀性的合理讓步context.Canceled設為 Ignored vs 設為 Failure:客戶端主動取消不應懲罰 breaker,是正確的業務語義選擇_ context.Context在 PR1 實作中明確忽略 ctx vs 直接使用:更清楚地表達「PR1 不使用 ctx,這是 PR2 的事」
Try It Fast¶
# 驗證 smoke test 修復
cd /projects/semi-brain
docker compose up -d --build
go test -v -tags smoke -count=1 -timeout 120s ./test/smoke/...
# 驗證 integration test 修復(需要較長時間建置自定義 postgres 映像)
go test -v -tags integration -count=1 -timeout 600s ./test/publisher/... ./test/player/...
# 驗證 circuit breaker PR1 API 變更後所有 call site 已更新
go build ./...
# 確認沒有偷渡 PR2 語義(搜尋是否有改到這些)
grep -r 'IsSuccessful\|defaultIsFailure\|failure counting' pkg/circuitbreaker/
Recommendation¶
-
每次更改環境變數名稱時,建立一個 checklist:docker-compose.yml、K8s manifest、ExternalSecret、CI workflow、README——缺一不可。
-
testcontainers 需要擴充套件時,統一使用
deployments/docker/postgres.Dockerfile,不要用-alpine映像,保持測試環境與生產一致。 -
PR 描述中明確標注「compile-time breaking API change」,不要使用「zero risk」這種不精確的表述,應改為「低 runtime 風險、高可編譯性可驗證」。
-
Circuit Breaker PR2 開工前,先確認 PR1 merge 且 CI 通過,避免在 PR1 尚未穩定時同時進行語義重構。
-
**PR2 的
context.Canceled→ Ignored 語義**需要在測試中明確驗證,避免未來有人「修正」成 Failure 而不知道這是刻意設計的業務決策。