Comments (22)
@laurenzlong If this is expected behavior, it should be reflected in the Firebase Authentication triggers documentation. There is no mention of the possibility of multiple executions, nor of the need for idempotency.
In fact, the document referenced above specifically mentions sending a welcome email as a use case, which is decidedly not an idempotent operation. The Firebase samples repo even includes such an example, which does nothing to account for multiple executions.
This is happening on one of my apps in production, where users are receiving 3 welcome emails. At best, this makes us look bad. Worse, if we get flagged as spam, it could affect our ability to send emails in the future.
If this is not going to be fixed, could you (or someone) please:
- Update the documentation
- Update the example
Thanks
from firebase-functions.
Am I the only one to find this behaviour completely ridiculous ? Does it mean that I need to implement an event system at the beginning of EACH of my functions if I don't want them to randomly be ran more than twice ??? This sounds a bit crazy to me tbh. How can we even use Cloud Function in production with such a weird behaviour.
Plus if I need to write to Firestore just for this specific use case, it's increasing my number of writes to the database, which will increase my bill at the end of the month. What the...
from firebase-functions.
I find also, that onCreate triggers often happen 1, 2, or 3 times.
While it is no doubt possible to create some additional mechanism to attempt to only execute the intended logic once... that certainly seems like the sort of thing the platform should be able to do for us?
from firebase-functions.
Any update from the backend team? The issue is still present and it's very annoyng. This should be fixed.
from firebase-functions.
@erperejildo I don't think this will be ever resolved. I believe that is the price we have to pay for the serverless.
I created a high-order function that runs a transaction on Firestore that checks if a record with given eventId
is present in the DB and if so skip the callback, otherwise it creates the record and runs the callback. I wrap all the events with it, and it works fine. I only wish it could be builtin functionality.
from firebase-functions.
what exactly is the benefit of this "intended behavior"? This seems to cause unnecessary problems for a lot devs using firebase. Can this be re-opened?
from firebase-functions.
Thanks for the report @notsonotso. We've made the backend team aware of this issue. I agree that it is annoying, and should be fixed. In the meanwhile, you can make use of event.eventId to handle the case where there are duplicate invocations, since the duplicate events will have the same eventId. In fact, I would recommend:
admin.database().ref('userqueue/' + event.eventId).set({...})
Closing this since it is not a SDK bug.
from firebase-functions.
Thank you @laurenzlong
- Where am I supposed to submit backend bugs?
- Can this happen on all kinds of events, or just onCreate?
from firebase-functions.
To reiterate again, this is expected behavior. You would need to write your functions in an idempotent way so that they are able to handle cases when they are invoked more than once.
from firebase-functions.
Question, has anyone ever noticed this with an onCall
or onRequest
function? We're trying to make everything idempotent, but these don't appear to have an event ID.
from firebase-functions.
@neilpoulin you can ensure it with transactions, here's the code I use:
import { EventContext } from 'firebase-functions'
import { collection, ref, transaction } from 'typesaurus'
export interface EventClaim {
eventType: string
eventId: string
time: Date
}
export const eventClaims = collection<EventClaim>('eventClaims')
export default function once<EventData>(
fn: (data: EventData, context: EventContext) => Promise<any>
) {
return async (data: EventData, context: EventContext) => {
const { eventId, eventType } = context
const claimRef = ref(eventClaims, eventId)
await transaction(async ({ get, set }) => {
const claimDoc = await get(claimRef)
if (claimDoc) throw new Error('The claim is already exist')
await set(claimRef, {
eventType,
eventId,
time: new Date()
})
})
return fn(data, context)
}
}
from firebase-functions.
It never occurred to me to wrap the check in a transaction. Thanks @kossnocorp !
from firebase-functions.
Well, "consistently". I just got three messages when adding a user.
from firebase-functions.
I'm also experiencing this occasionally with firestore().onCreate(...).
from firebase-functions.
Hi everyone, I was mistaken when I said that this is a bug. Cloud Functions aims to fire at least once, which means it could fire more than once. So you should write your functions in a idempotent way in order to handle the possibility of multiple triggering. I've filed an internal bug to better document this.
from firebase-functions.
https://stackoverflow.com/questions/54541177/firebase-cloud-functions-executed-several-times
from firebase-functions.
I would guess there were technical considerations, and it was decided to opt for triggering "at least once, maybe more" rather than "zero or once". You can't handle an event you never get, but you can deal with an event received multiple times.
from firebase-functions.
Part of the reason of using a framework is to abstract away such technical considerations. There are other minutea that firebase abstracts away for us, why not this?
from firebase-functions.
Still getting this issue after more than a year. Any updates?
from firebase-functions.
We have a couple dozen onCall
and I have not run into this issue.
from firebase-functions.
@kossnocorp Since writes to firestore are async, how can you be sure that your first event wrote to firestore before your second (read: duplicate) event tries to read the event ID? While the approach seems like a great start, it doesn't seem like it will work 100% of the time (unless I'm misunderstanding how the transaction block works). The only way I can figure out how to handle this is to write the function to be idempotent.
from firebase-functions.
@kossnocorp am I thinking about this correctly?
It seems that your once
function will only process the event once, even if the processing fails for some reason. This is not ideal for aggregation. For example, if the event is onDelete
and you want to decrement a total then if the decrement transaction fails for some reason, when it retries, the once
function will stop it and the counter will never be decremented.
Am I correct in thinking this is a plausible scenario (it's hard for me to test as I can't find a way to trigger the same event again)?
Here is some code that I've implemented to ensure the decrement happens if, for some reason, the original events transaction fails.
utils/once.ts
import { EventContext } from 'firebase-functions'
import * as admin from "firebase-admin";
export default function once<EventData>(eventHandler: (data: EventData, context: EventContext) => Promise<any>) {
return async (data: EventData, context: EventContext) => {
await admin.firestore().runTransaction(async (transaction) => {
const eventRef = admin.firestore().doc(`events/${context.eventId}`)
const eventDoc = await transaction.get(eventRef);
if (eventDoc.exists && eventDoc.data()?.success) throw new Error("Event already successfully processed");
transaction.set(eventRef, {eventId: context.eventId, createdAt: admin.firestore.FieldValue.serverTimestamp()});
});
return eventHandler(data, context);
}
}
export function setEventSuccess(transaction: admin.firestore.Transaction, context: EventContext): admin.firestore.Transaction {
const eventRef = admin.firestore().doc(`events/${context.eventId}`);
return transaction.set(eventRef, { success: true, successAt: admin.firestore.FieldValue.serverTimestamp() }, { merge: true });
}
index.ts
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import once, { setEventSuccess } from "./utils/once";
admin.initializeApp();
export const customerDeletedAggregation = functions.firestore
.document("customers/{id}")
.onDelete(once(async (snap, context) => {
// aggregate the deletion within stats/customers.total
const statsRef = admin.firestore().doc('stats/customers');
return admin.firestore().runTransaction(async (transaction) => {
// if this transaction succeeds the events success property will be true
// if it fails for any reason it will still be undefined so the event is still
// processed when retried
const statsDoc = await transaction.get(statsRef);
const data = statsDoc.data();
const total = data?.total || 1; // this is a delete so even if total is not set for some reason, there is at least this one
setEventSuccess(transaction, context)
.set(statsRef, {...data, total: total - 1 }, { merge: true });
});
}));
from firebase-functions.
Related Issues (20)
- Log incorrectly prints [Circular] for object property when it shares reference with another property in the same object HOT 1
- When crating a user, beforeSignIn may be called multiple times with uids not corresponding to firebase users HOT 3
- DataSnapshot.val() throws error for nested child which doesn't exist HOT 3
- Property 'child' in type 'DataSnapshot' is not assignable to the same property in base type 'DataSnapshot'. after updating `firebase-admin` to v12.0.0 HOT 5
- TypeError [ERR_INVALID_CHAR]: Invalid character in header content ["Content-Disposition"] HOT 4
- regression in "Build failed with error TS2416 in firebase-functions" HOT 7
- Bug in database.d.ts HOT 3
- Deploy function with the name test-helloWorld fails when using v2 HOT 6
- Inconsistend behaviour of defineList in deploy/emulator HOT 4
- TypeError: Cannot read properties of null (reading 'seconds') HOT 15
- Firebase Functions Encoding Failure for Self-Referencing Objects Leads to Maximum call stack size exceeded HOT 15
- Function execution took 58984 ms, finished with status: 'timeout' HOT 2
- [Firestore] Error: 4 DEADLINE_EXCEEDED: Deadline exceeded HOT 1
- HTTPS Request returns CORS errors even with the domain added as allowed origin HOT 4
- How to deploy function with latest secrets at RUNTIME? HOT 2
- DocumentOptions do not allow parameterized configuration values HOT 2
- Various Firebase functions started reporting crash: Maximum call stack size exceeded HOT 10
- regression with firebase-functions-test wrapping a v1 scheduled function HOT 2
- Please move `node-fetch` as a `devDependency` HOT 1
- `getDownloadURL` function fails in emulator mode if storage rules are not allowed HOT 3
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from firebase-functions.