やりたかったこと
ウェブサイトの問い合わせフォームAPIが、スプレッドシートに行を追加するスクリプトサーバーを直接呼び出していました。このスクリプトサーバーは書き込み中、全体にロック(lock) をかけるので、問い合わせが同時に何件も来ると1件だけ入り、残りは「ただいま混み合っています」というエラーで失敗していました。ユーザーはエラーを見て、入力した内容を失ってしまいました。
あらかじめ知っておくと便利な用語をいくつか紹介します。
- キュー(queue)とワーカー(worker): キューはやることを順番に並べておく場所で、ワーカーは列から1つずつ取り出して実際の作業をするプログラムです。飲食店の注文票と厨房にあたります。
- ack(確認応答): ワーカーがキューに「この作業は終わったので消して大丈夫です」と知らせる合図です。
- ジッター(jitter): リトライの間隔に混ぜるランダムな値です。失敗した呼び出し元が同じ瞬間に戻ってこないようにしてくれます。
やったこと
流れを「APIが直接呼び出す」から「APIはキューに入れてすぐ成功を返し、ワーカーがゆっくり書き込む」に変えました。
- APIは入力を検証し、問い合わせごとに固有の番号(UUID)を作って冪等キーとして付け、キューに入れてすぐに成功を返します。キューに入れること自体が失敗したときだけ、同じリクエスト内で直接呼び出しに戻ります。
- ワーカーはスクリプトサーバーを1回呼び出します。失敗が「やり直す価値のあるもの」(混雑、5xx、タイムアウト)なら、同じ配信の中で1〜4秒のランダムな間隔でさらに数回試します。
- それでもだめなら、わざと例外を投げて、キューが少しずつ長くなる間隔で再配信するようにします。最大回数を超えたら、エラー追跡ツールに問い合わせ番号と一緒に報告し、そこで初めてackします。
- スクリプトサーバーは、ロック待ち → 問い合わせ番号で重複チェック → 行の追加 → すぐに応答、だけを行い、通知は定期トリガーが別に送るように分けました。
学んだこと
キューを置いても競合は解消されません。 使っていたキューには「同時に1件だけ配信」というオプションがありませんでした。そのため10件が同じ瞬間にワーカーへ配信され、再配信も同じ時刻に集中してまたロックにぶつかりました。順序を整える役目は、ワーカー内でランダムな間隔でリトライする側に任せ、キューの長いバックオフは相手が長く故障している場合にだけ使うことにしました。→ リトライ、指数バックオフ、ジッター
例外をackすると静かに消えます。 ユーザーにはすでに「受付完了」と答えているため、ワーカーが想定外の例外を握りつぶして正常終了すると、問い合わせが消えても誰も気づきません。成功したときだけackし、冪等キーで再配信による重複に耐えられるようにしました。→ At-least-once配信、ackのタイミング、冪等キー
デプロイとトリガーが分かれているランタイムでは、エントリポイントが保証する必要があります。 スクリプトサーバーの時間ベースのトリガーは、コードをデプロイしても自然には作られません。デプロイだけしてトリガーを作らないと、通知が永遠に「待機」状態のまま残ります。エントリポイントが自分のトリガーを確認し、なければ作るようにしたところ、手動の手順がなくなりました。
まだ確認できていないこともあります。キュー経由の経路はローカルで実行できないので、ローカルテストはフォールバック経路でしか通っておらず、キュー経由の経路は本番ログでしか確認できていません。リトライ間隔のオプションが期待どおりに適用されなかった原因も、推測にとどまっています。