結論
Firestore トリガーによるCloud Functionsの無限ループを防ぐために、以下の管理フィールドを導入します。
prevent_trigger_at: timestamp
- フィールドの役割: ドキュメント更新時に、Cloud Functions内で現在時刻をこのフィールドに設定します
-
仕組み: 次回トリガー発動時に、
prevent_trigger_at
の値が前回と異なるかを確認し、一致しない場合は処理をスキップします - ポイント: このフィールドはクライアントから更新せず、サーバー側からのみ更新します
サーバー側(CloudFunction)からのみ、この項目を更新することにより、無限ループを抑止し、トリガー処理の安全性を向上させます。
ここからは余談
みなさん、Firestore トリガーを使っていますか?便利すぎますよね。
私が1年以上関わっているモバイルアプリプロジェクトでは、FirestoreをメインDBとして採用しています。そして、Cloud Functionsを利用したバックエンド処理も多いため、Firestore トリガーは欠かせない存在です。
ところが、開発初期のスキーマ設計が不十分だったため、無限ループに悩まされる事態に陥りました。
コレクションに書き込みが発生すると、トリガー条件と同じコレクションを処理の中で更新して、それが再びトリガーを発動し、さらなる更新を引き起こす…という無限ループです。
初期対応として、トリガー内で「更新内容が前回と同じなら処理をスキップする」というロジックを導入しました。しかし、処理内容が増えるにつれ、スキップ条件が複雑化して破綻。結果、設計全体を見直す必要に迫られました。
そこでprevent_trigger_atという、トリガー実行を判断するフィールドを設けることにしたのです。
現在のスキップするコード(JavaScript)としては以下のようになっています。
/**
* Firestore トリガーのスキップロジック
* - 新規作成時はスキップしない
* - 削除イベントはスキップ
* - prevent_trigger_atが更新されている場合はスキップ
* - 古いイベント(一定時間経過)はスキップ
*/
private isSkip(event: FirestoreEvent<Change<DocumentSnapshot>>): boolean {
const prev = event?.data?.before?.data?.(); // 前の状態
const doc = event?.data?.after?.data?.(); // 現在の状態
if (_.isNil(prev)) return false; // 新規作成イベント
if (_.isNil(doc)) return true; // 削除イベント
// prevent_trigger_atが変更されている場合はスキップ
if (prev.prevent_trigger_at?.toMillis() !== doc.prevent_trigger_at?.toMillis()) {
return true;
}
/**
* 古いイベントの破棄. サーキットブレーカー
* https://cloud.google.com/functions/docs/samples/functions-tips-infinite-retries#functions_tips_infinite_retries-nodejs
*/
const eventAge = Date.now() - Date.parse(event.time);
const eventMaxAge = 10000; // 最大許容時間: 10秒
if (eventAge > eventMaxAge) {
logger.error(`Dropping event ${event} with age ${eventAge} ms.`);
return true;
}
return false; // スキップ条件に該当しない場合は処理を実行
}
この処理がトリガーされた場合の共通処理として実行されるようにしてあり、これにより無限ループを止めることができています。
上記コードには、prevent_trigger_at以外にも共通のチェック項目はいれています。
それではよいFirestoreライフを。