イベント ドリブン関数の再試行を有効にする(第 1 世代)
このドキュメントでは、イベント ドリブン関数の再試行を有効にする方法について説明します。自動再試行は HTTP 関数では使用できません。
再試行のセマンティクス
Cloud Run functions では、イベントソースによって生成された各イベントにつき、最低 1 回はイベント ドリブン関数が必ず実行されることになっています。デフォルトでは、関数の呼び出しがエラーによって終了した場合、その関数が再度呼び出されることはなく、そのイベントはドロップします。イベント ドリブン関数の再試行を有効にすると、Cloud Run functions で失敗した関数の呼び出しが正常に完了するか、再試行期間が終了するまで再試行が行われます。
この再試行期間は 7 日後に終了します。Cloud Run functions は、指数バックオフ戦略を使用して、新しく作成されたイベント ドリブン関数を再試行します。バックオフ時間は 10~600 秒の間で増加します。このポリシーは、新しい関数を初めてデプロイすると適用されます。リリースノートに記載されている変更が有効になる前に最初にデプロイされた既存の関数については、たとえ再デプロイしても、遡って適用されることはありません
関数の再試行がデフォルトで有効になっていない場合、再試行は正常に実行されたと関数により常に報告され、200 OK
レスポンス コードがログに記録されます。これは、関数でエラーが発生した場合でも同様です。関数でエラーが発生したことを明確にするために、適切にエラーを報告してください。
イベント ドリブン関数が完了しない理由
まれに、内部エラーのため関数が早期に終了することがあります。関数は、デフォルトで自動的に再試行される場合もあれば、再試行されない場合もあります。
よくあるのは、関数コード内でエラーがスローされるためにイベント ドリブン関数が正常に完了できないケースです。これには、次のような理由が考えられます。
- 関数にバグがあるため、ランタイムから例外がスローされる。
- 関数がサービス エンドポイントに到達できないか、エンドポイントに到達しようとしている間にタイムアウトする。
- 関数から意図的に例外がスローされる(たとえば、パラメータの検証で不合格だった場合)。
- Node.js 関数は、拒否された Promise を返すか、コールバックに
null
以外の値を渡します。
いずれの場合も、関数はデフォルトで実行を停止し、イベントが破棄されます。エラーが発生したときに関数を再試行するには、「失敗時に再試行する」プロパティを設定して、デフォルトの再試行ポリシーを変更できます。これにより、関数が正常に完了するか、再試行タイムアウトになるまで、イベントが繰り返し再試行されます。
再試行を有効または無効にする
再試行を有効または無効にするには、gcloud
コマンドライン ツールまたは Google Cloud コンソールを使用します。デフォルトでは、再試行は無効になっています。
gcloud
コマンドライン ツールから再試行を構成する
gcloud
コマンドライン ツールを使用して再試行を有効にするには、関数をデプロイするときに、--retry
フラグを指定します。
gcloud functions deploy FUNCTION_NAME --retry FLAGS...
再試行を無効にするには、--retry
フラグを指定せずに関数を再デプロイします。
gcloud functions deploy FUNCTION_NAME FLAGS...
コンソールから再試行を構成する
新しい関数を作成する場合:
- [関数を作成] 画面で [トリガーを追加] を選択して、関数のトリガーとして機能するイベントのタイプを選択します。
- [失敗時に再試行する] チェックボックスをオンにして、再試行を有効にします。
既存の関数を更新する場合:
- Cloud Run functions の概要ページで、更新する関数の名前をクリックして [ファンクションの詳細] 画面を開き、メニューバーで [編集] を選択して [トリガー] ペインを表示します。
- [失敗時に再試行する] チェックボックスをオンまたはオフにして、再試行を有効または無効にします。
ベスト プラクティス
ここでは、再試行の使用に関するベスト プラクティスを説明します。
再試行を使用して一時的なエラーを処理する
関数は成功するまで継続的に再試行されるため、テストを通じてバグなどの永続的なエラーをコードから除去してから、再試行を有効にしてください。再試行によって解決される可能性が高い断続的なエラーや一時的なエラー(サービス エンドポイントの不安定さやタイムアウトなど)を処理するには、再試行が最適です。
無限再試行ループを避けるための終了条件の設定
再試行を使用する場合は、関数が連続ループに陥らないように保護することをおすすめします。そのためには、明確に定義された終了条件を含めてから関数の処理を開始します。この手法が機能するのは、関数が正常に開始し、終了条件を評価できる場合のみです。
簡単で効果的なアプローチは、特定の時間よりも古いタイムスタンプを持つイベントを破棄することです。これにより、エラーが持続的である場合や継続時間が予想よりも長い場合に、実行時間が過度に長くなるのを回避できます。
たとえば、次のコード スニペットは 10 秒を超えるすべてのイベントを破棄します。
Node.js
Python
Go
Java
C#
using CloudNative.CloudEvents; using Google.Cloud.Functions.Framework; using Google.Events.Protobuf.Cloud.PubSub.V1; using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Tasks; namespace TimeBoundedRetries; public class Function : ICloudEventFunction<MessagePublishedData> { private static readonly TimeSpan MaxEventAge = TimeSpan.FromSeconds(10); private readonly ILogger _logger; // Note: for additional testability, use an injectable clock abstraction. public Function(ILogger<Function> logger) => _logger = logger; public Task HandleAsync(CloudEvent cloudEvent, MessagePublishedData data, CancellationToken cancellationToken) { string textData = data.Message.TextData; DateTimeOffset utcNow = DateTimeOffset.UtcNow; // Every PubSub CloudEvent will contain a timestamp. DateTimeOffset timestamp = cloudEvent.Time.Value; DateTimeOffset expiry = timestamp + MaxEventAge; // Ignore events that are too old. if (utcNow > expiry) { _logger.LogInformation("Dropping PubSub message '{text}'", textData); return Task.CompletedTask; } // Process events that are recent enough. // If this processing throws an exception, the message will be retried until either // processing succeeds or the event becomes too old and is dropped by the code above. _logger.LogInformation("Processing PubSub message '{text}'", textData); return Task.CompletedTask; } }
Ruby
PHP
/** * This function shows an example method for avoiding infinite retries in * Google Cloud Functions. By default, functions configured to automatically * retry execution on failure will be retried indefinitely - causing an * infinite loop. To avoid this, we stop retrying executions (by not throwing * exceptions) for any events that are older than a predefined threshold. */ use Google\CloudFunctions\CloudEvent; function avoidInfiniteRetries(CloudEvent $event): void { $log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb'); $eventId = $event->getId(); // The maximum age of events to process. $maxAge = 10; // 10 seconds // The age of the event being processed. $eventAge = time() - strtotime($event->getTime()); // Ignore events that are too old if ($eventAge > $maxAge) { fwrite($log, 'Dropping event ' . $eventId . ' with age ' . $eventAge . ' seconds' . PHP_EOL); return; } // Do what the function is supposed to do fwrite($log, 'Processing event: ' . $eventId . ' with age ' . $eventAge . ' seconds' . PHP_EOL); // infinite_retries failed function executions $failed = true; if ($failed) { throw new Exception('Event ' . $eventId . ' failed; retrying...'); } }
再試行可能な関数と致命的なエラーを区別する
関数の再試行が有効になっている場合、未処理のエラーがあると再試行がトリガーされます。再試行を行わないエラーをコードがキャプチャしていることを確認してください。
Node.js
Python
Go
Java
C#
using CloudNative.CloudEvents; using Google.Cloud.Functions.Framework; using Google.Events.Protobuf.Cloud.PubSub.V1; using Microsoft.Extensions.Logging; using System; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace Retry; public class Function : ICloudEventFunction<MessagePublishedData> { private readonly ILogger _logger; public Function(ILogger<Function> logger) => _logger = logger; public Task HandleAsync(CloudEvent cloudEvent, MessagePublishedData data, CancellationToken cancellationToken) { bool retry = false; string text = data.Message?.TextData; // Get the value of the "retry" JSON parameter, if one exists. if (!string.IsNullOrEmpty(text)) { JsonElement element = JsonSerializer.Deserialize<JsonElement>(data.Message.TextData); retry = element.TryGetProperty("retry", out var property) && property.ValueKind == JsonValueKind.True; } // Throwing an exception causes the execution to be retried. if (retry) { throw new InvalidOperationException("Retrying..."); } else { _logger.LogInformation("Not retrying..."); } return Task.CompletedTask; } }
Ruby
PHP
use Google\CloudFunctions\CloudEvent; function tipsRetry(CloudEvent $event): void { $cloudEventData = $event->getData(); $pubSubData = $cloudEventData['message']['data']; $json = json_decode(base64_decode($pubSubData), true); // Determine whether to retry the invocation based on a parameter $tryAgain = $json['some_parameter']; if ($tryAgain) { /** * Functions with automatic retries enabled should throw exceptions to * indicate intermittent failures that a retry might fix. In this * case, a thrown exception will cause the original function * invocation to be re-sent. */ throw new Exception('Intermittent failure occurred; retrying...'); } /** * If a function with retries enabled encounters a non-retriable * failure, it should return *without* throwing an exception. */ $log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb'); fwrite($log, 'Not retrying' . PHP_EOL); }
再試行可能なイベント ドリブン関数をべき等にする
再試行可能なイベント ドリブン関数は、べき等にする必要があります。このような関数をべき等化するための一般的なガイドラインを次に示します。
- 多くの外部 API(Stripe など)では、べき等のキーをパラメータとして指定できます。このような API を使用している場合は、イベント ID をべき等のキーとして使用します。
- べき等では再試行が安全に行われるため、at-least-once 配信でうまく機能します。したがって、信頼性の高いコードを書くための一般的なベスト プラクティスは、べき等と再試行を組み合わせることです。
- コードが内部でべき等であることを確認します。次に例を示します。
- 結果が変わらずにミューテーションが 2 回以上起こることを確認する。
- 状態を変更する前にトランザクション内のデータベース状態を照会する。
- すべての副作用がそれ自体べき等であることを確認する。
- コードとは関係なく、トランザクション チェックを関数の外側に置く。たとえば、指定されたイベント ID がすでに処理されたことを記録している場所の状態を保持します。
- 重複した関数呼び出しを帯域外で処理する。たとえば、重複した関数呼び出しの後にクリーンアップする別のクリーンアップ プロセスを用意します。
再試行ポリシーを構成する
関数のニーズに応じて、再試行ポリシーを直接構成することもできます。以下を任意の組み合わせで設定できます。
- 再試行期間を 7 日間から 10 分に短縮する。
- 指数バックオフの再試行方法の最小および最大バックオフ時間を変更する。
- すぐに再試行できるように再試行方法を変更する。
- デッドレター トピックを構成する。
- 配信の試行回数の最大値と最小値を設定する。
再試行ポリシーを構成するには:
- HTTP 関数を記述します。
- Pub/Sub API を使用して Pub/Sub サブスクリプションを作成し、関数の URL をターゲットとして指定します。
Pub/Sub の直接構成の詳細については、失敗の処理に関する Pub/Sub のドキュメントをご覧ください。
次のステップ
- Cloud Run functions のデプロイ。
- Pub/Sub トリガー関数の呼び出し。
- Cloud Storage トリガー関数の呼び出し。
- Cloud Run functions と Pub/Sub のチュートリアル。
- Cloud Run functions と Cloud Storage のチュートリアル。