Home > Mobile >  Firebase Functions: How to maintain 'app-global' API client?
Firebase Functions: How to maintain 'app-global' API client?

Time:09-30

How can I achieve an 'app-wide' global variable that is shared across Cloud Function instances and function invocations? I want to create a truly 'global' object that is initialized only once per the lifetime of all my functions.

Context:

My app's entire backend is Firestore Firebase Cloud Functions. That is, I use a mix of background (Firestore) triggers and HTTP functions to implement backend logic. Additionally, I rely on a 3rd-party location service to continually listen to location updates from sensors. I want just a single instance of the client on which to subscribe to these updates.

The problem is that Firebase/Google Cloud Functions are stateless, meaning that function instances don't share memory/objects/state. If I call functionA, functionB, functionC, there's going to be at least 3 instances of locationService clients created, each listening separately to the 3rd party service so we end up with duplicate invocations of the location API callback.

Sample code:

// index.js
const functions = require("firebase-functions");

exports.locationService = require('./location_service');

this.locationService.initClient();

// define callable/HTTP functions & Firestore triggers
...

and

// location_service.js
var tracker = require("third-party-tracker-js");

const self = (module.exports = {
    initClient: function () {
        tracker.initialize('apiKey')
        .then((client)=>{
            client.setCallback(async function(payload) {
                console.log("received location update: ", payload)
                // process the payload ...
                // with multiple function instances running at once, we receive as many callbacks for each location update
            })

            client.subscribeProject()
            .then((subscription)=>{
                subscription.subscribe()
                .then((subscribeMsg)=>{
                    console.log("subscribed to project with message: ", subscribeMsg); // success
                });
                // subscription.unsubscribe(); // ??? at what point should we unsubscribe?
            })
            .catch((err)=>{
                throw(err)
            })
        })
        .catch((err)=>{
            throw(err)
        })
    },

});

I realize what I'm trying to do is roughly equivalent to implementing a daemon in a single-process environment, and it appears that serverless environments like Firebase/Google Cloud Functions aren't designed to support this need because each instance runs as its own process. But I'd love to hear any contrary ideas and possible workarounds.

Another idea...

Inspired by this related SO post and the official GCF docs on stateless functions, I thought about using Firestore to persist a tracker value that allows us to conditionally initialize the API client. Roughly like this:

// read value from db; only initialize the client if there's no valid subscription
let locSubscriberActive = await getSubscribeStatusFromDb();

if (!locSubscriberActive) {
   this.locationService.initClient();
}

// in `location_service.js`, do setSubscribeStatusToDb(); // set flag to true when we call subscribe(). reset when we get terminated

The problem faced: at what point do I unset/reset that value? Intuitively, I would do so the moment the function instance that initialized the client gets recycled/killed. However, it appears that it is not possible to know when a Firebase Cloud Function instance is terminated? I searched everywhere but couldn't find docs on how to detect such an event...

CodePudding user response:

What you're trying to do is not at all supported in Cloud Functions. It's important to realize that there may be any number of server instances allocated for each deployed function. That's how Cloud Functions scales up and down to match the load on the function in a cost-effective way. These instances might be terminated at any time for any reason. You have no indication when an instance terminates.

Also, instances are not capable of performing any computation when they are idle. CPU resources are clamped down after a function terminates, and are spun up again when the next function is invoked on that instance. You can't have any "daemon" code running when a function is not actively being invoked. I don't know what your locationService does, but it is certainly doing nothing at all after a function terminates, regardless of how it terminated.

For any sort of long-running or daemon-like code, Cloud Functions is not a suitable product. You should instead consider also using another product that lets you run code 24/7 without disruptions. App Engine and Compute Engine are viable alternatives, and you will have to think carefully about if and how you want their server instances to scale with load.

  • Related