From 4caea4d033fc64e4663969d5b382a5cbe82e314c Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Tue, 20 Oct 2026 16:24:17 +0100 Subject: [PATCH 1/2] fix(storage-resize-images): match the extension's instance limit and ingress The kit deployed with the 2nd gen defaults of 100 instances and ALLOW_ALL ingress, so with concurrency 1 at most 100 images resized at once. Set maxInstances to the extension's 3000 and ingress to ALLOW_INTERNAL_ONLY. --- kits/storage-resize-images/CHANGELOG.md | 2 ++ kits/storage-resize-images/README.md | 4 +++- kits/storage-resize-images/src/index.ts | 4 ++++ .../tests/deploy-options.test.ts | 15 ++++++++++++++- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/kits/storage-resize-images/CHANGELOG.md b/kits/storage-resize-images/CHANGELOG.md index 1a874569e..5cb61cc49 100644 --- a/kits/storage-resize-images/CHANGELOG.md +++ b/kits/storage-resize-images/CHANGELOG.md @@ -1,3 +1,5 @@ +- fix: `generateResizedImage` now scales to 3000 instances and accepts only internal traffic, as the extension did. Left unset, the Firebase CLI deployed it with the 2nd gen defaults of 100 instances and `ALLOW_ALL` ingress, so with one event per instance at most 100 images resized at once. The next deploy updates the function in place; no `.env` change is needed. + - fix: `generateResizedImage` now handles one event per instance, as the extension's 1st gen trigger did, and sizes its CPU from `FUNCTION_MEMORY` the 1st gen way. Left unset, the Firebase CLI deployed it with 1 vCPU and a concurrency of 80, so a burst of uploads landed on a single instance and every resize shared its CPU. With 80 large images uploaded at once, 58 requests reached the 60s timeout and 56 images were never resized: the trigger has no retry policy, the timeouts were logged as HTTP 200 at INFO severity, and no `onError` event or failed-image copy was written. The next deploy updates the function in place; no `.env` change is needed. - fix: an Eventarc publish failure no longer fails the function. A channel that was deleted or is unreachable answers `PERMISSION_DENIED`, and the publish was awaited before the work the function exists to do, so the image was never resized. The publish failure is now logged as a warning and the invocation continues. Deploys also declare `eventarcpublishing.googleapis.com`, so the API is enabled on your project whether or not you use events. diff --git a/kits/storage-resize-images/README.md b/kits/storage-resize-images/README.md index 1eb671c42..06bf445b3 100644 --- a/kits/storage-resize-images/README.md +++ b/kits/storage-resize-images/README.md @@ -182,7 +182,9 @@ was 1st gen. `FUNCTION_MEMORY` still accepts the same values (512 through 8192) and maps onto the equivalent 2nd gen memory setting. Like the extension, the function handles one event per instance (`concurrency: 1`) and gets the CPU 1st gen gave that memory size (`cpu: "gcf_gen1"`), so a burst of uploads -scales out across instances instead of sharing one. +scales out across instances instead of sharing one. It also keeps the +extension's limit of 3000 instances (`maxInstances: 3000`) and accepts only +internal traffic (`ingressSettings: "ALLOW_INTERNAL_ONLY"`). The function's service account needs `roles/eventarc.eventReceiver` and `roles/run.invoker` on top of the roles the extension asked for. The Firebase diff --git a/kits/storage-resize-images/src/index.ts b/kits/storage-resize-images/src/index.ts index 18596f8ea..181cb6078 100644 --- a/kits/storage-resize-images/src/index.ts +++ b/kits/storage-resize-images/src/index.ts @@ -109,6 +109,10 @@ export const generateResizedImage = onObjectFinalized( // policy, so those images are never resized. concurrency: 1, cpu: "gcf_gen1", + // The extension's 1st gen limit. At concurrency 1, the 2nd gen default of + // 100 instances caps how many images resize at once. + maxInstances: 3000, + ingressSettings: "ALLOW_INTERNAL_ONLY", }, (event) => handleObjectFinalized(event, getContext()) ); diff --git a/kits/storage-resize-images/tests/deploy-options.test.ts b/kits/storage-resize-images/tests/deploy-options.test.ts index 0edb8c3a6..c4618614d 100644 --- a/kits/storage-resize-images/tests/deploy-options.test.ts +++ b/kits/storage-resize-images/tests/deploy-options.test.ts @@ -79,7 +79,12 @@ describe("CONFIG_EXPRESSIONS", () => { describe("generateResizedImage instance sizing", () => { const endpoint = ( generateResizedImage as unknown as { - __endpoint: { concurrency?: number; cpu?: number | string }; + __endpoint: { + concurrency?: number; + cpu?: number | string; + maxInstances?: number; + ingressSettings?: string; + }; } ).__endpoint; @@ -90,6 +95,14 @@ describe("generateResizedImage instance sizing", () => { test("sizes CPU from memory as 1st gen did", () => { expect(endpoint.cpu).toBe("gcf_gen1"); }); + + test("scales to the extension's 1st gen limit of 3000 instances", () => { + expect(endpoint.maxInstances).toBe(3000); + }); + + test("accepts only internal traffic, as the extension did", () => { + expect(endpoint.ingressSettings).toBe("ALLOW_INTERNAL_ONLY"); + }); }); describe("generateResizedImage deploy region", () => { From 913d319f7d4a8555a986182eb6af969fbedd46db Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Tue, 27 Oct 2026 14:19:33 +0000 Subject: [PATCH 2/2] fix(storage-resize-images): leave the instance limit to setGlobalOptions A fixed limit of 3000 failed the deploy wherever it exceeded the regional Cloud Run quota. Ingress stays pinned to internal-only, as in the firestore-bigquery-export kit. --- kits/storage-resize-images/CHANGELOG.md | 2 +- kits/storage-resize-images/README.md | 25 +++++++++++++-- kits/storage-resize-images/src/index.ts | 6 ++-- .../tests/deploy-options.test.ts | 31 +++++++++++++++---- 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/kits/storage-resize-images/CHANGELOG.md b/kits/storage-resize-images/CHANGELOG.md index 5cb61cc49..9d118e797 100644 --- a/kits/storage-resize-images/CHANGELOG.md +++ b/kits/storage-resize-images/CHANGELOG.md @@ -1,4 +1,4 @@ -- fix: `generateResizedImage` now scales to 3000 instances and accepts only internal traffic, as the extension did. Left unset, the Firebase CLI deployed it with the 2nd gen defaults of 100 instances and `ALLOW_ALL` ingress, so with one event per instance at most 100 images resized at once. The next deploy updates the function in place; no `.env` change is needed. +- fix: `generateResizedImage` now accepts only internal traffic (`ALLOW_INTERNAL_ONLY`). Left unset, the Firebase CLI deployed it with `ALLOW_ALL`. Ingress set with `setGlobalOptions`, including by `ext:migrate`, does not apply to this function. Its instance limit stays at the 2nd gen default of 100; raise it with `setGlobalOptions({ maxInstances })`. - fix: `generateResizedImage` now handles one event per instance, as the extension's 1st gen trigger did, and sizes its CPU from `FUNCTION_MEMORY` the 1st gen way. Left unset, the Firebase CLI deployed it with 1 vCPU and a concurrency of 80, so a burst of uploads landed on a single instance and every resize shared its CPU. With 80 large images uploaded at once, 58 requests reached the 60s timeout and 56 images were never resized: the trigger has no retry policy, the timeouts were logged as HTTP 200 at INFO severity, and no `onError` event or failed-image copy was written. The next deploy updates the function in place; no `.env` change is needed. diff --git a/kits/storage-resize-images/README.md b/kits/storage-resize-images/README.md index 06bf445b3..ae6e86462 100644 --- a/kits/storage-resize-images/README.md +++ b/kits/storage-resize-images/README.md @@ -182,14 +182,33 @@ was 1st gen. `FUNCTION_MEMORY` still accepts the same values (512 through 8192) and maps onto the equivalent 2nd gen memory setting. Like the extension, the function handles one event per instance (`concurrency: 1`) and gets the CPU 1st gen gave that memory size (`cpu: "gcf_gen1"`), so a burst of uploads -scales out across instances instead of sharing one. It also keeps the -extension's limit of 3000 instances (`maxInstances: 3000`) and accepts only -internal traffic (`ingressSettings: "ALLOW_INTERNAL_ONLY"`). +scales out across instances instead of sharing one. + +`setGlobalOptions` can't change concurrency, CPU or ingress for this function. +Ingress is fixed to internal-only (`ALLOW_INTERNAL_ONLY`), since only Eventarc +calls the function. The extension let you choose ingress at install, and +`ext:migrate` passes that choice to `setGlobalOptions`, but it does not apply +here. The function's service account needs `roles/eventarc.eventReceiver` and `roles/run.invoker` on top of the roles the extension asked for. The Firebase CLI grants these for you. +### At most 100 images resize at once by default + +The function deploys with the 2nd gen default of 100 instances, where the +extension's 1st gen function could scale to 3000. With one event per instance, +that caps how many images resize at the same time. To raise it, call +`setGlobalOptions({ maxInstances })` from `firebase-functions/v2` in your +codebase. + +The total CPU and memory of those instances must fit your project's Cloud Run +quota in the function's region, so the highest value that deploys depends on +`FUNCTION_MEMORY` and the region. Too high a value fails the deploy with an +error such as `Max instances must be set to 2500 or fewer to set the requested +total CPU`. Lower `maxInstances`, or request a quota increase for Cloud Run in +that region. + ### Region `BUCKET_REGION` decides where the function deploys, as described below, rather diff --git a/kits/storage-resize-images/src/index.ts b/kits/storage-resize-images/src/index.ts index 181cb6078..86101061d 100644 --- a/kits/storage-resize-images/src/index.ts +++ b/kits/storage-resize-images/src/index.ts @@ -109,9 +109,9 @@ export const generateResizedImage = onObjectFinalized( // policy, so those images are never resized. concurrency: 1, cpu: "gcf_gen1", - // The extension's 1st gen limit. At concurrency 1, the 2nd gen default of - // 100 instances caps how many images resize at once. - maxInstances: 3000, + // Only Eventarc invokes this trigger. Pinned rather than read from + // `setGlobalOptions`, which may not have run yet when an ESM codebase + // imports the kit. `maxInstances` is left to `setGlobalOptions`. ingressSettings: "ALLOW_INTERNAL_ONLY", }, (event) => handleObjectFinalized(event, getContext()) diff --git a/kits/storage-resize-images/tests/deploy-options.test.ts b/kits/storage-resize-images/tests/deploy-options.test.ts index c4618614d..9ac840799 100644 --- a/kits/storage-resize-images/tests/deploy-options.test.ts +++ b/kits/storage-resize-images/tests/deploy-options.test.ts @@ -26,6 +26,7 @@ */ import { Expression } from "firebase-functions/params"; +import { setGlobalOptions } from "firebase-functions/v2"; import { afterEach, describe, expect, test, vi } from "vitest"; vi.mock("firebase-admin", async () => { @@ -82,7 +83,6 @@ describe("generateResizedImage instance sizing", () => { __endpoint: { concurrency?: number; cpu?: number | string; - maxInstances?: number; ingressSettings?: string; }; } @@ -96,11 +96,7 @@ describe("generateResizedImage instance sizing", () => { expect(endpoint.cpu).toBe("gcf_gen1"); }); - test("scales to the extension's 1st gen limit of 3000 instances", () => { - expect(endpoint.maxInstances).toBe(3000); - }); - - test("accepts only internal traffic, as the extension did", () => { + test("accepts only internal traffic", () => { expect(endpoint.ingressSettings).toBe("ALLOW_INTERNAL_ONLY"); }); }); @@ -134,3 +130,26 @@ describe("generateResizedImage deploy region", () => { expect(await importRegion(undefined)).toBeUndefined(); }); }); + +describe("generateResizedImage global options", () => { + // Global options persist for the rest of this file, so this runs last. + test("takes maxInstances from setGlobalOptions but keeps internal-only ingress", () => { + setGlobalOptions({ maxInstances: 500, ingressSettings: "ALLOW_ALL" }); + + const endpoint = ( + generateResizedImage as unknown as { + __endpoint: { + maxInstances?: number; + ingressSettings?: string; + concurrency?: number; + cpu?: number | string; + }; + } + ).__endpoint; + + expect(endpoint.maxInstances).toBe(500); + expect(endpoint.ingressSettings).toBe("ALLOW_INTERNAL_ONLY"); + expect(endpoint.concurrency).toBe(1); + expect(endpoint.cpu).toBe("gcf_gen1"); + }); +});