赤信号がすべて同じ赤信号ではない
一言でいうと
パイプラインの失敗は、インフラの問題・フレーキーテスト・本物の欠陥の3つに分類されます。分類しないと、人はすべてに「もう一度実行」で対応してしまい、本物の欠陥がその中に隠れます。
なぜ必要なのか
失敗が数日に1回なら、人はログを読みます。1日に10回なら、読みません。リトライボタンが先に押され、通れば忘れられます。この習慣がついたチームでは、本物の欠陥も、2、3回リトライされているうちにたまたま通れば、そのままマージされてしまいます。赤信号がシグナルであることをやめる瞬間です。
そのため必要なのは、よりよいログではなく分類です。この失敗がどの種類なのかを先に決め、種類ごとに異なる対応をします。
- インフラ: ダウンロードの失敗、名前解決の失敗、ディスク不足、メモリ超過、ランナーの回収。コードとは無関係です。対応は、上限のあるリトライと容量の調整で、件数を数えて傾向を見ます。
- 不安定(flaky): 同じコミットなのに結果が分かれます。時刻、乱数、実行順序、並行性、外部への依存に頼っているテストです。対応は隔離と修正で、リトライで覆い隠すと永遠に残ります。
- 本物の欠陥: 同じコミットなら、常に失敗します。対応は、直すか元に戻すかだけです。
分ける方法は、意外に単純です。同じコミットをもう一度実行して、結果が分かれるかです。分かれるなら不安定かインフラで、いつも同じなら本物の欠陥です。そのため、パイプラインは、「この実行がどのコミットだったのか」を結果に必ず残す必要があります。
再現できる失敗にする
不安定と本物の欠陥を分けるには、同じ条件を再び作れなければなりません。そのため、実行ごとに次のものを結果に書き残しておきます。
- 乱数シード。テストの順序をシャッフルしたり、ランダムなデータを使ったりするなら、シードをログに出力し、そのシードをもう一度入れて実行できるようにします。シードを残さないランダム化は、再現できない失敗を生みます。
- 時刻。日付に頼るテストは、月末や深夜0時、うるう年にだけ壊れます。テストが使う時刻を固定できるようにしておけば、その条件をそのまま再現できます。
- 環境。ツールのバージョン、ロケール、タイムゾーン、並列度。同じコミットなのに結果が分かれるなら、たいていこのうちのどれかが違っていました。
この3つを残すだけで、「再現できません」という報告が大きく減ります。
いつ壊れたのかを探す
失敗の原因となったコミットを探すとき、ログをさかのぼって読む方法は、コミット数に比例して時間がかかります。二分探索は、ログに定数を掛けた時間で済みます。git bisectのドキュメントは、675個のリビジョンを約10ステップ、337個を約9ステップに絞り込むと書いています。コミット1,000個を10回で絞り込むという意味です。
自動化するには、条件が1つあります。判定スクリプトが決定的でなければなりません。同じコミットで同じ答えを出さないと、二分探索は無関係なコミットを原因として指し示します。そのため、不安定なテストで二分探索を実行してはいけません。
git bisect runは、スクリプトの終了コードで判定します。規約は正確に決まっています。
0 이 커밋은 정상(good/old)
1..127 이 커밋은 문제 있음(bad/new) 단, 125 는 제외
125 판정할 수 없음 — 이 커밋은 건너뛴다(skip)
128..255 이분 탐색 자체를 중단한다
125が別に用意されている理由が重要です。ビルドがそもそも通らないコミットのように、判定できないものを「問題あり」と報告すると、原因がそちらへ誤って誘導されます。そして126と127は、POSIXシェルが「実行できない」「コマンドが見つからない」に使う値なので、125が、この用途に使える最大の値として選ばれました。判定スクリプトを書くときは、exit -1のようなものを使わないよう注意します。その値は255になり、探索をまるごと中断させます。
フィードバック時間バジェット
フィードバック時間に数値の上限を設けないと、パイプラインは静かに遅くなります。1ステップずつ増えた時間には誰も気づかず、ある日「もともと少し時間がかかるものです」になります。
上限を超えたときにすることは、決めておくのがよいです。マージをブロックするステップとしないステップを分けて遅いものを後ろへ送る、テストを分割する、あまり壊れない領域のテストを変更範囲に応じてスキップする、といった方法です。どの方法でも、何を切り捨てたかを記録する必要があります。切り捨てたものを書き残さないと、数か月後にその領域で事故が起きたとき、なぜ捕まえられなかったのかを誰も説明できません。
現場での姿
- 失敗の種類を数え始めた最初の週に、たいてい「インフラが半分」という結果が出ます。すると、直す対象がテストではなく、容量とリトライのポリシーであることが明確になります。
- フレーキーテストの一覧を作っておくと、新しい失敗がその一覧にあるかどうかをまず見るようになり、調査の時間が大きく減ります。一覧になかったものが不安定に振る舞ったら、それ自体が事件です。
- 「昨日までは動いていたのに」は、たいてい昨日ではありません。二分探索を実行してみると、2週間前のコミットが出てくることがよくあります。その間に、誰もその経路に触れていなかっただけです。
- アーティファクトに、どのコミットから出たのかを
git describeのような値で埋め込んでおけば、事故のときに「今動いているのはどのコミットなのか」をたどり直す作業が、数分で終わります。
参考
- git bisect: https://git-scm.com/docs/git-bisect
- git describe: https://git-scm.com/docs/git-describe
- 継続的インテグレーション: https://martinfowler.com/articles/continuousIntegration.html
次のラボですること
gitリポジトリを1つ作ってコミットを積み、途中のどこかで壊れるようにしておいたうえで、原因のコミットを二分探索で探します。まず手でgit bisect start / good / badを実行して、何ステップで絞り込めるかを数え、同じことを判定スクリプトで自動化します。スクリプトが終了コードの規約を守っているかを確認し、ビルドが通らないコミットをわざと紛れ込ませて、125でスキップしないと、原因がどのように誤って指し示されるかを自分の目で見ます。続いて、結果が分かれる判定スクリプトを入れて、二分探索が崩れるのを確認し、最後に、失敗ログを種類ごとに数えて報告する分類スクリプトを書きます。