Too much delay in the completion of the cloud function - firebase

This is the cloud function that I'm using to send notification to the shopkeepers when an order is accepted by the shipper. But sometimes it takes at least 20 seconds to complete and more often take more than 3 minutes. My other cloud functions are working completely fine. Can't figure out the issue with this function.
exports.onChangeOfOrderStatus = functions.firestore
.document('orders/{documentId}')
.onUpdate(async (change, context) => {
// Get an object with the current document value.
// If the document does not exist, it has been deleted.
const document = change.after.exists ? change.after.data() : null;
// Get an object with the previous document value (for update or delete)
const oldDocument = change.before.data();
const newDocument = change.after.data();
const oldStatus = oldDocument.orderStatus;
const newStatus = newDocument.orderStatus;
functions.logger.log(oldStatus);
functions.logger.log('TO');
functions.logger.log(newStatus);
let orderPassed = false;
let shopsIds = [];
Array.prototype.push.apply(shopsIds, newDocument.shopsWhoGotOrders);
functions.logger.log("Printing shopIds 1st time");
shopsIds = getUnique(shopsIds);
printArray(shopsIds); //Code works fine and instantly at this point of line
let shopTokensAre = [];
if (oldStatus == 'prepending' && newStatus == 'pending') {
shopsIds.forEach(async result => {
await admin.firestore().collection("users")
.where('role', "==", 'shopkeeper')
.where('uid', '==', result)
.get().then(snapshot => {
snapshot.forEach(async doc => {
shopTokensAre.push(doc.data().token);
functions.logger.log("Printing shopIds: 2nd time"); // This line
//takes time to print
functions.logger.log(doc.data().token);
await admin.messaging().send({
token: doc.data().token,
notification: {
title: "Hi TELLOO Shopkeeper",
body: 'You received a new order, Please Accept/Reject it fastly',
imageUrl: 'https://support.kraken.com/hc/article_attachments/360085484571/ProApp_NewOrderButton_02082021.png',
}
})
.then(snapshot => {
functions.logger.log("Notifications sent");
});
});
});
});
}
});

I suggest reviewing the Tips & Tricks guide for Cloud Functions to check the recommendations and avoid issues when using Cloud Functions.
Several of the recommendations in this document center on what is known as a cold start. Functions are stateless, and the execution environment is often initialized from scratch, which is called a cold start.
From the issue you're describing, it is most likely that it could be a cold start issue. You could check the minimum instances configured for your function.
By default, Cloud Functions scales the number of instances based on the number of incoming requests. You can change this default behavior by setting a minimum number of instances that Cloud Functions must keep ready to serve requests. Setting a minimum number of instances reduces cold starts of your application.
You can set a minimum instance limit for existing functions, by following these steps:
Go to the Cloud Functions page in the Google Cloud Console:
Go to the Cloud Functions page
Click the name of an existing function to be taken to its Function details page.
Click Edit.
Click Runtime, build, and connection settings to expand additional options.
In the Minimum instances field in the Autoscaling section, enter a number greater than or equal to 1.
Click Next.
Click Deploy.
Additionally, you could check the dependencies you use in your function:
Because functions are stateless, the execution environment is often initialized from scratch (during what is known as a cold start). When a cold start occurs, the global context of the function is evaluated.
If your functions import modules, the load time for those modules can add to the invocation latency during a cold start. You can reduce this latency and the time needed to deploy your function, by loading dependencies correctly and not loading dependencies your function doesn't use.
See also:
Minimizing cold start time (Firecasts)

You may see better performance avoiding the async forEach function. I've observed a similar slowness to what you've described when a Cloud Function gets overloaded.
exports.onChangeOfOrderStatus = functions.firestore
.document('orders/{documentId}')
.onUpdate(async (change, context) => {
const oldDocument = change.before.data();
const newDocument = change.after.data();
const oldStatus = oldDocument.orderStatus;
const newStatus = newDocument.orderStatus;
if (oldStatus !== 'prepending' || newStatus !== 'pending') {
return;
}
let shopsIds = [];
Array.prototype.push.apply(shopsIds, newDocument.shopsWhoGotOrders);
shopsIds = getUnique(shopsIds);
const messagingPromises = [];
for (const shopsId of shopsIds) {
const querySnapshot = await admin
.firestore()
.collection('users')
.where('role', '==', 'shopkeeper')
.where('uid', '==', shopsId)
.get();
querySnapshot.forEach((doc) => {
const messagingPromise = admin.messaging().send({
token: doc.data().token,
notification: {
title: 'Hi TELLOO Shopkeeper',
body: 'You received a new order, Please Accept/Reject it fastly',
imageUrl:
'https://support.kraken.com/hc/article_attachments/360085484571/ProApp_NewOrderButton_02082021.png',
},
});
messagingPromises.push(messagingPromise);
});
}
await Promise.all(messagingPromises);
});

As I see, your code it runs well until you have to query the data and do some process on it.
Nested .forEach are making your code slower, so it could be a good idea to change them with for() loops, as pointed here.
Array.ForEach is about 95% slower than for()
Also, you should use Javascript promises to terminate your function properly, as stated in the official documentation:
By terminating functions correctly, you can avoid excessive charges from functions that run for too long or loop infinitely.
...
Use these recommended approaches to manage the lifecycle of your functions:
Resolve functions that perform asynchronous processing (also known as "background functions") by returning a JavaScript promise
See also:
Why is my Cloud Firestore query slow?
The Firebase Blog: Keeping our Promises (and Callbacks)

Related

Is return value important in Firebase Cloud Functions

I am writing the Firebase Could Functions with TypeScript and the following is a simple method to update a document.
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp(functions.config().firebase);
export const handleTestData = functions.firestore.document('test/{docID}').onCreate(async (snap, context) => {
const data = snap.data();
if (data) {
try {
await admin.firestore().doc('test1/' + context.params.docID + '/').update({duplicate : true});
} catch (error) {}
}
});
In this method, the promise is handled by async await and there is no return statement and it's working fine. Most of the examples/tutorials I have seen always have a return statement in each method.
Is there any impact/difference I don't return anything in Firebase Cloud Functions? If I should return something, can I return null?
Is return value important in Firebase Cloud Functions?
Yes, it is really key, in a Cloud Function which performs asynchronous processing (also known as "background functions") to return a JavaScript promise when all the asynchronous processing is complete, as explained in the documentation.
Doing so is important for two main reasons (excerpts from the doc):
You make sure that the Cloud Functions instance running your Cloud Function does not shut down before your function successfully reaches its terminating condition or state.
You can avoid excessive charges from Cloud Functions that run for too long or loop infinitely.
Why is your Cloud Function running correctly even if you don't return a Promise?
Normally your Cloud Function should be terminated before the asynchronous operations are completed, because you don't return a Promise and therefore indicate to the Cloud Functions platform that it can terminate the Cloud Functions instance running the Cloud Function.
But sometimes, the Cloud Functions platform does not terminate the Function immediately and the asynchronous operations can be completed. This is not at all guaranteed and totally out of your control.
Experience has shown that for short asynchronous operations this last case happens quite often and the developer thinks that everything is ok. But, all of sudden, one day, the Cloud Function does not work... and sometimes it does work: The developer is facing an "erratic" behaviour without any clear logic, making things very difficult to debug. You will find a lot of questions in Stack Overflow that illustrate this situation.
So concretely, in your case you can adapt your code like:
export const handleTestData = functions.firestore.document('test/{docID}').onCreate(async (snap, context) => {
const data = snap.data();
if (data) {
try {
// See the return below: we return the Promise returned by update()
return admin.firestore().doc('test1/' + context.params.docID + '/').update({duplicate : true});
} catch (error) {
return null; // <- See the return
}
} else {
return null; // <- See the return
}
});
or like
export const handleTestData = functions.firestore.document('test/{docID}').onCreate(async (snap, context) => {
const data = snap.data();
if (data) {
try {
await admin.firestore().doc('test1/' + context.params.docID + '/').update({duplicate : true});
return null; // <- See the return
} catch (error) {
return null; // <- See the return
}
} else {
return null; // <- See the return
}
});
Returning null (or true, or 1...) is valid since an async function always returns a Promise.

What happens to async calls in completed firebase functions?

If I choose not to wait for an async call to complete inside a firebase function, what happens to the async call when the function completes? On what thread does it continue, does it still run within the function's runtimeOpts and how does this affect usage?
import * as firebase from 'firebase';
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
exports.action = functions.https.onRequest((req, res) => {
admin.database().ref('action').add({ 1: 2 });
res.end(); // or just `return;` for other functions
});
Or maybe even
exports.action2 = functions.https.onRequest((req, res) => {
new Promise(resolve => setTimeout(() => admin.database().ref('action').add({ 1: 2 })}, 10_000));
res.end();
});
Have a look at this section of the documentation on Cloud Functions, which explains why it is "important to manage the lifecycle of a function to ensure that it resolves properly".
If you want to be sure that "the Cloud Functions instance running your Cloud Function (CF) does not shut down before your CF successfully reaches its terminating condition or state" (in your case, when the asynchronous work is done), you need to "resolve your CF by returning a JavaScript promise".
So, if you don't do that, there is the risk that the Cloud Functions instance running your CF shuts down before your calls to one or more async functions complete.
However, it does NOT mean that this will always happen. It may be very well possible that the Cloud Functions instance running your CF continues to run after you sent back the response to the client (with res.end();) resulting in the async work being completed.
The problem is that this is totally out of your control (it depends on how the Cloud Functions platform manages you Cloud Function instances): sometimes it may continue running (completing the async work), sometimes it may not.
You will find a lot of questions on Cloud Functions on Stack Overflow which explain that their problem is that the Cloud Function sometimes executes the async work correctly and sometimes not: the reason is simply because they don't return the Promises returned by the async calls.
Does this affect usage?
Yes, as clearly said in the doc: "By terminating functions correctly, you can avoid excessive charges from functions that run for too long or loop infinitely."
So, concretely, in your case you should do something like:
exports.action = functions.https.onRequest(async (req, res) => { // <-- Note the async keyword here
await admin.database().ref('action').add({ 1: 2 });
res.end(); // or just `return;` for other functions
});
Note that if your Cloud Function is synchronous (it does not call any asynchronous method) you have to terminate it with a simple return; statement (or by calling send(), redirect(), or end() for an HTTPS Cloud Function). For example:
exports.action = functions.https.onRequest((req, res) => {
const result = 1 + 3;
res.status(200).send(result);
});
or
exports.unusefulCloudFunction = functions.firestore
.document('users/{userId}').onWrite((change, context) => {
const result = 1 + 3;
console.log(result);
return;
});

How to load 2 different Firestore docs in one 'onUpdate' Cloud Function?

I am trying to make an "onUpdate" function that loads the document that has been updated. Then I want to load another document using the data received by the wildcards. So to summarize I want to access the document that was updated and one more that is in the same collection.
I want : /userProfiles/{doc1}/employees/{doc2} AND /userProfiles/{doc1}.
I can get them both but when I try to use the data from one, it doesn't read the previous data and gives me a ReferenceError.
The end goal is to use both these docs to send an email with nodemailer. Thanks for any help.
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const nodemailer = require('nodemailer');
admin.initializeApp();
exports.testLog = functions.firestore
.document('/userProfiles/{doc1}/employees/{doc2}')
.onUpdate((change, context) => {
var info = [];
const doc1 = context.params.doc1;
const doc2 = context.params.doc2;
const db = admin.firestore();
return (
db
.collection("userProfiles")
.doc(`${doc1}`)
.get()
.then(doc => {
var email = doc.data().email;
var phone = doc.data().phone;
info.push(doc.data());
console.log(email, phone); // sees and gets info
return email, phone;
}),
db
.collection("userProfiles")
.doc(`${doc1}`)
.collection(`employees`)
.doc(`${doc2}`)
.get()
.then(doc => {
info.push(doc.data());
var Status = doc.data().Status;
console.log(phone, `${Status}`); //phone is undefined
if (`${Status}` === "Alarm") {
// replace with variables from the users settings page
console.log(`${info.phone}`); // phone is undefined
let transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 587,
secure: false,
auth: {
user: "xxxxxx#gmail.com",
pass: "xxxxxxxxxx"
}
});
// send mail with defined transport object
let mailOptions = {
from: '"Fred Foo 👻" <foo#example.com>',
to: `${info.phone}`, // tried phone as well
subject: "Hello ✔",
text: "216+?",
};
transporter.sendMail(mailOptions, error => {
if (error) {
return console.log(error);
} else {
return console.log("message sent");
}
});
}
console.log(Status);
// return
return console.log("im after the if statement. No alarm triggered");
})
.then(message => console.log(message.sid, "success"))
.catch(err => console.log(err))
);
});
So I want to get the phone number and the Status in these 2 images
The error that is returned:
ReferenceError: phone is not defined
There are two things that aren't quite working the way you expect leading to your problem:
The handling of promises isn't really passing data the way you expect -- in particular, the variables phone and email exist only in one promise handler, they aren't global in scope, so phone and email aren't being passed down the promise chain.
You don't actually need to ever read the second document, as the content is passed to you in the function itself. This actually greatly simplifies the overall thing you are doing, and makes dealing with the first point nearly trivial, since you can skip the second database call.
Look at this code where I have omitted the messaging code for clarity and just left in place most of the log messages:
exports.firestoreOnUpdateTest = functions.firestore
.document('/userProfiles/{doc1}/employees/{doc2}')
.onUpdate((change, context) => {
// var info = []; I have removed this list, it is not necessary
const doc1 = context.params.doc1;
// no need to get the doc2 parameter, as we are handed the doc itself by the function call.
const doc2content = change.after.data();
const db = admin.firestore();
return (
db
.collection("userProfiles")
.doc(`${doc1}`)
.get()
.then(doc => {
const doc1content = doc.data();
const email = doc1content.email;
const phone = doc1content.phone;
console.log(email, phone); // sees and gets info
console.log(`No need to fetch doc2, as I already have it: ${JSON.stringify(doc2content)}`);
const Status = doc2content.Status;
console.log(`email for user is still: ${email}`); // email is now defined
console.log(phone, `${Status}`); // phone is now defined
if (`${Status}` === "Alarm") {
console.log(`${phone}`); // phone is now defined
return console.log('message would be sent here - code omitted')
}
console.log(Status);
return console.log("im after the if statement. No alarm triggered");
})
.catch(err => console.error(err))
);
});
In the new version, we just store the content from the document that triggered us, including the Status parameter. We then fetch the document with the content we need -- at the higher level in the tree. Once that document is returned, we just process it and combine with the data from doc2. All the fields are now defined (assuming, of course, the database objects are well-formed).
Your messaging code would be re-inserted right were the obvious log message is.
Finally, the info list I don't think is necessary now, so I've removed it. Instead, I recommend you build what you need as you construct the message itself from the data already on hand. That said, your original code wasn't accessing it correctly (that is, as a list) anyway and may have been confusing you further.
Finally, I haven't addressed the use of the Nodemailer module as the question focused primarily on the undefined fields, but I suspect your original code may not be entirely correct either -- as it doesn't either return a promise back from sendMail() or perform an await on that call (and make the entire function async), so you will need to look at that more closely.

How to trigger function when date stored in firestore database is todays date?

I am creating an app where I need to send push notification when today's date matches with the date stored in database in order to send push notification.
How to achieve this?
Update:
You can use a scheduled Cloud Function, instead of writing an HTTPS Cloud Function that is called via n online CRON Job service. The Cloud Function code stays exactly the same, just the trigger changes.
Scheduled Cloud Functions were not available at the time of writing the initial anwser.
Without knowing your data model it is difficult to give a precise answer, but let's imagine, to simplify, that you store in each document a field named notifDate with format DDMMYYY and that those documents are store in a Collection named notificationTriggers.
You could write an HTTPS Cloud Function as follows:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const cors = require('cors')({ origin: true });
const moment = require('moment');
admin.initializeApp();
exports.sendDailyNotifications = functions.https.onRequest((request, response) => {
cors(request, response, () => {
const now = moment();
const dateFormatted = now.format('DDMMYYYY');
admin.firestore()
.collection("notificationTriggers").where("notifDate", "==", dateFormatted)
.get()
.then(function(querySnapshot) {
const promises = [];
querySnapshot.forEach(doc => {
const tokenId = doc.data().tokenId; //Assumption: the tokenId is in the doc
const notificationContent = {
notification: {
title: "...",
body: "...", //maybe use some data from the doc, e.g doc.data().notificationContent
icon: "default",
sound : "default"
}
};
promises
.push(admin.messaging().sendToDevice(tokenId, notificationContent));
});
return Promise.all(promises);
})
.then(results => {
response.send(data)
})
.catch(error => {
console.log(error)
response.status(500).send(error)
});
});
});
You would then call this Cloud Function every day with an online CRON job service like https://cron-job.org/en/.
For more examples on how to send notifications in Cloud Functions, have a look at those SO answers Sending push notification using cloud function when a new node is added in firebase realtime database?, node.js firebase deploy error or Firebase: Cloud Firestore trigger not working for FCM.
If you are not familiar with the use of Promises in Cloud Functions I would suggest you watch the 3 videos about "JavaScript Promises" from the Firebase video series: https://firebase.google.com/docs/functions/video-series/
You will note the use of Promise.all() in the above code, since you are executing several asynchronous tasks (sendToDevice() method) in parallel. This is detailed in the third video mentioned above.
Use Google Cloud Functions Scheduled Triggers
https://cloud.google.com/scheduler/docs/tut-pub-sub
Using a scheduled trigger you can specify how many times to invoke your function by specifying the frequency using the unix-cron format. Then within the function you can do date check and other needed logic

"TypeError: functions.firestore.collection is not a function"

Looking through the Firestore documentation, I see many examples of functions.firestore.document but I don't see any examples of functions.firestore.collection. Firestore syntax is
firebase.firestore().collection('...').doc('...')
I get an error message with
firebase.firestore().document('...')
Yet in Cloud Functions with this code:
exports.myFunction = functions.firestore.collection('...').doc('...').onUpdate(event => {
on deploy I get an error message:
TypeError: functions.firestore.collection is not a function
When I change the code to
exports.getWatsonTokenFirestore = functions.firestore.document('...').onUpdate(event => {
I don't get an error message on deploy.
Why does Cloud Functions appear to have a different data structure than Cloud Firestore?
Here's my full Cloud Function. My collection is User_Login_Event and my document is Toggle_Value:
exports.getWatsonTokenFS = functions.firestore.document('User_Login_Event/{Toggle_Value}').onUpdate(event => {
var username = 'TDK',
password = 'swordfish',
url = 'https://' + username + ':' + password + '#stream.watsonplatform.net/authorization/api/v1/token?url=https://stream.watsonplatform.net/speech-to-text/api';
request({url: url}, function (error, response, body) {
admin.firestore().collection('IBM_Watson_Token').document('Token_Value').update('token');
});
return 0; // prevents an error message "Function returned undefined, expected Promise or value"
});
The function deploys without error but when it executes I get this error message:
TypeError: firebase.firestore is not a function
I'm confused as firebase.firestore isn't in my Cloud Function. It's in my Angular front-end code in various places, without a problem. What is this error message referring to? I tried changing the line
admin.firestore().collection('IBM_Watson_Token').document('Token_Value').update('token');
to
firebase.firestore().collection('IBM_Watson_Token').document('Token_Value').update('token');
and to
console.log("getWatsonTokenFS response");
but I got the same error message.
Yes. You should format it as...
exports.getWatsonTokenFirestore = functions.firestore.document('myCollection/{documentId}').onUpdate(event => {
// code
});
collection and doc are methods within firebase.firestore. To access them via functions.firestore, you must use document.
You can see a full list of Classes for Cloud Firestore and the latest SDK for Cloud Functions for Firebase
Update
I've been working on your code. I've added in all of the dependencies and initialization, which I assume that you have in your code. I can't see where you're using any data from Firestore in your IBM Watson request and I can't see how you're writing any of the returned data back to Firestore. As I'm not familiar with your request method, I've commented it out, to give you what should be a working example of an update to Firestore and writes something back. I also edited some of your code to make it more readable and changed the Cloud Functions code to reflect v1.0.0, released today (I've been testing it for a while) :)
const admin = require('firebase-admin');
const functions = require('firebase-functions');
admin.initializeApp();
const firestore = admin.firestore();
exports.getWatsonTokenFS = functions.firestore
.document('User_Login_Event/{Toggle_Value}')
.onUpdate((snap, context) => {
let username = 'TDK';
let password = 'swordfish';
let url = `https://${username}:${password}#stream.watsonplatform.net/authorization/api/v1/token?url=https://stream.watsonplatform.net/speech-to-text/api`;
// request({url}, function (error, response, body) {
// firestore.doc(`${IBM_Watson_Token}/${Token_Value}`).update('token');
// });
return firestore.doc(`IBM_Watson_Token/Token_Value`).update('token')
.then(response => {
return Promise.resolve();
})
.catch(err => {
return Promise.reject(err);
});
});
Now that Firebase has updated firebase-admin to 5.12.0 and firebase-functions to 1.0.1 my test function is working. The function that Jason Berryman wrote is correct except for two lines. Jason wrote:
.onUpdate((snap, context) => {
That should be
.onUpdate((change, context) => {
Secondly, Jason wrote:
return firestore.doc(`IBM_Watson_Token/Token_Value`).update('token')
The corrected line is:
return firestore.collection('IBM_Watson_Token').doc('Token_Value').update({
token: 'newToken'
})
I made two changes in Jason's code. First, I changed the location syntax; more on this below. Second, update() requires an object as the argument.
To show the syntax for locations, I wrote a simple Cloud Function that triggers when a value at a location in Cloud Firestore changes, and then writes a new value to a different location in Cloud Firestore. I removed the line const firestore = admin.firestore(); to make the code more clear:
const admin = require('firebase-admin');
const functions = require('firebase-functions');
admin.initializeApp();
exports.testFunction = functions.firestore.document('triggerCollection/{documentID}').onUpdate((change, context) => {
return admin.firestore().collection('writeCollection').doc('documentID').update({
token: 'newValue'
})
.then(response => {
return Promise.resolve();
})
.catch(err => {
return Promise.reject(err);
});
});
Let's compare three syntaxes for locations in Cloud Firestore. First, in the browser I use this syntax:
firebase.firestore().collection('myCollection').doc('documentID')
Next, in a Cloud Function trigger I use this syntax:
functions.firestore.document('myCollection/{documentID}')
Third, in the Cloud Function return I use this syntax:
admin.firestore().collection('myCollection').doc('documentID')
The first and last lines are the same except that from the browser you call Firebase with firebase, when from the server you call Firebase using the firebase-admin Node package, here aliased to admin.
The middle line is different. It's calling Firebase using the firebase-functions Node package, here aliased to functions.
In other words, Firebase is called using different libraries, depending on whether you're calling from the browser or the server (e.g., a Cloud Function), and whether in a Cloud Function you're calling a trigger or a return.
Cloud functions is triggered based on events happening in Firebase example in realtime database, authentication.
Cloud firestore is triggered based on events happening in Firestore which uses the concept of documents and collections.
As explained here:
https://firebase.google.com/docs/functions/firestore-events
The cloud firestore triggers are used when there is a change in a document.
I had the same problem. I used following
const getReceiverDataPromise = admin.firestore().doc('users/' + receiverUID).get();
const getSenderDataPromise = admin.firestore().doc('users/' + senderUID).get();
return Promise.all([getReceiverDataPromise, getSenderDataPromise]).then(results => {
const receiver = results[0].data();
console.log("receiver: ", receiver);
const sender = results[1].data();
console.log("sender: ", sender);
});

Resources