Join us for Firebase Summit on November 10, 2021. Tune in to learn how Firebase can help you accelerate app development, release with confidence, and scale with ease. Register

非同期関数の再試行

このドキュメントでは、失敗時に非同期(非 HTTPS)バックグラウンド関数をリクエストして再試行する方法について説明します。

再試行のセマンティクス

Cloud Functions では、イベントソースによって生成された各イベントにつき、最低 1 回はイベント ドリブン関数が必ず実行されることになっています。ただし、デフォルトでは、関数の呼び出しがエラーによって終了した場合、その関数が再度読み出されることはなく、そのイベントはドロップします。イベント ドリブン関数の再試行を有効にすると、失敗した関数の呼び出しが正常に完了するか、再試行期間(デフォルトは 7 日間)が終了するまで再試行が行われます。

イベント ドリブン関数が完了しない理由

まれに、内部エラーのため関数が早期に終了することがあります。関数は、デフォルトで自動的に再試行される場合もあれば、再試行されない場合もあります。

よくあるのは、関数コード内でエラーがスローされるためにイベント ドリブン関数が正常に完了できないケースです。これが起こる理由としては、次のようなものがあります。

  • ファンクションにバグがあるため、ランタイムから例外がスローされる。
  • ファンクションがサービス エンドポイントに到達できないか、エンドポイントに到達しようとしている間にタイムアウトする。
  • 関数から意図的に例外がスローされる(たとえば、パラメータの検証に失敗した場合)。
  • Node.js に記述されている関数が、拒否された Promise を返すか、コールバックに null 以外の値を渡す場合。

上記のいずれの場合も、関数はデフォルトで実行を停止し、イベントが破棄されます。エラーが発生したときに関数を再試行する場合は、「失敗時に再試行する」プロパティを設定して、デフォルトの再試行ポリシーを変更できます。これにより、関数が正常に完了するまで数日間イベントが繰り返し再試行されます。

再試行の有効化と無効化

GCP Console の使用

GCP Console で再試行を有効または無効にする手順は次のとおりです。

  1. Cloud Platform Console の [Cloud Functions の概要] ページに移動します。

  2. [関数を作成] をクリックします。または、既存の関数をクリックしてその詳細ページに移動し、[編集] をクリックします。

  3. 関数の必須フィールドを入力します。

  4. [トリガー] フィールドがイベントベースのトリガータイプ(Cloud Pub/Sub や Cloud Storage など)に設定されていることを確認します。

  5. [詳細] をクリックして詳細設定を表示します。

  6. [失敗時に再試行する] というラベルが付いたチェックボックスをオンまたはオフにします。

関数コード内

Cloud Functions for Firebase では、関数のコード内で再試行を有効にできます。functions.foo.onBar(myHandler); のようなバックグラウンド関数に対してこれを行うには、runWith を使用して失敗ポリシーを構成します。

functions.runWith({failurePolicy: true}).foo.onBar(myHandler);

このコードのように true と設定すると、失敗時に再試行する関数が構成されます。

ベスト プラクティス

ここでは、再試行の使用に関するベスト プラクティスを説明します。

再試行を使用して一時的なエラーを処理する

ファンクションは成功するまで継続的に再試行されるため、テストを通じてバグなどの永続的なエラーをコードから除去してから、再試行を有効にしてください。再試行によって解決される可能性が高い断続的なエラーや一時的なエラー(サービス エンドポイントの不安定さやタイムアウトなど)を処理するには、再試行が最適です。

無限再試行ループを避けるための終了条件の設定

再試行を使用する場合は、関数が連続ループに陥らないように保護することをおすすめします。そのためには、明確に定義された終了条件を含めてから関数の処理を開始します。この手法が機能するのは、関数が正常に開始し、終了条件を評価できる場合のみです。

簡単で効果的なアプローチは、特定の時間よりも古いタイムスタンプを持つイベントを破棄することです。これにより、エラーが持続的である場合や継続時間が予想よりも長い場合に、実行時間が過度に長くなるのを回避できます。

たとえば、次のコード スニペットは 10 秒を超えるすべてのイベントを破棄します。

const eventAgeMs = Date.now() - Date.parse(event.timestamp);
const eventMaxAgeMs = 10000;
if (eventAgeMs > eventMaxAgeMs) {
  console.log(`Dropping event ${event} with age[ms]: ${eventAgeMs}`);
  callback();
  return;
}

Promises で catch を使用する

関数の再試行が有効になっている場合、未処理のエラーがあると再試行がトリガーされます。再試行を行わないエラーをコードがキャプチャしていることを確認してください。

対処例を次に示します。

return doFooAsync().catch((err) => {
    if (isFatal(err)) {
        console.error(`Fatal error ${err}`);
    }
    return Promise.reject(err);
});

再試行可能なイベント ドリブン関数をべき等にする

再試行可能なイベント ドリブン関数は、べき等にする必要があります。このような関数をべき等化するための一般的なガイドラインを次に示します。

  • 多くの外部 API(Stripe など)では、べき等のキーをパラメータとして指定できます。このような API を使用している場合は、イベント ID をべき等のキーとして使用します。
  • べき等では再試行が安全に行われるため、at-least-once 配信でうまく機能します。したがって、信頼性の高いコードを書くための一般的なベスト プラクティスは、べき等と再試行を組み合わせることです。
  • コードが内部でべき等であることを確認します。次に例を示します。
    • 結果が変わらずにミューテーションが 2 回以上起こることを確認する。
    • 状態を変更する前にトランザクション内のデータベース状態を照会する。
    • すべての副作用がそれ自体べき等であることを確認する。
  • コードとは関係なく、トランザクション チェックを関数の外側に置く。たとえば、指定されたイベント ID がすでに処理されたことを記録している場所の状態を保持します。
  • 重複した関数呼び出しを帯域外で処理する。たとえば、重複した関数呼び出しの後にクリーンアップする別のクリーンアップ プロセスを用意します。