待てば直る失敗がある
API のエラーとは、ステータスコードが「呼び出しがうまくいかなかった」と伝える返答です。少し待ってもう一度試せば直るエラーもあれば、何度試しても直らないエラーもあります。
よいプログラムは、この二つを見分けます。前者は、待ち時間を延ばしながら再試行し、後者は、そこで止めて知らせます。
この課のゴール
メモ
再試行する失敗と、直すべき失敗を見分けられる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| ステータスコード | 呼び出しの結果を表す、返答の中の数 | 429 |
| レート制限 | 一分間にできる呼び出しの数の上限 | |
| バックオフ | 失敗のたびに待ち時間を延ばすこと | |
| ジッター | 待ち時間のランダムな変化。一斉に再試行しないため | |
| タイムアウト | 返答を待つ最長の時間 |
学生が入力するセルは、このプレビューでは空白です。
ステータスコードが次の行動を教える
200 番台のステータスは、呼び出しが成功したことを表します。400 番台のステータスは、たいてい呼び出しの作り方が間違っているので、同じ呼び出しはまた失敗します。
ほかのステータスは、短く一時的な問題を表します。時間切れ、サーバーの混雑、レート制限などです。その場合は、待ってからもう一度試すのが正しい対応です。
例:再試行か、修正か
| ステータス | 意味 | 対応 |
|---|---|---|
| 400 | リクエストが間違い | リクエストを直す |
| 401 | 有効なキーがない | キーを直す |
| 403 | 許可なし、または残りの枠なし | 原因を直す |
| 408、504 | 時間切れ | 再試行する |
| 429 | リクエストが多すぎる | 待ってから再試行する |
| 500、502、503 | サーバーに問題 | 数回再試行する |
学生が入力するセルは、このプレビューでは空白です。
ステータスを分け、記録済みの429を読む
docker exec -w /home/node/project agent node -e '/* どの失敗なら、もう一度試す価値があるでしょうか。 */ const kind = (status) => { if (status >= 200 && status < 300) return "ok"; return [408, 429, 500, 502, 503, 504].includes(status) ? "retry" : "fix"; }; const notes = { 200: "成功", 400: "リクエストが間違い", 401: "有効なキーがない", 403: "許可されていない", 408: "時間切れ", 429: "リクエストが多すぎる", 500: "サーバーの故障", 503: "サーバーの準備ができていない" }; for (const status of Object.keys(notes)) { console.log(status + " " + kind(Number(status)).padEnd(5) + " " + notes[status]); } /* 記録済みの429を、データとしてこのスクリプトに書きました。呼び出しはしません。 */ const recorded = { status: 429, headers: { "retry-after": "2" }, body: { error: { message: "リクエストが多すぎます。ゆっくりお願いします。" } } }; console.log("記録済みの429:" + recorded.body.error.message + "サーバーは " + recorded.headers["retry-after"] + " 秒を求めています。");'ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
バックオフは毎回長く待つ
すぐ再試行すると、失敗の原因になった負荷をさらに増やします。バックオフは、少し待ち、次にその倍、さらにその倍と、上限まで待ち時間を延ばします。
ジッターも加えます。待ち時間を、半分から全部の間のランダムな量だけ変えます。そうすれば、いっしょに失敗した百台のクライアントが、同じ瞬間に再試行しません。返答に Retry-After ヘッダーがあれば、それを使います。
バックオフの予定表を表示する
docker exec -w /home/node/project agent node -e '/* 失敗のたびに待ち時間を延ばし、上限を設け、ばらつき(ジッター)を加えます。 */ const base = 1; const cap = 8; const delays = []; for (let attempt = 0; attempt < 6; attempt++) delays.push(Math.min(cap, base * 2 ** attempt)); console.log("待ち時間(秒):" + delays.join(", ")); const jittered = delays.map((one) => one / 2 + Math.random() * one / 2); console.log("ばらつきあり、ある実行の例:" + jittered.map((one) => one.toFixed(2)).join(", ")); console.log("ばらつきありの待ち時間は、元の半分から元の全部までの間になります。");'ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
よくある間違い
注意
どのエラーも再試行すべきだと考える人が多くいます。そうではありません。400 や 401 は、毎回同じように失敗します。それを再試行するループは、時間と枠を無駄にするだけです。通る可能性のある失敗だけを、数回だけ再試行します。
タイムアウトで、待ち続けずにすむ
返答が来ない呼び出しもあります。待つ最長の時間としてタイムアウトを決め、それを超えたら、再試行する価値のある失敗として扱います。
下の補助ファイルは、これらを一つにまとめます。タイムアウト、数回の試行、ジッター付きのバックオフがあり、直すべき失敗ではすぐ止まり、何もうまくいかなければ記録済みのサンプルを表示します。
retry.js その1を書く
docker exec -w /home/node/project agent node -e 'require("fs").writeFileSync("retry.js", ["// retry.js その1:どのステータスを再試行するか、どれだけ待つか。", "const lab = require(\"./lab.js\");", "const kind = (status) => {", " if (status >= 200 && status < 300) return \"ok\";", " return [408, 429, 500, 502, 503, 504].includes(status) ? \"retry\" : \"fix\";", "};", "const pause = (ms) => new Promise((done) => setTimeout(done, ms));", "", "// バックオフ、Retry-After ヘッダー(あれば)、上限、ジッター。", "function waitFor(attempt, response) {", " let wait = 150 * 2 ** attempt;", " if (response) wait = Math.max(wait, Number(response.headers.get(\"retry-after\")) * 1000 || 0);", " wait = Math.min(wait, 3000);", " return wait / 2 + Math.random() * wait / 2;", "}"].join("\n") + "\n")'ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
再試行のループを足す
docker exec -w /home/node/project agent node -e 'require("fs").appendFileSync("retry.js", ["", "// その2:再試行します。ただし、直すべき失敗は再試行しません。", "async function callWithRetry(body, tries) {", " for (let attempt = 1; attempt <= tries; attempt++) {", " let response = null;", " try {", " response = await lab.send(body, 15000);", " if (response.ok) return { ok: true, data: await response.json() };", " console.log(attempt + \" 回目:HTTP \" + response.status + \"、\" + kind(response.status));", " if (kind(response.status) === \"fix\") return { ok: false, why: \"原因を直す、再試行しない\" };", " } catch (error) {", " console.log(attempt + \" 回目:応答なし(\" + error.name + \")\");", " }", " if (attempt < tries) await pause(waitFor(attempt, response));", " }", " return { ok: false, why: tries + \" 回試して断念\" };", "}", "", "module.exports = { callWithRetry };"].join("\n") + "\n")'ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
再試行付きでモデルを呼ぶ
docker exec -w /home/node/project agent node -e 'const { callWithRetry } = require("./retry.js"); (async () => { const body = { max_tokens: 20, messages: [{ role: "user", content: "一語であいさつしてください。" }] }; const result = await callWithRetry(body, 3); if (result.ok) console.log("モデルの答え:" + result.data.choices[0].message.content); else console.log("記録済みのサンプル:こんにちは。(" + result.why + ")"); })();'ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
さらに:試行回数を変える
retry.js で、最初の待ち時間を 150 から 50 に変えます。最後の Run で、試行回数を 3 から 5 に変えます。実行して、表示を読んでください。
人が待っているボタンには何を選ぶか、夜のあいだ動く仕事には何を選ぶかを、言ってみましょう。