What happens to async calls in completed firebase functions? - firebase

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;
});

Related

Firebase Functions: when to call initializeApp/deleteApp

I have a variety of functions for Cloud Functions that use things derived from firebase-admin/app's initializeApp(). Since multiple functions use this, I had the idea of calling initializeApp() close to the beginning of the Typescript/Javascript. Like this:
import { initializeApp } from 'firebase-admin/app';
...
const app = initializeApp();
...
export const activeUser = https.onCall(
async (email: string, ctx: CallableContext) => {
...
const auth = getAuth(app);
const firestore = getFirestore(app);
try {
const user = await auth.getUserByEmail(email);
const snpsht = await firestore.collection('users').doc(user.uid).get();
...
export const userExists = https.onCall(
async (email: string, ctx: CallableContext) => {
if (!emulating() && forbiddenOrigin(ctx)) {
return `${ERRORFLAG}: forbidden`;
}
const auth = getAuth(app);
...
A testing headache showed me that I forgot about calling deleteApp(app).
Is deleteApp() necessary, or will the app be deleted as a side effect of Cloud tearing down the function?
Should initializeApp()/deleteApp() be called within each function where an app is needed, ie within activeUser and userExists in my above example?
Is there any way to tell Cloud Functions to run setup/teardown code before running a given function?
A call to deleteApp is not necessary. When a Cloud Functions instance terminates, all memory gets shut down with it (which is what deleteApp would do). The entire server instance is completely gone.
You only need to call deleteApp if you are controlling a process that needs to continue after the call to deleteApp with the Firebase resources freed from memory. Cloud Functions does not meet this criteria since you don't control the process startup or shutdown.

Too much delay in the completion of the cloud function

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)

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.

Firebase Scheduled Function failing after first schedule

This is my function. It simply requests a weather report every 5 minutes from an API and writes the data into a Firestore collection/document:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const fetch = require('node-fetch');
admin.initializeApp();
const db = admin.firestore();
const cors = require("cors")({ origin: true });
exports.getWeather = functions
.region('europe-west2')
.pubsub.schedule('every 5 minutes').onRun((context) => {
const weatherApiKey = '**************************';
const weatherLocation = {
'Winchelsea Beach': '354689',
'Rye': '310224',
'Hastings': '310087'
};
const weatherApiUrl = 'http://datapoint.metoffice.gov.uk/public/data/val/wxfcs/all/json/' + weatherLocation["Winchelsea Beach"] + '?res=3hourly&key=' + weatherApiKey;
fetch(weatherApiUrl)
.then(res => res.json())
.then(json => {
db.collection("weather").doc("forecast").set({
data: json,
updatedLast: new Date().getTime()
});
});
});
The function added data to the DB successfully the very first time it ran but it doesn't run every 5 minutes, in fact it's stopped completely, and it says there's an error in my logs.
This is what is displayed in my logs:
There are no more logs after this when there should be logs every 5 minutes. If I re-deploy, it runs again with logs but then stops.
I've also checked Cloud Scheduler and it shows Result: Failed:
I'm getting the idea that scheduled functions won't again run with bugs but I don't know what's wrong with my function to get a Function return undefined, expected Promise or value error.
How can I resolve this?
The error message is telling you that your function should return a promise that resolves when all the async work is complete. If you don't, then the function will terminate early, and the async work is highly unlikely to complete, as Cloud Functions shuts down right afterward.
Is suggest reading the documentation to better understand how it works.
The solution is pretty straightforward. Return a promise that resolves after both the fetch and Firestore write are fully complete by chaining promises correctly.
return fetch(weatherApiUrl)
.then(res => res.json())
.then(json => {
return db.collection("weather").doc("forecast").set({
data: json,
updatedLast: new Date().getTime()
});
});

"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