diff --git a/kits/delete-user-data/CHANGELOG.md b/kits/delete-user-data/CHANGELOG.md index 0e40c6a6f..a69845f22 100644 --- a/kits/delete-user-data/CHANGELOG.md +++ b/kits/delete-user-data/CHANGELOG.md @@ -1,3 +1,5 @@ +- fix: deploy `clearData`, `handleSearch` and `handleDeletion` with the extension's runtime settings: `concurrency: 1`, `cpu: "gcf_gen1"` (0.1666 vCPU) and `ingressSettings: "ALLOW_INTERNAL_ONLY"`. `setGlobalOptions` can't change these. Before, the kit took the 2nd gen defaults of concurrency `80`, 1 vCPU and `ALLOW_ALL`. With auto-discovery on, one 256MiB `handleSearch` instance took many searches at once, ran out of memory, and dropped every search in flight on it without an error, so user data was left undeleted. Existing kit deployments switch on their next deploy. + - 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 after the work, so the deletion completed but the invocation still failed. 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. The failed invocation used to be redelivered, re-running a deletion that had already succeeded; it no longer is. - fix: `ENABLE_AUTO_DISCOVERY` accepts the extension's `yes` / `no` values again. The kit had declared it as a boolean param, which only counts the literal `true` as enabled, so a `.env` copied from an installed extension instance carried `yes` and silently left auto-discovery switched off. The param is now the extension's labeled `Yes` / `No` select and is read the same way the extension read it. diff --git a/kits/delete-user-data/README.md b/kits/delete-user-data/README.md index 465d7a7cd..54d7d5247 100644 --- a/kits/delete-user-data/README.md +++ b/kits/delete-user-data/README.md @@ -202,6 +202,20 @@ Auth event `google.firebase.auth.user.v2.deleted` instead of the 1st gen This mainly matters if you have infrastructure or alerting keyed to function generation. +Each function still runs like the extension's: one event per instance +(`concurrency: 1`) with the 1st gen CPU for its memory (`cpu: "gcf_gen1"`, +0.1666 vCPU at the default 256MiB) and internal-only ingress, rather than the +2nd gen defaults of concurrency `80`, 1 vCPU and `ALLOW_ALL`. `setGlobalOptions` +can't change concurrency, CPU or ingress, so an ingress setting carried over by +`ext:migrate` doesn't apply to the kit. During a burst, Cloud Run may log +`no available instance` errors while it scales; Eventarc redelivers those +events. +One difference remains: 2nd gen caps each function at 100 instances by default, +where the extension's 1st gen functions could scale to 3000. To raise it, call +`setGlobalOptions({ maxInstances })` from `firebase-functions/v2` in your +codebase before you load the kit, since the kit's functions read the global +options when they are defined. + ### Events need an existing channel, and their source has changed The extension created its Eventarc channel when you installed it with events diff --git a/kits/delete-user-data/src/index.ts b/kits/delete-user-data/src/index.ts index cca4a43d0..2d981b536 100644 --- a/kits/delete-user-data/src/index.ts +++ b/kits/delete-user-data/src/index.ts @@ -109,7 +109,23 @@ function getContext(): HandlerContext { return ctx; } -export const clearData = onUserDeleted((event) => { +/** + * The extension's 1st gen functions ran at 0.1666 vCPU, one event per + * instance, with internal-only ingress. Without these options the CLI deploys + * the 2nd gen defaults: 1 vCPU, concurrency 80 and `ALLOW_ALL`. At concurrency + * 80, auto-discovery fans many `handleSearch` events onto one 256MiB instance, + * which runs out of memory and drops every event in flight on it. + * + * Ingress is always internal-only, so `setGlobalOptions({ ingressSettings })` + * doesn't change it, as in the firestore-bigquery-export kit. + */ +const EXTENSION_RUNTIME_OPTIONS = { + concurrency: 1, + cpu: "gcf_gen1", + ingressSettings: "ALLOW_INTERNAL_ONLY", +} as const; + +export const clearData = onUserDeleted(EXTENSION_RUNTIME_OPTIONS, (event) => { // The Auth event delivers no user record when the payload envelope is empty, // so bail before getContext() rather than initialising the SDKs for nothing. const uid = event.data?.uid; @@ -122,6 +138,7 @@ export const clearData = onUserDeleted((event) => { export const handleSearch = onMessagePublished( { + ...EXTENSION_RUNTIME_OPTIONS, topic: CONFIG_EXPRESSIONS.discoveryTopicName, }, (event) => runSearch(event.data.message.json, getContext()) @@ -129,6 +146,7 @@ export const handleSearch = onMessagePublished( export const handleDeletion = onMessagePublished( { + ...EXTENSION_RUNTIME_OPTIONS, topic: CONFIG_EXPRESSIONS.deletionTopicName, }, (event) => runDeletion(event.data.message.json, getContext()) diff --git a/kits/delete-user-data/tests/index.test.ts b/kits/delete-user-data/tests/index.test.ts index b031ab638..c8709200e 100644 --- a/kits/delete-user-data/tests/index.test.ts +++ b/kits/delete-user-data/tests/index.test.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { beforeEach, describe, expect, test, vi } from "vitest"; +import { setGlobalOptions } from "firebase-functions/v2"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; vi.mock("../src/logs"); vi.mock("../src/handlers", async (importOriginal) => ({ @@ -23,7 +24,7 @@ vi.mock("../src/handlers", async (importOriginal) => ({ })); import { handleClear } from "../src/handlers"; -import { clearData } from "../src/index"; +import { clearData, handleDeletion, handleSearch } from "../src/index"; import * as logs from "../src/logs"; function deletionEvent(data: unknown) { @@ -68,3 +69,53 @@ describe("clearData", () => { expect(logs.deletionEventMissingUid).toHaveBeenCalledWith("event-id"); }); }); + +describe("runtime options", () => { + // Concurrency 80 let auto-discovery fan many searches onto one 256MiB + // instance, which ran out of memory and dropped them. + test.each([ + ["clearData", clearData], + ["handleSearch", handleSearch], + ["handleDeletion", handleDeletion], + ])( + "%s runs one event per gen1 CPU instance with internal-only ingress", + (_name, fn) => { + const endpoint = (fn as any).__endpoint; + + expect(endpoint.concurrency).toBe(1); + expect(endpoint.cpu).toBe("gcf_gen1"); + expect(endpoint.ingressSettings).toBe("ALLOW_INTERNAL_ONLY"); + } + ); + + describe("with global options set", () => { + afterEach(() => { + delete (globalThis as any)[ + Symbol.for("firebase-functions:v2:globalOptions") + ].value; + }); + + // The README tells users to raise maxInstances and set the region with + // setGlobalOptions, so the kit's function-level options must not override + // them. Ingress stays internal-only, like the extension's. + test("keeps the global options and the extension's runtime", async () => { + setGlobalOptions({ + ingressSettings: "ALLOW_ALL", + maxInstances: 3000, + region: "europe-west1", + }); + vi.resetModules(); + const kit = await import("../src/index"); + + for (const fn of [kit.clearData, kit.handleSearch, kit.handleDeletion]) { + const endpoint = (fn as any).__endpoint; + + expect(endpoint.ingressSettings).toBe("ALLOW_INTERNAL_ONLY"); + expect(endpoint.maxInstances).toBe(3000); + expect(endpoint.region).toEqual(["europe-west1"]); + expect(endpoint.concurrency).toBe(1); + expect(endpoint.cpu).toBe("gcf_gen1"); + } + }); + }); +});