Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practice for creating multiple lightweight Google Cloud Functions?

The way it seems Google Cloud Functions works is by:

  • your modules go inside a functions directory
  • that functions directory then contains a package.json file to contain the shared dependencies across all your modules
  • modules can contain many exported functions per module
  • google cloud functions and firebase functions have different opinions how to handle exported functions in modules
    • for gcp, it seems to work like this: the module is uploaded, and then you specify via the web interface or command line which exported method should be called from the loaded module
    • for firebase, it seems to work like this: the listener methods from firebase-functions return the handler, but also attaches to that handler the trigger meta-data. The firebase-tools cli app then requires your code locally, grabs the exported functions, then creates cloud functions for each exported method based on its attached meta-data from the firebase-functions stuff. As such, if you put all your cloud functions in the same module, then it will deploy that module multiple times for each cloud function, and for each cloud function the entire module is loaded, and then the specific exported function is called.
  • if you configure a exported function to be a http trigger, it uses an undefined version of express.js, and an amorphous amount and order of bundled middlewares

This is strange as:

  • say even if the modules one.js and two.js require different packages at runtime, the shared package.json between them means that their startup time will be slower than if done individually as they will both need to install all the dependencies of the package rather than just their own
  • if you have several exported functions inside index.js, such as hi() and hello(), then the hi cloud function will also have the hello() function loaded in memory despite not using it, as well as the hello cloud function will have hi() in memory despite not using it, as for both the resulting cloud function will still use the same index.js file, loading everything inside that module into memory even if other parts aren't needed

As such, what is the best practice for making sure your cloud functions run optimally with the lightest runtime footprint possible? As it seems the design decisions by Google mean that the more cloud functions you make, then the more junk gets bundled with each cloud function, slowing them down and costing more.


As an side, it seems to me that this would have been a better approach for google: Each cloud function should have its own directory, and in each directory there is a package.json file and a index.js file. The index.js file then does a module.exports = function(...args){} or a export default function(...args){}.

This way the architecture aligns with how one expects cloud functions to operate - being that a cloud function represents a single function - rather than a cloud function being the installation of the shared dependencies between your all your cloud functions, then the loading of a module that can contain multiple cloud functions but only one is used, then the execution of only one function out of that loaded module.

Funnily enough, Azure Functions seems be designed exactly the way I expect cloud functions to operate: https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-node

like image 644
balupton Avatar asked Jul 18 '17 09:07

balupton


People also ask

How many requests can handle a single cloud function?

By default each Cloud Run container instance can receive up to 80 requests at the same time; you can increase this to a maximum of 1000.

How do clouds scale Functions?

Cloud Functions scales by creating new instances of your function. Each of these instances can handle only one request at a time, so large spikes in request volume might result in creating many instances.

How long can a cloud function run by default before timing out?

In Cloud Functions (1st gen), the maximum timeout duration is nine minutes (540 seconds). In Cloud Functions (2nd gen), the maximum timeout duration is 60 minutes (3600 seconds) for HTTP functions and 9 minutes (540 seconds) for event-driven functions.


2 Answers

Rather than exporting each Cloud Function individually, we can export all of them at once by requiring a file that export's every file in a specific directory and its subdirectories.

functions/index.js
'use strict';

const admin = require('firebase-admin');
const functions = require('firebase-functions');
const logging = require('@google-cloud/logging')();
const stripe = require('stripe')(functions.config().stripe.token);

admin.initializeApp(functions.config().firebase);

module.exports = require('./exports')({
    admin, functions, logging, stripe
});

We can create a folder for each provider, e.g. auth, database, https, in which we can organise the associated resource's events such as auth.user.onCreate or database.ref.onWrite.

By structuring our files this way with an event in each file, we can search for the function files and use the file path to dynamically create the Cloud Function's name and export it.

e.g. functions/exports/auth/user/onCreate.f.js -> onCreateAuthUser

functions/exports/index.js
'use strict';

module.exports = (app) => {

    const glob = require('glob');

    glob.sync('./**/*.f.js', { cwd: __dirname }).forEach(file => {

        const only = process.env.FUNCTION_NAME;
        const name = concoctFunctionName(file);

        if (only === undefined || only === name) {
            exports[name] = require(file)(app);
        }
    });

    return exports;
}

function concoctFunctionName(file) {

    const camel = require('camelcase');
    const split = file.split('/');
    const event = split.pop().split('.')[0];
    const snake = `${event}_${split.join('_')}`;

    return camel(snake);
}

Finally, our function files can use the argument passed to access commonly required services such as Firebase Admin and Functions, and even Stripe.

functions/exports/auth/user/onCreate.f.js
'use strict';

module.exports = ({ admin, functions, stripe }) => {

    return functions.auth.user().onCreate(event => {

        const { email, uid } = event.data;
        const ref = admin.database.ref.child(`user-customer/${uid}`);

        return stripe.customers.create({ 
            email, metadata: { uid } 
        }).then(customer => {
            return ref.set(customer);
        });
    });
}
like image 125
Callam Avatar answered Nov 09 '22 01:11

Callam


I'm using modofun (https://modofun.js.org), which is a router for multiple operations based on the request path. This allows me to gather related functions into a module that's deployed as a single Google Cloud Function. The dependencies for all functions in that module are the same, so that makes it dependency-efficient. And you can also share common global resources, like database connections.

I agree that deploying every single function on its own is a waste of resources, and it's very hard to maintain.

I did this for a Google Cloud Functions deployment I have in production.

like image 21
Filipe Avatar answered Nov 09 '22 00:11

Filipe