跳轉到

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_partmanpg_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 故障:

  1. smoke test 500docker-compose.ymlREDIS_ADDR 改為 REDIS_URL=redis://redis:6379/0,對應 commit d0e3a0c 的環境變數改名。

  2. integration test FAILtestinfra.goSetupSharedEnv() 從直接使用 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

  1. 每次更改環境變數名稱時,建立一個 checklist:docker-compose.yml、K8s manifest、ExternalSecret、CI workflow、README——缺一不可。

  2. testcontainers 需要擴充套件時,統一使用 deployments/docker/postgres.Dockerfile,不要用 -alpine 映像,保持測試環境與生產一致。

  3. PR 描述中明確標注「compile-time breaking API change」,不要使用「zero risk」這種不精確的表述,應改為「低 runtime 風險、高可編譯性可驗證」。

  4. Circuit Breaker PR2 開工前,先確認 PR1 merge 且 CI 通過,避免在 PR1 尚未穩定時同時進行語義重構。

  5. **PR2 的 context.Canceled → Ignored 語義**需要在測試中明確驗證,避免未來有人「修正」成 Failure 而不知道這是刻意設計的業務決策。


本文檔由 Semi-Brain 自動生成

Session ID: 59325dc8-0079-4c87-a93e-f2734386d733

分析信心度: 88%