I have written a GCP Cloud Function with a http trigger ("REST API") that invokes a Cloud Function with a Firebase Realtime Database trigger (onCreate).
The Cloud Function with a Firebase Realtime Database trigger performs REST calls to other services based on received data from the REST API.
I have noticed that the called services sometimes returns http 429 (too many calls) since my REST API does not have a limit to how many calls can be received.
The REST API has security measures in place to prevent unauthorised calls to invoke the Cloud Function with a Firebase Realtime Database trigger (onCreate). I do not wish to limit the amount of calls to my API, but rather place all requests in a queue and process them in sequence.
It is important that all calls are processed as promptly as possible. I do not wish to process transactions in 60 second intervals.
In my current solution all calls to the GCP HTTP http REST API immediately triggers the GCP Cloud Function via Firebase Realtime Database insert (onCreate event).
What I would like is to maybe have a queue in between my REST API and the Firebase Realtime Database insert (onCreate event) to ensure that only one GCP Cloud Function can execute simultaneously.
What is the best way to achieve this functionality?
Kind regards /K
EDIT:
Might Maximum instances be a solution here?
https://cloud.google.com/functions/docs/configuring/max-instances
You can Trigger Cloud Functions using Cloud Tasks. Here's an example I use whereby emails are placed inside of the Cloud Task queue I created and they are then processed by the task runner one-after-another:
import { CloudTasksClient } from '#google-cloud/tasks';
// define and use the following for your task:
// CLOUD_FUNCTION_URL
// TASK_HTTP_METHOD
// TASK_CONTENT_TYPE
// SERVICE_ACCOUNT_EMAIL
// PROJECT_ID
// REGION
// QUEUE_NAME
const client = new CloudTasksClient({ projectId: PROJECT_ID });
/**
* Build the Cloud Task
* In the below example we take a POST body and
* stringify it before converting to base64.
*/
const convertedPayload = JSON.stringify(payload);
const body = Buffer.from(convertedPayload).toString('base64');
const task: TaskT = {
httpRequest: {
url: CLOUD_FUNCTION_URL,
httpMethod: TASK_HTTP_METHOD,
body,
headers: {
'Content-Type': TASK_CONTENT_TYPE,
},
oidcToken: {
serviceAccountEmail: SERVICE_ACCOUNT_EMAIL,
},
},
scheduleTime: {
seconds: Date.now() / 1000, // <--- start the task now (in unix time)
},
};
return client.createTask({
parent: client.queuePath(PROJECT_ID, REGION, QUEUE_NAME),
task,
});
You'll also need to configure some IAM permissions for your development and app IAM users like Cloud Functions Invoker and Cloud Functions Viewer.
Related
I have a test suite that runs integration tests for my Firebase Cloud Functions against a locally running Firebase emulator, is it possible to assert the number of reads/writes made to the emulated Firestore instance?
E.g. for the following function I would want to assert that 1 write occurs & 0 reads
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
exports.addMessage = functions.https.onCall(async (data, context) => {
const original = data.query.text;
const snapshot = await admin.firestore().collection("/messages").add({ original: original });
return { code: 200, message: "Success" };
});
Currently, Its not possible to assert the number of reads and writes in a Firebase Cloud Function test.
I would suggest that the best way would be is to write the test data to the Firebase Emulator, then read it back and also count the number of collections/documents as well using Client SDK.
If you want to assert data coming from the Firebase Emulator, then you can use Requests Monitor. We used Client SDK for this purpose as Admin SDK requests and access calls will not be listed because it bypass Security Rules. It's a current limitation according to this Blog.
The Firebase emulator has a log that shows requests as shown in example below.
Note: I don't think that you should have a dependency on it, since Firebase Emulator Request Monitor is a new feature that its implementation may change and Admin SDK can be included over time.
I am working on cloud functions especially schedule functions. I need to trigger a function periodically each 5 minutes, but in only test step. I need to run it on pubsub emulator without deploying it.
How to do it?
I tried to use firebase shell, but it triggered only once
exports.scheduledFunctionPlainEnglish =functions.pubsub.schedule('every 2 minutes')
.onRun((context) => {
functions.logger.log("this runs every 2 minutes")
return null;
})
Scheduled functions are loaded to the Cloud Functions emulator runtime and are bound to the PubSub emulator topic.
But as #samstern said (https://github.com/firebase/firebase-tools/issues/2034):
you'd have to manually trigger them using a Pub/Sub message.
You can do it like this:
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import { PubSub } from '#google-cloud/pubsub';
if (!admin.apps.length) {
admin.initializeApp();
}
const pubsub = new PubSub({
apiEndpoint: 'localhost:8085' // Change it to your PubSub emulator address and port
});
setInterval(() => {
const SCHEDULED_FUNCTION_TOPIC = 'firebase-schedule-yourFunctionName';
console.log(`Trigger sheduled function via PubSub topic: ${SCHEDULED_FUNCTION_TOPIC}`);
const msg = await pubsub.topic(SCHEDULED_FUNCTION_TOPIC).publishJSON({
foo: 'bar',
}, { attr1: 'value1' });
}, 5 * 60 * 1000); // every 5 minutes
Additional info about this concept (thanks to #kthaas):
https://github.com/firebase/firebase-tools/pull/2011/files#diff-6b2a373d8dc24c4074ee623d433662831cadc7c178373fb957c06bc12c44ba7b
https://github.com/firebase/firebase-tools/pull/2011/files#diff-73f0f0ab73ffbf988f109e0a4c8b3b8a793f30ef33929928a892d605f0f0cc1f
As you said, you can use firebase shell to run your function once.
And in firebase shell, you can use NodeJS commands.
Use setInterval
Inside firebase functions:shell, use setInterval to run your function every 2 minutes.
user#laptop:~$ firebase functions:shell
✔ functions: functions emulator started at http://localhost:5000
i functions: Loaded functions: myScheduledFunction
firebase > setInterval(() => myScheduledFunction(), 120000)
> this runs every 2 minutes
Single line script
Since version 8.4.3 of firebase-tools, and especially this PR, the pipe solution does not work anymore.
In Bash, you can even pipe the setInterval command to firebase shell
user#laptop:~$ echo "setInterval(() => myScheduledFunction(), 120000)" | firebase functions:shell
For those of you seeing this in 2023, it's still not supported.
My solution was to abstract the code that does the "work" out of functions.pubsub.schedule and into their own functions. Then create a separate file (i added it at the top of the functions folder) with a setInterval inside it that fires the aforementioned abstracted function.
For example, somewhere in your code:
exports.myScheduledFunctionCode = () => {
console.log('why, hello there interval');
}
And in the timers.js (for example) file at the top of the /functions directory:
setInterval(() => {
myScheduledFunctionCode();
}, 60000);
Then, you can fire up your Firebase Emulator suite. In another Terminal session, just run a vanilla $ node functions/timers.js. Now your scheduled function code is running, and your whole emulator suite too.
Hope this helps someone!
This is currently not supported for scheduled functions. The documentation states:
Using the shell, you mock data and perform function calls to simulate interaction with products that the Emulator Suite does not currently support: Storage, PubSub, Analytics, Remote Config, Storage, Auth, and Crashlytics.
Scheduled functions are an unsupported extension of pubsub triggers.
Feel free to file a feature request with Firebase support.
I have created a google cloud task and the queue keeps retrying and the function is not getting invoked as well.
This is the log from the cloud console.
attemptResponseLog: {
attemptDuration: "0.133874s"
dispatchCount: "19"
maxAttempts: 0
responseCount: "0"
retryTime: "2020-06-21T21:20:18.518655Z"
scheduleTime: "2020-06-21T21:20:15.718098Z"
status: "UNAVAILABLE"
targetAddress: "POST some url"
targetType: "HTTP"
}
I ran into this same error, and I must say that the documentation is not clear enough.
WARNING : I feel there's a bit of latency for the roles to be taking into account, especially with the ServiceAccountUser one.
I made multiple test, and tried to keep the lowest rights possible, so I did try to remove some... do some test, it works... great, it's not necessary to have this right... came back later, and the thing is broken.
Here is my setup :
I use Cloud Scheduler to trigger a Cloud Function every 15 minutes by posting a message on a queue.
The CloudFunction build a list of tasks to compute stats on MySQL and create the tasks
Another Cloud Function run SQL query to get stats and store the results in FireStore.
I use cloud task so that the load on MySQL is not too heavy.
Below, I use functional names to make it easy to understand.
TaskCreatorCloudFunction running with TaskCreatorServiceAccount
TaskCreatorServiceAccount requires
the "Cloud Task Enqueuer" role #1
be a ServiceAccountUser on the CloudTaskComputeStatsServiceAccount (see after) #2
The Roles needed to do the job(read SQL to get the list of tasks to create, write logs, access secret manager, listen to pubsub as it's triggered by Cloud Scheduler via pubsub)
TaskImplementationCloudFunction (http) running with TaskImplementationServiceAccount
TaskImplementationServiceAccount has no specific role for CloudTasks, only the one needed to do the job (read SQL, write logs, access secret manager, firestore write)
The TaskQueue is named "compute-stats-on-mysql".
I've created a dedicated ServiceAccount named CloudTaskComputeStatsServiceAccount #3
CloudTaskComputeStatsServiceAccount has the specifics rights for the whole thing to work.
Cloud Function Invoker #4
Add CloudTaskComputeStatsServiceAccount as ServiceAccountUser on TaskImplementationServiceAccount #5
To do the last one in the console (script version below), you need to
go to IAM->Service Account
check the TaskImplementationServiceAccount
In the upper right corner, click "Show Info Panel" if it's not already displayed
click the Add Member
Paste the full name of the CloudTaskComputeStatsServiceAccount
Choose Service Account User as role
Save
You can edit this in the console, but it's better to script it.
gcloud tasks queues create compute-stats-on-mysql \
--max-dispatches-per-second=10 \
--max-concurrent-dispatches=15 \
--max-attempts=2 \
--min-backoff=1s
#3
gcloud iam service-accounts create CloudTaskComputeStatsServiceAccount --description="Service Account for the cloud task compute-stats-on-mysql" --display-name="Service Account for the cloud task compute-stats-on-mysql"
#4
gcloud projects add-iam-policy-binding ${PROJECT_ID} --member serviceAccount:CloudTaskComputeStatsServiceAccount#${PROJECT_ID}.iam.gserviceaccount.com --role "roles/cloudfunctions.invoker"
#1
gcloud projects add-iam-policy-binding ${PROJECT_ID} --member serviceAccount:TaskCreatorServiceAccount#${PROJECT_ID}.iam.gserviceaccount.com --role "roles/cloudtasks.enqueuer"
#5
gcloud iam service-accounts add-iam-policy-binding TaskImplementationServiceAccount#${PROJECT_ID}.iam.gserviceaccount.com --member="serviceAccount:CloudTaskComputeStatsServiceAccount#${PROJECT_ID}.iam.gserviceaccount.com" --role "roles/iam.serviceAccountUser"
#2
gcloud iam service-accounts add-iam-policy-binding CloudTaskComputeStatsServiceAccount#${PROJECT_ID}.iam.gserviceaccount.com --member="serviceAccount:TaskCreatorServiceAccount#${PROJECT_ID}.iam.gserviceaccount.com" --role=roles/iam.serviceAccountUser
when Creating the Task, you use the CloudTaskComputeStatsServiceAccount in the oidcToken
const body = Buffer.from(JSON.stringify(data)).toString('base64');
const task = {
httpRequest: {
httpMethod: 'POST',
url,
oidcToken: {
serviceAccountEmail: "CloudTaskComputeStatsServiceAccount#${PROJECT_ID}.iam.gserviceaccount.com",
},
headers: {
'Content-Type': 'application/json',
},
body,
},
};
My understanding is that when you run the
const [response] = await cloudTasksClient.createTask({parent, task});
The Cloud Function (Task Creator) need to Create Task, and act as the "CloudTaskComputeStatsServiceAccount"
And "CloudTaskComputeStatsServiceAccount" need to have the cloud function invoker and act as the target cloud function.
Indeed it's not a service account issue. It's the OIDC token audience missing. Seems that for cloud functions this is needed. I found two references... you can recreate this problem with the OIDC token in the cli by omitting this argument to gcloud tasks create-http-task
--oidc-token-audience=OIDC_TOKEN_AUDIENCE
The audience to be used when generating an OpenID Connect token to be
included in the request sent to the target when executing the task.
If not specified, the URI specified in the target will be used
The second reference that popped up, in ruby shows audience
https://googleapis.dev/ruby/google-cloud-tasks/latest/Google/Cloud/Tasks/V2/OidcToken.html
Code using google-cloud-tasks 1.5.0, the tasks object looks like this, where url_oidc has just the url to the cloud function (i.e. the trigger url... no url parameters)
# Construct the request body.
task = {
'http_request': { # Specify the type of request.
'http_method': 'GET',
'url': url_final, # The full url path that the task will be sent to.
'oidc_token': {
'service_account_email': service_account_email,
'audience': url_oidc
}
}
}
I have a listener to Firestore DB changes and it fetches automatically every time there is a change, however, if I decide to implement it in Cloud Function and call it from the client app, will it cost more because it will running 24h/7 even when users are not using the app?
This is in Client side:
firestore()
.collection('Collection').doc().collection('public')
.where('act', '==', 1)
.orderBy('time', 'asc')
.limit(10)
.onSnapshot({
error: (e) => this.setState({ errorMessage: e, loading: false }),
next: (querySnapshot) => { this._calculateLocationDistance(querySnapshot) },
});
Moreover, is it necessary to do it in Cloud Function? Is it risky if I leave it in the client side?
You can't really use listeners effectively in Cloud Functions. Cloud Functions are meant to be stateless. They serve a single request at a time, and clean up afterward. If you try to use a listener, it just won't work the way you expect. Cloud Functions also don't keep a socket open to the requester. Once a response is sent, the connection is closed, and there's no way to keep it open.
Given these constraints, functions typically just use get() to fetch data a single time, and return the results to the client. If you want realtime results, that should be implemented on the client.
If you are working with a backend that can keep a socket connection open to a client, it is no less expensive to have a listener on the backend that delivers results to the client. You are still charged a document read for each document read by the listener as it continues to receive results.
I learned all about Google Cloud Functions Triggers and I understood that they are good for doing some data preprocessing before write/update/delete happens in Cloud Firestore. This is true right?
However, instead of listening to changes in the database, I would like to write custom code in Cloud Functions and call it directly from the client SDK.
The reason I want to do this is because I don't want to have long complicated database integration logic in my client code. I just want to call a cloud function named createGame for instance and let the custom cloud function handle the nested calls to the Firestore, do sanity checks and return me with only the clean data that the client will be displaying.
I found out that there is a Flutter plugin for this called cloud_functions.
Also this documentation shows how to write custom callable functions.
We can write the custom callable Cloud Function as follows:
exports.createGame = functions.https.onCall((data, context) => {
// ...
return {
...
}
});
We can call the corresponding custom callable Cloud Function from Flutter client as follows:
final dynamic response = await CloudFunctions.instance.call(
functionName: 'createGame',
parameters: <String, dynamic>{
'message': 'hello world!',
'count': clientCount,
},
);
There are tons of Cloud Functions trigger examples/videos/tutorials. But very few of callable Cloud Functions. That's why I am a little skeptic about this subject.
Question:
My question is if this is good practice? Or should I do what this specific Cloud Function is doing from the client? Please answer while keeping pricing, speed, reliability and other factors in mind also:)
One last thing,
The function implementation uses https.onCall. Do you think https is slowing things down?