diff --git a/CHANGELOG.md b/CHANGELOG.md index f80b20b3933..33b4dabd2e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ +- Improve performance and reliability when deploying multiple 2nd gen functions using single builds. (#6376) - Fixed an issue where `emulators:export` did not check if the target folder is empty. (#6313) - Fix "Could not find the next executable" on Next.js deployments (#6372) diff --git a/src/deploy/functions/release/fabricator.ts b/src/deploy/functions/release/fabricator.ts index ff4556168a9..c36799ef256 100644 --- a/src/deploy/functions/release/fabricator.ts +++ b/src/deploy/functions/release/fabricator.ts @@ -130,17 +130,22 @@ export class Fabricator { }; const upserts: Array> = []; - const scraper = new SourceTokenScraper(); + const scraperV1 = new SourceTokenScraper(); + const scraperV2 = new SourceTokenScraper(); for (const endpoint of changes.endpointsToCreate) { this.logOpStart("creating", endpoint); - upserts.push(handle("create", endpoint, () => this.createEndpoint(endpoint, scraper))); + upserts.push( + handle("create", endpoint, () => this.createEndpoint(endpoint, scraperV1, scraperV2)) + ); } for (const endpoint of changes.endpointsToSkip) { utils.logSuccess(this.getLogSuccessMessage("skip", endpoint)); } for (const update of changes.endpointsToUpdate) { this.logOpStart("updating", update.endpoint); - upserts.push(handle("update", update.endpoint, () => this.updateEndpoint(update, scraper))); + upserts.push( + handle("update", update.endpoint, () => this.updateEndpoint(update, scraperV1, scraperV2)) + ); } await utils.allSettled(upserts); @@ -167,12 +172,16 @@ export class Fabricator { return deployResults; } - async createEndpoint(endpoint: backend.Endpoint, scraper: SourceTokenScraper): Promise { + async createEndpoint( + endpoint: backend.Endpoint, + scraperV1: SourceTokenScraper, + scraperV2: SourceTokenScraper + ): Promise { endpoint.labels = { ...endpoint.labels, ...deploymentTool.labels() }; if (endpoint.platform === "gcfv1") { - await this.createV1Function(endpoint, scraper); + await this.createV1Function(endpoint, scraperV1); } else if (endpoint.platform === "gcfv2") { - await this.createV2Function(endpoint); + await this.createV2Function(endpoint, scraperV2); } else { assertExhaustive(endpoint.platform); } @@ -180,18 +189,22 @@ export class Fabricator { await this.setTrigger(endpoint); } - async updateEndpoint(update: planner.EndpointUpdate, scraper: SourceTokenScraper): Promise { + async updateEndpoint( + update: planner.EndpointUpdate, + scraperV1: SourceTokenScraper, + scraperV2: SourceTokenScraper + ): Promise { update.endpoint.labels = { ...update.endpoint.labels, ...deploymentTool.labels() }; if (update.deleteAndRecreate) { await this.deleteEndpoint(update.deleteAndRecreate); - await this.createEndpoint(update.endpoint, scraper); + await this.createEndpoint(update.endpoint, scraperV1, scraperV2); return; } if (update.endpoint.platform === "gcfv1") { - await this.updateV1Function(update.endpoint, scraper); + await this.updateV1Function(update.endpoint, scraperV1); } else if (update.endpoint.platform === "gcfv2") { - await this.updateV2Function(update.endpoint); + await this.updateV2Function(update.endpoint, scraperV2); } else { assertExhaustive(update.endpoint.platform); } @@ -276,7 +289,7 @@ export class Fabricator { } } - async createV2Function(endpoint: backend.Endpoint): Promise { + async createV2Function(endpoint: backend.Endpoint, scraper: SourceTokenScraper): Promise { const storageSource = this.sources[endpoint.codebase!]?.storage; if (!storageSource) { logger.debug("Precondition failed. Cannot create a GCFv2 function without storage"); @@ -351,14 +364,19 @@ export class Fabricator { while (!resultFunction) { resultFunction = await this.functionExecutor .run(async () => { + apiFunction.buildConfig.sourceToken = await scraper.getToken(); const op: { name: string } = await gcfV2.createFunction(apiFunction); return await poller.pollOperation({ ...gcfV2PollerOptions, pollerName: `create-${endpoint.codebase}-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name, + onPoll: scraper.poller, }); }) .catch(async (err: any) => { + // Abort waiting on source token so other concurrent calls don't get stuck + scraper.abort(); + // If the createFunction call returns RPC error code RESOURCE_EXHAUSTED (8), // we have exhausted the underlying Cloud Run API quota. To retry, we need to // first delete the GCF function resource, then call createFunction again. @@ -463,7 +481,7 @@ export class Fabricator { } } - async updateV2Function(endpoint: backend.Endpoint): Promise { + async updateV2Function(endpoint: backend.Endpoint, scraper: SourceTokenScraper): Promise { const storageSource = this.sources[endpoint.codebase!]?.storage; if (!storageSource) { logger.debug("Precondition failed. Cannot update a GCFv2 function without storage"); @@ -482,16 +500,22 @@ export class Fabricator { const resultFunction = await this.functionExecutor .run( async () => { + apiFunction.buildConfig.sourceToken = await scraper.getToken(); const op: { name: string } = await gcfV2.updateFunction(apiFunction); return await poller.pollOperation({ ...gcfV2PollerOptions, pollerName: `update-${endpoint.codebase}-${endpoint.region}-${endpoint.id}`, operationResourceName: op.name, + onPoll: scraper.poller, }); }, { retryCodes: [...DEFAULT_RETRY_CODES, CLOUD_RUN_RESOURCE_EXHAUSTED_CODE] } ) - .catch(rethrowAs(endpoint, "update")); + .catch((err: any) => { + scraper.abort(); + logger.error((err as Error).message); + throw new reporter.DeploymentError(endpoint, "update", err); + }); endpoint.uri = resultFunction.serviceConfig?.uri; const serviceName = resultFunction.serviceConfig?.service; diff --git a/src/deploy/functions/release/sourceTokenScraper.ts b/src/deploy/functions/release/sourceTokenScraper.ts index 96b5a04d742..8a013072c4c 100644 --- a/src/deploy/functions/release/sourceTokenScraper.ts +++ b/src/deploy/functions/release/sourceTokenScraper.ts @@ -3,6 +3,10 @@ import { assertExhaustive } from "../../../functional"; import { logger } from "../../../logger"; type TokenFetchState = "NONE" | "FETCHING" | "VALID"; +interface TokenFetchResult { + token?: string; + aborted: boolean; +} /** * GCF v1 deploys support reusing a build between function deploys. @@ -11,8 +15,8 @@ type TokenFetchState = "NONE" | "FETCHING" | "VALID"; */ export class SourceTokenScraper { private tokenValidDurationMs; - private resolve!: (token?: string) => void; - private promise: Promise; + private resolve!: (token: TokenFetchResult) => void; + private promise: Promise; private expiry: number | undefined; private fetchState: TokenFetchState; @@ -22,19 +26,29 @@ export class SourceTokenScraper { this.fetchState = "NONE"; } + abort(): void { + this.resolve({ aborted: true }); + } + async getToken(): Promise { if (this.fetchState === "NONE") { this.fetchState = "FETCHING"; return undefined; } else if (this.fetchState === "FETCHING") { - return this.promise; // wait until we get a source token + const tokenResult = await this.promise; + if (tokenResult.aborted) { + this.promise = new Promise((resolve) => (this.resolve = resolve)); + return undefined; + } + return tokenResult.token; } else if (this.fetchState === "VALID") { + const tokenResult = await this.promise; if (this.isTokenExpired()) { this.fetchState = "FETCHING"; this.promise = new Promise((resolve) => (this.resolve = resolve)); return undefined; } - return this.promise; + return tokenResult.token; } else { assertExhaustive(this.fetchState); } @@ -58,7 +72,10 @@ export class SourceTokenScraper { const [, , , /* projects*/ /* project*/ /* regions*/ region] = op.metadata?.target?.split("/") || []; logger.debug(`Got source token ${op.metadata?.sourceToken} for region ${region as string}`); - this.resolve(op.metadata?.sourceToken); + this.resolve({ + token: op.metadata?.sourceToken, + aborted: false, + }); this.fetchState = "VALID"; this.expiry = Date.now() + this.tokenValidDurationMs; } diff --git a/src/gcp/cloudfunctionsv2.ts b/src/gcp/cloudfunctionsv2.ts index 1bea4985fcb..8098e3d9794 100644 --- a/src/gcp/cloudfunctionsv2.ts +++ b/src/gcp/cloudfunctionsv2.ts @@ -41,6 +41,7 @@ export interface BuildConfig { runtime: runtimes.Runtime; entryPoint: string; source: Source; + sourceToken?: string; environmentVariables: Record; // Output only @@ -320,6 +321,11 @@ export async function createFunction(cloudFunction: InputCloudFunction): Promise GOOGLE_NODE_RUN_SCRIPTS: "", }; + cloudFunction.serviceConfig.environmentVariables = { + ...cloudFunction.serviceConfig.environmentVariables, + FUNCTION_TARGET: functionId, + }; + try { const res = await client.post( components.join("/"), @@ -404,6 +410,8 @@ async function listFunctionsInternal( * Customers can force a field to be deleted by setting that field to `undefined` */ export async function updateFunction(cloudFunction: InputCloudFunction): Promise { + const components = cloudFunction.name.split("/"); + const functionId = components.splice(-1, 1)[0]; // Keys in labels and environmentVariables and secretEnvironmentVariables are user defined, so we don't recurse // for field masks. const fieldMasks = proto.fieldMasks( @@ -420,6 +428,12 @@ export async function updateFunction(cloudFunction: InputCloudFunction): Promise GOOGLE_NODE_RUN_SCRIPTS: "", }; fieldMasks.push("buildConfig.buildEnvironmentVariables"); + + cloudFunction.serviceConfig.environmentVariables = { + ...cloudFunction.serviceConfig.environmentVariables, + FUNCTION_TARGET: functionId, + }; + try { const queryParams = { updateMask: fieldMasks.join(","), diff --git a/src/test/deploy/functions/release/fabricator.spec.ts b/src/test/deploy/functions/release/fabricator.spec.ts index 99b8e5e0517..0e3e7cc8dc5 100644 --- a/src/test/deploy/functions/release/fabricator.spec.ts +++ b/src/test/deploy/functions/release/fabricator.spec.ts @@ -455,7 +455,7 @@ describe("Fabricator", () => { } ); - await fab.createV2Function(ep); + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); expect(pubsub.createTopic).to.have.been.called; expect(gcfv2.createFunction).to.have.been.called; }); @@ -476,7 +476,7 @@ describe("Fabricator", () => { } ); - await expect(fab.createV2Function(ep)).to.be.rejectedWith( + await expect(fab.createV2Function(ep, new scraper.SourceTokenScraper())).to.be.rejectedWith( reporter.DeploymentError, "create topic" ); @@ -500,7 +500,7 @@ describe("Fabricator", () => { } ); - await fab.createV2Function(ep); + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); expect(eventarc.getChannel).to.have.been.called; expect(eventarc.createChannel).to.not.have.been.called; expect(gcfv2.createFunction).to.have.been.called; @@ -530,7 +530,7 @@ describe("Fabricator", () => { } ); - await fab.createV2Function(ep); + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); expect(eventarc.createChannel).to.have.been.called; expect(gcfv2.createFunction).to.have.been.called; }); @@ -568,7 +568,7 @@ describe("Fabricator", () => { } ); - await fab.createV2Function(ep); + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); expect(eventarc.createChannel).to.have.been.calledOnceWith({ name: channelName }); expect(poller.pollOperation).to.have.been.called; }); @@ -594,22 +594,51 @@ describe("Fabricator", () => { } ); - await expect(fab.createV2Function(ep)).to.eventually.be.rejectedWith( - reporter.DeploymentError, - "upsert eventarc channel" - ); + await expect( + fab.createV2Function(ep, new scraper.SourceTokenScraper()) + ).to.eventually.be.rejectedWith(reporter.DeploymentError, "upsert eventarc channel"); }); it("throws on create function failure", async () => { gcfv2.createFunction.rejects(new Error("Server failure")); const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - await expect(fab.createV2Function(ep)).to.be.rejectedWith(reporter.DeploymentError, "create"); + await expect(fab.createV2Function(ep, new scraper.SourceTokenScraper())).to.be.rejectedWith( + reporter.DeploymentError, + "create" + ); gcfv2.createFunction.resolves({ name: "op", done: false }); poller.pollOperation.rejects(new Error("Fail whale")); - await expect(fab.createV2Function(ep)).to.be.rejectedWith(reporter.DeploymentError, "create"); + await expect(fab.createV2Function(ep, new scraper.SourceTokenScraper())).to.be.rejectedWith( + reporter.DeploymentError, + "create" + ); + }); + + it("tries to grab new token on abort", async () => { + const sc = new scraper.SourceTokenScraper(); + sc.poller({ + metadata: { + sourceToken: "magic token", + target: "projects/p/locations/l/functions/f", + }, + }); + + gcfv2.createFunction.onFirstCall().rejects({ message: "unknown" }); + gcfv2.createFunction.resolves({ name: "op", done: true }); + + const ep1 = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); + const ep2 = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); + const fn1 = fab.createV2Function(ep1, sc); + const fn2 = fab.createV2Function(ep2, sc); + try { + await Promise.all([fn1, fn2]); + } catch (err) { + // do nothing, error is expected + } + await expect(sc.getToken()).to.eventually.equal("magic token"); }); it("deletes broken function and retries on cloud run quota exhaustion", async () => { @@ -620,7 +649,7 @@ describe("Fabricator", () => { poller.pollOperation.resolves({ name: "op" }); const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - await fab.createV2Function(ep); + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); expect(gcfv2.createFunction).to.have.been.calledTwice; expect(gcfv2.deleteFunction).to.have.been.called; @@ -632,7 +661,7 @@ describe("Fabricator", () => { run.setInvokerCreate.rejects(new Error("Boom")); const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - await expect(fab.createV2Function(ep)).to.be.rejectedWith( + await expect(fab.createV2Function(ep, new scraper.SourceTokenScraper())).to.be.rejectedWith( reporter.DeploymentError, "set invoker" ); @@ -645,7 +674,7 @@ describe("Fabricator", () => { run.setInvokerCreate.resolves(); const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - await fab.createV2Function(ep); + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); expect(run.setInvokerCreate).to.have.been.calledWith(ep.project, "service", ["public"]); }); @@ -662,7 +691,7 @@ describe("Fabricator", () => { { platform: "gcfv2" } ); - await fab.createV2Function(ep); + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); expect(run.setInvokerCreate).to.have.been.calledWith(ep.project, "service", ["custom@"]); }); @@ -672,7 +701,7 @@ describe("Fabricator", () => { run.setInvokerCreate.resolves(); const ep = endpoint({ httpsTrigger: { invoker: ["private"] } }, { platform: "gcfv2" }); - await fab.createV2Function(ep); + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); expect(run.setInvokerCreate).to.not.have.been.called; }); }); @@ -684,7 +713,7 @@ describe("Fabricator", () => { run.setInvokerCreate.resolves(); const ep = endpoint({ callableTrigger: {} }, { platform: "gcfv2" }); - await fab.createV2Function(ep); + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); expect(run.setInvokerCreate).to.have.been.calledWith(ep.project, "service", ["public"]); }); }); @@ -696,7 +725,7 @@ describe("Fabricator", () => { run.setInvokerCreate.resolves(); const ep = endpoint({ taskQueueTrigger: {} }, { platform: "gcfv2" }); - await fab.createV2Function(ep); + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); expect(run.setInvokerCreate).to.not.have.been.called; }); @@ -712,7 +741,7 @@ describe("Fabricator", () => { }, { platform: "gcfv2" } ); - await fab.createV2Function(ep); + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); expect(run.setInvokerCreate).to.have.been.calledWith(ep.project, "service", ["custom@"]); }); }); @@ -727,7 +756,7 @@ describe("Fabricator", () => { { platform: "gcfv2" } ); - await fab.createV2Function(ep); + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); expect(run.setInvokerCreate).to.have.been.calledWith(ep.project, "service", ["public"]); }); }); @@ -741,7 +770,7 @@ describe("Fabricator", () => { { platform: "gcfv2" } ); - await fab.createV2Function(ep); + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); expect(run.setInvokerCreate).to.not.have.been.called; }); }); @@ -751,11 +780,17 @@ describe("Fabricator", () => { gcfv2.updateFunction.rejects(new Error("Server failure")); const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - await expect(fab.updateV2Function(ep)).to.be.rejectedWith(reporter.DeploymentError, "update"); + await expect(fab.updateV2Function(ep, new scraper.SourceTokenScraper())).to.be.rejectedWith( + reporter.DeploymentError, + "update" + ); gcfv2.updateFunction.resolves({ name: "op", done: false }); poller.pollOperation.rejects(new Error("Fail whale")); - await expect(fab.updateV2Function(ep)).to.be.rejectedWith(reporter.DeploymentError, "update"); + await expect(fab.updateV2Function(ep, new scraper.SourceTokenScraper())).to.be.rejectedWith( + reporter.DeploymentError, + "update" + ); }); it("throws on set invoker failure", async () => { @@ -764,7 +799,7 @@ describe("Fabricator", () => { run.setInvokerUpdate.rejects(new Error("Boom")); const ep = endpoint({ httpsTrigger: { invoker: ["private"] } }, { platform: "gcfv2" }); - await expect(fab.updateV2Function(ep)).to.be.rejectedWith( + await expect(fab.updateV2Function(ep, new scraper.SourceTokenScraper())).to.be.rejectedWith( reporter.DeploymentError, "set invoker" ); @@ -783,7 +818,7 @@ describe("Fabricator", () => { { platform: "gcfv2" } ); - await fab.updateV2Function(ep); + await fab.updateV2Function(ep, new scraper.SourceTokenScraper()); expect(run.setInvokerUpdate).to.have.been.calledWith(ep.project, "service", ["custom@"]); }); @@ -800,7 +835,7 @@ describe("Fabricator", () => { { platform: "gcfv2" } ); - await fab.updateV2Function(ep); + await fab.updateV2Function(ep, new scraper.SourceTokenScraper()); expect(run.setInvokerUpdate).to.have.been.calledWith(ep.project, "service", ["custom@"]); }); @@ -817,7 +852,7 @@ describe("Fabricator", () => { { platform: "gcfv2" } ); - await fab.updateV2Function(ep); + await fab.updateV2Function(ep, new scraper.SourceTokenScraper()); expect(run.setInvokerUpdate).to.have.been.calledWith(ep.project, "service", ["public"]); }); @@ -827,7 +862,7 @@ describe("Fabricator", () => { run.setInvokerUpdate.resolves(); const ep = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); - await fab.updateV2Function(ep); + await fab.updateV2Function(ep, new scraper.SourceTokenScraper()); expect(run.setInvokerUpdate).to.not.have.been.called; }); @@ -840,9 +875,33 @@ describe("Fabricator", () => { { platform: "gcfv2" } ); - await fab.updateV2Function(ep); + await fab.updateV2Function(ep, new scraper.SourceTokenScraper()); expect(run.setInvokerUpdate).to.not.have.been.called; }); + + it("tries to grab new token on abort", async () => { + const sc = new scraper.SourceTokenScraper(); + sc.poller({ + metadata: { + sourceToken: "magic token", + target: "projects/p/locations/l/functions/f", + }, + }); + + gcfv2.updateFunction.onFirstCall().rejects({ message: "unknown" }); + gcfv2.updateFunction.resolves({ name: "op", done: true }); + + const ep1 = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); + const ep2 = endpoint({ httpsTrigger: {} }, { platform: "gcfv2" }); + const fn1 = fab.updateV2Function(ep1, sc); + const fn2 = fab.updateV2Function(ep2, sc); + try { + await Promise.all([fn1, fn2]); + } catch (err) { + // do nothing, error is expected + } + await expect(sc.getToken()).to.eventually.equal("magic token"); + }); }); describe("deleteV2Function", () => { @@ -1205,7 +1264,11 @@ describe("Fabricator", () => { const createV1Function = sinon.stub(fab, "createV1Function"); createV1Function.resolves(); - await fab.createEndpoint(ep, new scraper.SourceTokenScraper()); + await fab.createEndpoint( + ep, + new scraper.SourceTokenScraper(), + new scraper.SourceTokenScraper() + ); expect(createV1Function).is.calledOnce; expect(setTrigger).is.calledOnce; expect(setTrigger).is.calledAfter(createV1Function); @@ -1218,7 +1281,11 @@ describe("Fabricator", () => { const createV2Function = sinon.stub(fab, "createV2Function"); createV2Function.resolves(); - await fab.createEndpoint(ep, new scraper.SourceTokenScraper()); + await fab.createEndpoint( + ep, + new scraper.SourceTokenScraper(), + new scraper.SourceTokenScraper() + ); expect(createV2Function).is.calledOnce; expect(setTrigger).is.calledOnce; expect(setTrigger).is.calledAfter(createV2Function); @@ -1230,10 +1297,9 @@ describe("Fabricator", () => { const createV1Function = sinon.stub(fab, "createV1Function"); createV1Function.rejects(new reporter.DeploymentError(ep, "set invoker", undefined)); - await expect(fab.createEndpoint(ep, new scraper.SourceTokenScraper())).to.be.rejectedWith( - reporter.DeploymentError, - "set invoker" - ); + await expect( + fab.createEndpoint(ep, new scraper.SourceTokenScraper(), new scraper.SourceTokenScraper()) + ).to.be.rejectedWith(reporter.DeploymentError, "set invoker"); expect(createV1Function).is.calledOnce; expect(setTrigger).is.not.called; }); @@ -1247,7 +1313,11 @@ describe("Fabricator", () => { const updateV1Function = sinon.stub(fab, "updateV1Function"); updateV1Function.resolves(); - await fab.updateEndpoint({ endpoint: ep }, new scraper.SourceTokenScraper()); + await fab.updateEndpoint( + { endpoint: ep }, + new scraper.SourceTokenScraper(), + new scraper.SourceTokenScraper() + ); expect(updateV1Function).is.calledOnce; expect(setTrigger).is.calledOnce; expect(setTrigger).is.calledAfter(updateV1Function); @@ -1260,7 +1330,11 @@ describe("Fabricator", () => { const updateV2Function = sinon.stub(fab, "updateV2Function"); updateV2Function.resolves(); - await fab.updateEndpoint({ endpoint: ep }, new scraper.SourceTokenScraper()); + await fab.updateEndpoint( + { endpoint: ep }, + new scraper.SourceTokenScraper(), + new scraper.SourceTokenScraper() + ); expect(updateV2Function).is.calledOnce; expect(setTrigger).is.calledOnce; expect(setTrigger).is.calledAfter(updateV2Function); @@ -1273,7 +1347,11 @@ describe("Fabricator", () => { updateV1Function.rejects(new reporter.DeploymentError(ep, "set invoker", undefined)); await expect( - fab.updateEndpoint({ endpoint: ep }, new scraper.SourceTokenScraper()) + fab.updateEndpoint( + { endpoint: ep }, + new scraper.SourceTokenScraper(), + new scraper.SourceTokenScraper() + ) ).to.be.rejectedWith(reporter.DeploymentError, "set invoker"); expect(updateV1Function).is.calledOnce; expect(setTrigger).is.not.called; @@ -1302,7 +1380,11 @@ describe("Fabricator", () => { const createV2Function = sinon.stub(fab, "createV2Function"); createV2Function.resolves(); - await fab.updateEndpoint(update, new scraper.SourceTokenScraper()); + await fab.updateEndpoint( + update, + new scraper.SourceTokenScraper(), + new scraper.SourceTokenScraper() + ); expect(deleteTrigger).to.have.been.called; expect(deleteV1Function).to.have.been.calledImmediatelyAfter(deleteTrigger); diff --git a/src/test/deploy/functions/release/sourceTokenScraper.spec.ts b/src/test/deploy/functions/release/sourceTokenScraper.spec.ts index 28a0490e99e..ee7c4dea3ab 100644 --- a/src/test/deploy/functions/release/sourceTokenScraper.spec.ts +++ b/src/test/deploy/functions/release/sourceTokenScraper.spec.ts @@ -66,6 +66,20 @@ describe("SourceTokenScraper", () => { await expect(scraper.getToken()).to.eventually.equal("magic token #2"); }); + it("tries to fetch a new source token upon abort", async () => { + const scraper = new SourceTokenScraper(); + await expect(scraper.getToken()).to.eventually.be.undefined; + scraper.abort(); + await expect(scraper.getToken()).to.eventually.be.undefined; + scraper.poller({ + metadata: { + sourceToken: "magic token", + target: "projects/p/locations/l/functions/f", + }, + }); + await expect(scraper.getToken()).to.eventually.equal("magic token"); + }); + it("concurrent requests for source token", async () => { const scraper = new SourceTokenScraper();