接続拒否とタイムアウトは全く別の知らせだ
一言でいうと
「APIが動かない」という報告で、最も多くの情報を含んでいるのは、ログではなく、失敗メッセージの正確な文言です。
なぜ必要なのか
顧客が「APIが死んでいます」と言うとき、その言葉の裏には、まったく違う5つの状況が隠れていることがあります。そして、どれなのかは、失敗メッセージの1行でほとんど分かれます。
Connection refusedは、悪い知らせではありません。パケットが宛先まで行って、戻ってきたという証拠です。ルーティング、ファイアウォール、NATのような経路上のすべてのゲートをすでに通過したということで、残る原因は、対象ホスト1台の中に絞られます。プロセスが死んでいる、別のポートで待ち受けている、0.0.0.0ではなく127.0.0.1にだけバインドされている、のいずれかです。
Connection timed outには、何の情報もありません。誰も答えなかったという意味で、ファイアウォールが黙って捨てたのかもしれず、ルーティングがないのかもしれず、相手が過負荷なのかもしれません。実務で使うファイアウォールは、ほとんどいつも、黙って捨てるポリシーなので、ファイアウォールがブロックすると、refusedではなくtimeoutになります。そのため、refusedを見てファイアウォールを調べるのは、すでに通過したゲートをもう一度確認することです。
Name or service not knownは、対象サーバーにパケットが1つも出ていません。ファイアウォールのログに何もないのが正常で、調査は名前解決のほうに向かいます。
ここに、失敗までにかかった時間も一緒に測ると、確信が生まれます。数ミリ秒で終わったなら、往復が完了した(refused)ということで、2分7秒ごろに切れたなら、カーネルがSYNの再送をすべて使い切ったということです。アプリのタイムアウトを30秒に設定したのに、実際には127秒で失敗したなら、その設定が適用されていないというシグナルです。
どう動くのか
接続ができたら、次はステータスコードです。ここで最もよく誤診されるのが、502と504です。
502は、応答を受け取れなかったのではなく、有効でない応答を受け取ったのです。応答は来たのに、プロキシがパースできなかった場合も502です。そのため、502に対してタイムアウトを延ばしても、何の効果もありません。時間内に応答を受け取れなかった場合は504です。
そして、ドキュメントが不十分なAPIに接続するときに、必ず確認すべきことが、さらに3つあります。
ページネーションの終わりをどう知るか。has_nextのような明示的なフラグがあればそれを信じ、なければ空の配列が来るまで回ります。ここでよくあるバグは、最初のページのtotalだけを見てページ数を計算し、実際には最後のページを落としてしまうことです。割り算の余りを忘れるという、古典的なミスです。
一時的な失敗をどう扱うか。503やネットワークエラーは、リトライで乗り越えられることが多いです。ただし、リトライしてよいものと、いけないものに分かれます。照会は何度繰り返しても同じですが、作成のリクエストをリトライすると、重複が作られます。4xxは、リクエストを直さない限り何度送っても同じ結果なので、リトライの対象ではありません。
ドキュメントにない欠損がどれだけあるか。これが実務で最もよく人を捕まえます。仕様では必須と書かれたフィールドが、実際の応答では空文字列で来ることがよくあり、その値をそのまま集計に入れると、黙って間違った数字が出ます。
現場での姿
そのため、新しいAPIに接続するときの最初の作業は、コードを書くことではなく、全件調査です。すべてのページを1回回りながら、総件数、合計、そして各フィールドの欠損件数を数えてみます。
この1回の調査が、その後の数週間のデバッグをなくします。「全60件のうち6件はregionが空です。これらをどう処理しますか」という質問を最初の週に投げれば、あとで地域別の売上合計が合わないという報告が来ません。
他人のAPIに頼るコードを安全にするには
全件調査で、何を相手にしているかがわかったなら、次は、そのAPIが不安定になっても、こちらが崩れないようにすることです。他人のAPIは、こちらが直せないので、前提をコードに書いておくことが、唯一の防御です。
受け取ったものをそのまま信じません。仕様で必須と書かれたフィールドが、空で来るのをすでに見ました。そのため、読み取る地点で型と範囲を確認し、外れていれば、その件をスキップしますが、スキップした数を数えて残します。黙って捨てると、あとで合計が合わない理由を探せず、まるごと失敗させると、1件のために全体が止まります。
タイムアウトを必ず設定します。デフォルト値がない、または無限のライブラリが多いです。タイムアウトのない呼び出し1つがワーカーを永遠に掴んでしまうと、前に見たコネクションプール枯渇が、そのまま再現します。接続までの時間と応答までの時間を別に設定できるなら、分けて設定します。
リトライは冪等なものにだけ行います。前に述べたとおり、照会は何度やっても同じですが、作成は違います。そして、リトライの間隔にランダム性を混ぜないと、相手が少し不安定になってから回復するときに、すべてのクライアントが同じ瞬間に押し寄せて、再び倒してしまいます。
応答をそのまま保存しておきます。原本を残しておけば、パースのルールをあとで直したときに、やり直せます。パースした結果だけを保存すると、ルールが間違っていたと気づいた時点で、すでに原本がありません。保存のコストより、再度取得するコストのほうが、たいていはるかに大きいです。
最後に、相手が変わったことに気づく仕組みを置きます。応答のフィールド構成や件数が、普段と大きく違うときに知らせるようにしておけば、APIが黙って変わったときに、ユーザーより先に気づけます。他人のAPIは予告なしに変わり、予告があったとしても、そのメールはたいていこちらには届きません。
次のラボですること
ドキュメントが1ページのWikiしかない注文APIに接続して、バージョンを確認し、すべてのページを回って総件数と合計を出し、ドキュメントにない欠損を数え、不安定なエンドポイントをリトライで通過します。