firebase-functions icon indicating copy to clipboard operation
firebase-functions copied to clipboard

Firebase Cloud Functions V2: setGlobalOptions Does Not Apply Region Correctly

Open masanori-iwata opened this issue 1 year ago • 23 comments

Related issues

[REQUIRED] Version info

node: v18.7.0

firebase-functions: v4.5.0

firebase-tools:

firebase-admin: v11.11.1

[REQUIRED] Test case

In Firebase Cloud Functions V2, when using setGlobalOptions to set the region to asia-northeast1, some Firestore trigger functions are still being initialized in the default region us-central1.

[REQUIRED] Steps to reproduce

  1. Set up a Firebase Cloud Function using setGlobalOptions to specify the region as asia-northeast1.
  2. Define a Firestore trigger function in the Firebase Functions codebase.
  3. Deploy the project using Firebase CLI.
  4. Check the Firebase CLI logs to see in which region the function has been initialized.
  5. Notice that the function is initialized in us-central1 instead of the specified asia-northeast1.

[REQUIRED] Expected behavior

All functions should respect the global options set, especially regarding the region. The expected behavior is that functions are initialized in the specified region asia-northeast1.

[REQUIRED] Actual behavior

Functions such as Firestore triggers are being initialized in the us-central1 region, despite the setGlobalOptions specifying asia-northeast1.

code

[functions/index.js]
import { initializeApp, applicationDefault } from 'firebase-admin/app';
import { setGlobalOptions } from 'firebase-functions/v2';

initializeApp({
  credential: applicationDefault(),
});

setGlobalOptions({
  region: 'asia-northeast1',
  timeoutSeconds: 540,
  memory: '2GiB',
  minInstances: 0,
  maxInstances: 5,
  concurrency: 2,
});

export * as v1 from '#root/v1/index.js';

[functions/v1/index.js]
import * as patch from '#root/v1/patch/index.js';

export {
  // auth,
  // firestore,
  // storage,
  // https,
  patch,
};

[functions/v1/patch/index.js]
import { onDocumentUpdated } from 'firebase-functions/v2/firestore';
export const onCreateManagementTemplate = onDocumentUpdated(
  {
    document: '...',
  },
  async (event) => {
    // 
  },
);

log

✔  functions: Loaded functions definitions from source: v1.patch.onCreateManagementTemplate.
✔  functions[us-central1-v1-patch-onCreateManagementTemplate]: firestore function initialized.

Were you able to successfully deploy your functions?

Yes, the functions were deployed successfully, but the regional setting was not applied as expected.

masanori-iwata avatar Dec 16 '23 05:12 masanori-iwata

I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.

google-oss-bot avatar Dec 16 '23 05:12 google-oss-bot

Hello @masanori-iwata. Is the issue that functions in another file don't get the region settings applied?

exaby73 avatar Jan 05 '24 12:01 exaby73

While not the original author, I'm experiencing the same. And yes, for me I set setGlobalOptions in the index file but the functions themselves are in multiple other files.

fwal avatar Jan 11 '24 15:01 fwal

Hello @masanori-iwata. Is the issue that functions in another file don't get the region settings applied?

Yes. I set it on a top level file but it did not work on another files.

masanori-iwata avatar Jan 12 '24 01:01 masanori-iwata

@masanori-iwata can you give the version of firebase-tools you are using by running the below command:

firebase --version

OutdatedGuy avatar Jan 13 '24 19:01 OutdatedGuy

firebase --version

the version is 13.0.2.

masanori-iwata avatar Jan 14 '24 05:01 masanori-iwata

I cannot reproduce this issue, unfortunately. Here's what I tried:

// src/index.ts
import { setGlobalOptions } from "firebase-functions/v2/options";
import * as admin from "firebase-admin";
import { onRequest } from "firebase-functions/v2/https";

admin.initializeApp({
  credential: admin.credential.applicationDefault(),
});

setGlobalOptions({ region: "asia-south1", maxInstances: 10 });

export const helloWorld = onRequest((_, res) => {
  res.send("Hello from Firebase!");
});

export * from "./more-functions";
// src/more-functions.ts
import { onRequest } from "firebase-functions/v2/https";
import { onDocumentWritten } from "firebase-functions/v2/firestore";

export const anotherHelloWorld = onRequest((_, res) => {
  res.send("Hello from another Firebase function!");
});

export const firestoreTrigger = onDocumentWritten(
  "users/{userId}",
  (context) => {
    console.log("New user:", context.data?.after.data());
  }
);

I get the following in the terminal after running firebase deploy --only functions:

✔  functions[helloWorld(asia-south1)] Successful update operation.
✔  functions[anotherHelloWorld(asia-south1)] Successful update operation.
✔  functions[firestoreTrigger(asia-south1)] Successful create operation.

As you can see in the first file, I have set the global options only once, but it applies to all functions. Do note you have to select the region for V1 functions separately as setGlobalOptions is specific to V2.

exaby73 avatar Feb 07 '24 13:02 exaby73

Hey @exaby73, I found some interesting results:

  1. If you copy code structure provided by @masanori-iwata in Javascript (not Typescript) you can reproduce the issue.
  2. The issue is not produced in any typescript code
  3. If you converted the project to use Javascript and use the code sample provided here you can reproduce the issue.
  4. Looks like in Javascript project, setGlobalOptions only works for functions defined in the same file: Screenshot 2024-02-07 at 7 33 05 PM

OutdatedGuy avatar Feb 07 '24 14:02 OutdatedGuy

@OutdatedGuy thanks for this input! I can reproduce it with a JS project. I'm guessing in TS, all files are bundled into a single file by tsc which is why setGlobalOptions works there, but with different files, it doesn't work. A workaround is to have setGlobalOptions in each file. I found that having a shared function call at the top of each file works:

// src/options.js
import { setGlobalOptions } from "firebase-functions/v2/options";

export function configureOptions() {
  setGlobalOptions({ region: "asia-south1", maxInstances: 10 });
}

That function can be called at the top of each file so that there is a single source of truth for global options

exaby73 avatar Feb 08 '24 08:02 exaby73

Experiencing the same issue, using Typescript.

index.ts:

initializeApp()
setGlobalOptions({ region: 'europe-north1' })

exports.backdrop = {
...
}

Region is not applied to the functions when deployed.

Unfortunately, using the workaround suggested by @exaby73 will result in the following errors:

{"severity":"WARNING","message":"Calling setGlobalOptions twice leads to undefined behavior"}

{"severity":"WARNING","message":"Calling setGlobalOptions twice leads to undefined behavior"}

{"severity":"WARNING","message":"Calling setGlobalOptions twice leads to undefined behavior"}

{"severity":"WARNING","message":"Calling setGlobalOptions twice leads to undefined behavior"}

{"severity":"WARNING","message":"Calling setGlobalOptions twice leads to undefined behavior"}

{"severity":"WARNING","message":"Calling setGlobalOptions twice leads to undefined behavior"}

OskarGroth avatar Apr 11 '24 13:04 OskarGroth

@OskarGroth, it shouldn't be an issue as long as the options are the same (which is why I recommended having a global function called). But yes, I agree this is not ideal in the slightest.

exaby73 avatar Apr 12 '24 02:04 exaby73

I'm experiencing the same, despite bundling to a single file (from TypeScript). It fails when doing setGlobalOptions in the top-level index-file (which re-exports functions from discrete files), and it works then it is done from within the discrete files.

Transpiled and bundled output that works:

(top level) ...

setGlobalOptions({ region: "europe-north1" });
const myFunctionName = onRequest({ cors: !0 }, (t, e) => {
  console.log("Hello world", t.body), e.send("OK");
});
export {
  myFunctionName
};

Bundled output that fails:

(top level) ...

const myFunctionName = onRequest({ cors: !0 }, (t, e) => {
  console.log("Hello world", t.body), e.send("OK");
});
setGlobalOptions({ region: "europe-north1" });
export {
  myFunctionName
};

Seems like the order here matters, and the way (Vite) bundles is the real problem (for me).

jesperstarkar avatar Aug 04 '24 21:08 jesperstarkar

I would think that vite wouldn't mess with the ordering of the code. Can you give an example 2 files I can pop into a new generated TS project to try and investigate what is going on?

exaby73 avatar Aug 29 '24 10:08 exaby73

I just stumbled across this issue when I wanted to deploy a document trigger with onDocumentUpdated and got the message A firestore trigger location must match the firestore database region. from here https://github.com/firebase/firebase-tools/blob/master/src/deploy/functions/services/firestore.ts#L40. I didn't get this before, so I wonder why it changed now. I restored the database via terraform scripts. Maybe this changes something?

GerroDen avatar Sep 06 '24 17:09 GerroDen

@GerroDen Could you check which region your database is from the Google Cloud Console

exaby73 avatar Sep 09 '24 19:09 exaby73

@exaby73 Google Cloud Console says like Firebase Console europe-west1. Which is also what I set as global options:

setGlobalOptions({
  region: "europe-west1",
  maxInstances: 10,
  timeoutSeconds: 540,
  memory: "256MiB",
});

GerroDen avatar Sep 11 '24 07:09 GerroDen

FWIW, I could not get setGlobalOptions in my index file to apply to all my functions when using ES6, but with CommonJS it works just fine.

francois-launchbase avatar Oct 31 '24 16:10 francois-launchbase

We are seeing the same issue after migrating the TypeScript project from CommonJS to ES Modules. This was working fine before in index.ts:

setGlobalOptions({ region: 'europe-west1' })

export * from './functions'

but now with ESM the global options are not applied. Going back to CommonJS is not a solution, unfortunately.

svenjacobs avatar Feb 26 '25 08:02 svenjacobs

I ended up replacing setGlobalOptions and explicitly set the region for all functions individually.

jesperstarkar avatar Feb 26 '25 09:02 jesperstarkar

I ended up replacing setGlobalOptions and explicitly set the region for all functions individually.

I fear we have to go the same route but setGlobalOptions not working in ES modules is something that Google should have a look hat imho 👀

svenjacobs avatar Feb 26 '25 09:02 svenjacobs

I tried to use dynamic imports. Interestingly the region is applied for functions imported dynamically but then unfortunately all functions have a default- prefix, like default-function1 and default-function2.

export default {
  ...(await import('./function1.js')),
  ...(await import('./function2.js')),
}

It's possible to work around this by exporting each function separately, however it becomes quite tedious if you have many functions and/or your modules export more than just one function.

export const function1 = (await import('./function1.js')).function1
export const function2 = (await import('./function1.js')).function2

svenjacobs avatar Feb 26 '25 10:02 svenjacobs

When migrating from CommonJS to ES Modules (ECMAScript) in a Firebase Functions project using TypeScript, I had the same issue where setGlobalOptions() from "firebase-functions/v2/options" was not being applied properly.

After switching to ES Modules, my Cloud Functions no longer respected the global configuration options set using:

import { setGlobalOptions } from "firebase-functions/v2/options";

setGlobalOptions({
  region: "asia-south1",
  timeoutSeconds: 540,
});

The issue seems to be related to the evaluation order of module imports in ES Modules. Since ES Modules execute imports before any statements in the main file, some Firebase functions were being defined before setGlobalOptions() was applied.

Solution that worked for me: Using dynamic imports (await import()) for function handlers ensures setGlobalOptions() is applied before any function is registered.

Fixed Code (TypeScript + ES Modules) Compatible with Firebase Functions v2 and modern ES Module syntax [functions/index.ts]

import { setGlobalOptions } from "firebase-functions/v2/options";
import { initializeApp } from "firebase-admin/app";

// Initialize Firebase Admin
initializeApp();

// Set global options for Cloud Functions
setGlobalOptions({
  region: "us-central1",
  timeoutSeconds: 540,
});

// Dynamically import functions 
const { sampleFunction1 } = await import("./path/to/sampleFunction1.js");
const { sampleFunction2 } = await import("./path/to/sampleFunction2.js");

// Export functions
export { sampleFunction1, sampleFunction2 };

x-sangeeth avatar Mar 11 '25 15:03 x-sangeeth

Solution that worked for me: Using dynamic imports (await import()) for function handlers ensures setGlobalOptions() is applied before any function is registered.

Well, that's exactly what I wrote above 😉 However this solution becomes impractical when you have many Cloud Functions.

svenjacobs avatar Mar 11 '25 15:03 svenjacobs

Any update on this? I'm also having the same issue after migrating from CommonJS to ES Modules

Sandlens avatar Aug 14 '25 02:08 Sandlens