Singleton CosmosDB client for Azure Node.js Function App

Viewed 464

I have an EventGrid triggered Node.js Azure function app.

It can receive 100 simultaneous calls to run. If the process succeeds or fails, it writes to CosmosDB.

However the initialization of the CosmosDB client is an expensive, long running process, so I want to share this initialized client as a Singleton amongst all of the 100 instances of the function app.

Looking at the Node.js documentation, it states that a module will be cached (same instance) if it exports an object. https://nodejs.org/api/modules.html#modules_caching i.e. it will act as a singleton if I return an object.

When I test my code in Azure, I see it being called over and over.

Question: How do I initialize my CosmosDB client once amongst all of the concurrent "threads".

In my example code below, I'm faking out the long running Cosmos initialization with a long running operation of 5000m/s.

My expensive operation module returns an object.

expensiveOperation.js

const snooze = ms => new Promise(resolve => setTimeout(resolve, ms));

let value = null;

const getInstance = async (context) => {
    if(value !== null) return value;
    context.log("Long running process started...")
    await snooze(5000);
    value = 5;
    return value;
}

exports.something = {
    getInstance
};

index.js

const { something } = require("../expensiveOperation");

module.exports = async function (context, req) {
  context.log("JavaScript HTTP trigger function processed a request.");

  var first = await something.getInstance(context);
  context.done();
};
2 Answers

tl;dr
You are doing it right but your thinking model is not correct.

Long answer
I would like to add some clarity to the terminology.
There is a function host, instance and invocation.
The documentation can guide us here.
You approach is correct, but your thinking model is not completely accurate.
Azure function model The Azure Function host is the black box and controls what is happening inside and at which point the new instance is setup.
Based on the number of messages in your Event Grid it will map your requests to the new instance and there is no control to which instance the messages will be mapped.
Each of this instances will have its own CosmosDB client.

Application Insights can help you see the number of instances that are running with minimal delay:
Live Metrics

  1. Functions are meant to be stateless.

Write functions to be stateless

Functions should be stateless and idempotent if possible. Associate any required state information with your data.

  1. If you want to share state, then official way this is supported is via Durable functions. To quote from brochure:

Simplify complex orchestration challenges resolution

Serverless functions are meant to be short-lived and stateless—until you need them to solve stateful problems. Remove this limitation in a fully managed way without provisioning more resources, just by coding your workflow definition. Simplify complex, stateful coordination requirements programmatically in event-driven applications with the Durable Functions extension.

  1. Durable functions come with their own baggage. So know what you're doing.

You should rethink your approach/design.

  1. Any reason why Azure Cosmos DB output binding for Azure Functions doesn't work for you?
  2. Assuming binding doesn't work. An alternative would be to add a Q-output binding to your function and post the results success/failure to the Q as a message. Write another Function that is triggered from this Q that would write it to Cosmos DB. Though you have a delay in task completion and DB update, it'll improve the UX/API-response-times. You could also optimize via batchSize.
Related