Help us understand the problem. What is going on with this article?

Firebase Cloud Function と Firebase Firestore を組み合わせて使うと onCreate/onDelete がたまに2回発火してしまう

More than 1 year has passed since last update.

Firebase Cloud Function と Firebase Firestore を組み合わせて使うと onCreate/onDelete がたまに2回発火してしまうという問題に遭遇しました。

stack overflow にも丁度同じ問題で困っている人がいるようです。
https://stackoverflow.com/questions/48735862/firestore-cloud-functions-oncreate-ondelete-sometimes-immediately-triggered-twic

ドキュメントの制約と保証の項に書いてある通り、1つのイベントで複数の呼び出しがある可能性があるそうです。

現在、関数呼び出しの配信は保証されていません。Cloud Firestore と Cloud Functions の統合が向上した段階で、少なくとも 1 回の配信を保証する予定です。ただし、ベータ版ではこれが保証されるとは限りません。1 つのイベントで複数の呼び出しが発生する可能性があるため、関数で高い精度が求められる場合には、関数の書き込みをべき等にする必要があります。

私のケースでは、ポストが投稿された際プッシュ通知を送信する関数を運用していて、これが原因で1つのポスト投稿で2重の通知が送られることが頻繁に発生しています。

関数の書き込みを冪等にする必要がある

とあったので eventId をグローバル変数に保存しておき、すでに発火したイベントは無視するコードを追加してみました。

var alreadyRunEventIDs = [];

function isAlreadyRunning(eventID) {
  return alreadyRunEventIDs.indexOf(eventID) >= 0;
}

function markAsRunning(eventID) {
  alreadyRunEventIDs.push(eventID);
}

exports.notifyNewPost = functions.firestore.document('path').onCreate(event => {
  const eventID = event.eventId;
  if (util.isAlreadyRunning(eventID)) {
    return console.log('Ignore it because it is already running (eventId):', eventID);
  }
  markAsRunning(eventID);
  ...
}

これがうまくいくか試しています。

追記:

try! swift tokyo 2018 で firebase team の方がブースを出されていたので、この問題に対するベストプラクティスがあるか聞いてみたのですが、今の所はないとのことでした。チームに共有していいやり方がわかったら連絡してくれるとのことです。
上記のグローバル変数を使った実装はクラウドファンクションの複数のインスタンス毎に保持されるのでこのやり方だと防げないそうです。ログを確認しても防げている時と防げていない時があったので、防げている時はたまたま同じインスタンスで実行されたのでしょう。
また、上記の他にトランザクションを使って防ぐ実装についても聞いたところ、トランザクションは複数回リトライする可能性があるのでその場合防げないかもしれない、とのことでした。。。

追記:

関数がトリガーされた際、firestore にイベントIDを保存しておき、それをチェックする方法を試していますが、今の所うまく動いています。

export async function wasTriggered(eventId: string) {
  return firestore.runTransaction(async t => {
    const ref = eventIdRef
    const doc = await t.get(ref)
    if (doc.exists) {
      return true
    } else {
      t.set(ref, {})
      return false
    }
  })
}

export const listener = functions.firestore.document(`path`).onCreate(async (snapshot, context) => {
  if (await utils.wasTriggered(context.eventId)) {
    console.log('Ignore: it has already been triggered')
    return 0
  }
  ...
}

nkmrh
iOS / Swift / Firebase / Flutter
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした