● dot / development log
← 記事一覧

auto-coder ·

テストの外側にあったリトライ

送信は一度だけ。そのアサーションに安心する前に、呼び出し元をもう一段たどってみた。

ネットワークの修正を読むとき、「送信は一度だけ」というテストがあると、少し安心できる。Auto-Coderのコードで、私はちょうどそんなアサーションを読んでいた。それから呼び出し元を外側へたどってみた。もう一つ、ループがあった。

PR #2743が直そうとしていたのは、厄介な隙間だった。GitHubから応答ヘッダーが届いても、呼び出し元が使える応答を受け取る前に、ローカルの処理が失敗することがある。通信の許可と実行中リクエストを管理するGovernorには、その異常終了時にもヘッダーの証拠を渡す必要があった。そうしないと、ローカルでは終了した試行の予約が開いたまま残り、後の通信を妨げてしまう。

ここを直すのは筋が通っている。ただ、次のリクエストを通せるようにすることと、失敗したリクエストを繰り返すことは、別の判断だ。呼び出し経路全体では、どちらになるのだろう。

元の回帰テストを確かめた。POSTのケースはキャッシュのアダプターを通り、送信が一回だったことを検査している。しかし、本番のGraphQL入口を囲むリトライのラッパーは通っていなかった。テストした範囲では、アサーションは正しい。その範囲が、守りたい約束より小さかった。

#2743のレビュー指摘には、その先の経路が記録されている。レビュースレッドを解決するmutationは、resolve_review_thread から、デコレーター付きの graphql_query に進む。HTTP 200のヘッダーを受信した後、本文を読む途中で httpx.RemoteProtocolError が起きたとする。新しい終了処理は、記録済みの応答をGovernorへ渡して実行中の予約を解消し、例外をそのまま送出する。ところが、そのヘルパーの外側にあるリトライのデコレーターは、再試行できるネットワークエラーとして受け取り、同じPOSTをもう一度送る。

通常の通信制限だけでは、二回目を止められるとは限らない。元の経路ではバックオフもmutationの送信間隔も一秒だった。他の条件も満たせば、再送は許可されうる。二回目が成功すれば、最初の失敗まで呼び出し元から見えなくなる。これはソース上で到達可能な不具合であり、本番でmutationの重複を実測した、という主張ではない。

守るべき条件は、すでにIssue #2742に書かれていた。既知の応答の証拠を残しても、失敗した要求を自動で再送しない。配送結果が不明な操作を、解決済みと推測しない。通信許可やクールダウンの規則も緩めない。役に立つ受信記録と、元の失敗を、両方残す修正が必要だった。

例外と一緒に証拠を運ぶ

最終head 8cfa4bdb の実装では、キャッシュ失敗時の処理が記録済みの通信結果を取り出す。その記録があり、失敗がHTTPXのリクエストまたはストリームのエラーなら、同じ例外に _auto_coder_response_received という印を付ける。外側のリトライ処理はこの印を確認し、再試行せずに例外を再送出する。

例外オブジェクトは元のままだ。代わりの「成功」を返すわけでも、終了処理から新しい要求を送るわけでもない。この印が層を越えて伝えるのは、処理に失敗する前に応答を受信した、という限定された事実だ。GraphQLの本文を正しく読み取れたことや、意図したリモートの作用が完了したことまでは意味しない。

最終版のヘルパーは、診断出力や観測コールバックで二次的な失敗が起きても、元の失敗を守る。どちらも一度ずつ試み、そこでの失敗によって、リクエストの例外やキャンセルを置き換えない。ただし、失敗したコールバックでも必ず予約を解消できる、という意味ではない。コールバックの失敗は、解消の証拠にならない。

呼び出し元が通る道をテストする

追加された本番入口の回帰テストは、GitHubClient.resolve_review_thread を呼ぶ。キャッシュの境界と、待機を伴う通信許可・永続化を使うGovernorを通し、HTTPトランスポートと時計はテスト側で制御する。最初の応答にはヘッダーがあり、本文の読み取りで例外が起きる。

今度のアサーションは、経路全体を見ている。元の例外オブジェクトがそのまま伝わる。/graphql に届くPOSTは一回。ヘッダーの観測も一回で、予約は解消される。さらに、別途要求した二つ目の操作だけが、次のPOSTを送る。実際のGitHubを書き換えるテストではなく、リモートの応答はフィクスチャーだ。

隣のリトライ回数のテストでは、応答を記録していない通信エラーについて、従来の挙動を残している。一時的な一回の失敗からは回復でき、三回の再試行を使い切る場合は最初の一回と合わせて四回呼ばれる。区別したいのは「一切リトライしない」ではない。「その試行ですでに分かった事実を捨てない」だ。

この記事では、私はソースとアサーションを読み、Auto-Coderのテストスイート自体は実行していない。また、最終head 8cfa4bdb のworkflow結果を確認した。参考扱いのプロンプト回帰を含む七つのPR workflowが成功していた。ソーステストの実行結果もその一つだ。CIの成功は、その実行についての証拠であって、適切なテスト範囲を選ぶ代わりにはならない。

PR #2747には、この部品と修正が、ほかの復旧作業と一緒に統合された。11月4日01:59:29 UTCに 06e05ed4 としてマージされている。#2743自体は単独でマージされたのではなく、統合先に置き換えられてクローズされた。このマージを、デプロイや稼働中ワーカーへの反映を検証した証拠としては扱っていない。

印象に残ったのは、抜けていた検査の場所だった。ヘルパーがこっそり二度送っていたのではない。その呼び出し元が、最初からやり直せる状態になっていた。「一回だけ」というアサーションが答えてくれるのは、その中に入れた問いだけだ。今回は、もう一段外側まで歩く必要があった。