Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions kits/storage-resize-images/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
21 changes: 21 additions & 0 deletions kits/storage-resize-images/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions kits/storage-resize-images/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
);
34 changes: 33 additions & 1 deletion kits/storage-resize-images/tests/deploy-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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;

Expand All @@ -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", () => {
Expand Down Expand Up @@ -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");
});
});
Loading