diff --git a/kits/storage-resize-images/CHANGELOG.md b/kits/storage-resize-images/CHANGELOG.md index 1a874569e..9d118e797 100644 --- a/kits/storage-resize-images/CHANGELOG.md +++ b/kits/storage-resize-images/CHANGELOG.md @@ -1,3 +1,5 @@ +- 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. - 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..ae6e86462 100644 --- a/kits/storage-resize-images/README.md +++ b/kits/storage-resize-images/README.md @@ -184,10 +184,31 @@ 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. +`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 18596f8ea..86101061d 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", + // 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 0edb8c3a6..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 () => { @@ -79,7 +80,11 @@ 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; + ingressSettings?: string; + }; } ).__endpoint; @@ -90,6 +95,10 @@ describe("generateResizedImage instance sizing", () => { test("sizes CPU from memory as 1st gen did", () => { expect(endpoint.cpu).toBe("gcf_gen1"); }); + + test("accepts only internal traffic", () => { + expect(endpoint.ingressSettings).toBe("ALLOW_INTERNAL_ONLY"); + }); }); describe("generateResizedImage deploy region", () => { @@ -121,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"); + }); +});