10

I have observed this behavior occasionally with both onCreate and onDelete triggers. enter image description here

Both the executions happened for the same document created in firestore. There's only one document there so I don't understand how it could trigger the handler twice. the handler itself is very simple:

module.exports = functions.firestore.document('notes/{noteId}').onCreate((event) => {
  const db = admin.firestore();
  const params = event.params;
  const data = event.data.data();
  // empty
});

this doesn't happen all the time. What am I missing?

xaksis
  • 448
  • 5
  • 14
  • what is there inside noteId? – Peter Haddad Feb 11 '18 at 20:09
  • @PeterHaddad Unfortunately, I did not log the params for this particular call. I have added the logs now and will update the questions if this happens again. When it happens for onDelete though, I have actually verified that it triggers onDelete for the same document id twice. – xaksis Feb 11 '18 at 20:14

4 Answers4

8

See the Cloud Firestore Triggers Limitations and Guarantees:

Delivery of function invocations is not currently guaranteed. As the Cloud Firestore and Cloud Functions integration improves, we plan to guarantee "at least once" delivery. However, this may not always be the case during beta. This may also result in multiple invocations for a single event, so for the highest quality functions ensure that the functions are written to be idempotent.

There is a Firecast video with tips for implementing idempotence.

Also two Google Blog posts: the first, the second.

Bob Snyder
  • 37,759
  • 6
  • 111
  • 158
  • Wow, thank you for your response. I can't believe I missed this while reading the docs. Will work on making the functions idempotent. – xaksis Feb 11 '18 at 20:54
  • You can use the `eventId` that firebase passes to your function to help with indepotency as described [here](https://stackoverflow.com/questions/47972550/firebase-cloud-firestore-trigger-idempotency). – abagshaw Feb 21 '18 at 03:51
  • Does it fixed in Version 1.0.0 of the Firebase SDK for Cloud Functions ? – saranpol May 06 '18 at 08:28
  • 1
    @saranpol: I think this issue still exists with version 1.0.0. – Bob Snyder May 06 '18 at 14:24
  • 1
    OK, thank you @BobSnyder I think I will use EventContext.eventId and transaction to fix this issue – saranpol May 06 '18 at 19:28
  • 3
    Am I the only one to find the behaviour completely outrageous ? Does it mean that I need to implement an eventID system on each of my functions that I don't want to be randomly ran twice ? How can Google even expect us to use Firebase in production with such a weird behaviour ? Such a bummer, seriously. – schankam May 15 '19 at 08:41
  • @schankam you're completely right, I'm using firebase as a backend for an inventory tracking system and so can't afford to have the function run multiple times and fuck up the inventory levels. It's just unacceptable – fuadj Jun 29 '20 at 08:48
  • @fuadj After using Cloud Functions for a while now, I have to say that I find my own comment invalid right now. When using Cloud Functions, we just have to keep in mind that functions have to be idempotent, and then build our architecture accordingly. I wasn't really aware of this back then, but if you know it upfront then it's always possible to find a solution. – schankam Jul 01 '20 at 15:39
4

Based on @saranpol's answer we use the below for now. We have yet to check if we actually get any duplicate event ids though.

const alreadyTriggered = eventId => {
  // Firestore doesn't support forward slash in ids and the eventId often has it
  const validEventId = eventId.replace('/', '')

  const firestore = firebase.firestore()
  return firestore.runTransaction(async transaction => {
    const ref = firestore.doc(`eventIds/${validEventId}`)
    const doc = await transaction.get(ref)
    if (doc.exists) {
      console.error(`Already triggered function for event: ${validEventId}`)
      return true
    } else {
      transaction.set(ref, {})
      return false
    }
  })
}

// Usage
if (await alreadyTriggered(context.eventId)) {
  return
}
Simon Bengtsson
  • 7,573
  • 3
  • 58
  • 87
  • The transaction seems like a smart addition to this idea. Maybe depending on how many events you process, it would be good to add a timestamp and clean stuff up older than X days? – Jesse Pangburn Aug 04 '20 at 22:12
  • By replacing the "/' character, won't this introduce possible collisions with other events ids? – Andres C Mar 10 '21 at 18:01
  • Since I assume the IDs are randomly generated I think that is highly unlikely – Simon Bengtsson Mar 11 '21 at 14:15
1

In my case I try to use eventId and transaction to prevent onCreate sometimes triggered twice

(you may need to save eventId in list and check if it exist if your function actually triggered often)

const functions = require('firebase-functions')
const admin = require('firebase-admin')
const db = admin.firestore()
exports = module.exports = functions.firestore.document('...').onCreate((snap, context) => {

  const prize = 1000
  const eventId = context.eventId
  if (!eventId) {
    return false
  }

  // increment money
  const p1 = () => {
    const ref = db.doc('...')
    return db.runTransaction(t => {
        return t.get(ref).then(doc => {
          let money_total = 0
          if (doc.exists) {
            const eventIdLast = doc.data().event_id_last
            if (eventIdLast === eventId) {
              throw 'duplicated event'
            }
            const m0 = doc.data().money_total
            if(m0 !== undefined) {
              money_total = m0 + prize
            }
          } else {
            money_total = prize
          }
          return t.set(ref, { 
            money_total: money_total,
            event_id_last: eventId
          }, {merge: true})
        })
    })
  }

  // will execute p2 p3 p4 if p1 success
  const p2 = () => {
    ...
  }

  const p3 = () => {
    ...
  }

  const p4 = () => {
    ...
  }

  return p1().then(() => {
    return Promise.all([p2(), p3(), p4()])
  }).catch((error) => {
    console.log(error)
  })
})
saranpol
  • 2,177
  • 1
  • 23
  • 22
0

Late to the party, I had this issue but having a min instance solved the issue for me Upon looking @xaxsis attached screenshot, my function took almost the amount of time about 15 seconds for the first request and about 1/4 of that for the second request

Mark Carlton
  • 61
  • 1
  • 2
  • 8