diff --git a/src/deploy/functions/release/fabricator.ts b/src/deploy/functions/release/fabricator.ts index d0c7d5e1ce3..49630b1052a 100644 --- a/src/deploy/functions/release/fabricator.ts +++ b/src/deploy/functions/release/fabricator.ts @@ -52,6 +52,9 @@ const eventarcPollerOptions: Omit { if (experiments.isEnabled("functionsv2deployoptimizations")) { - apiFunction.buildConfig.sourceToken = await scraper.getToken(); + apiFunction.buildConfig.sourceToken = await scraper.getToken({ + timeoutMs: SOURCE_TOKEN_FETCH_TIMEOUT_MS, + }); } const op: { name: string } = await gcfV2.createFunction(apiFunction); return await poller.pollOperation({ @@ -515,7 +520,9 @@ export class Fabricator { .run( async () => { if (experiments.isEnabled("functionsv2deployoptimizations")) { - apiFunction.buildConfig.sourceToken = await scraper.getToken(); + apiFunction.buildConfig.sourceToken = await scraper.getToken({ + timeoutMs: SOURCE_TOKEN_FETCH_TIMEOUT_MS, + }); } const op: { name: string } = await gcfV2.updateFunction(apiFunction); return await poller.pollOperation({ diff --git a/src/deploy/functions/release/sourceTokenScraper.spec.ts b/src/deploy/functions/release/sourceTokenScraper.spec.ts index e3cd4d329f1..00a2a4cf9d5 100644 --- a/src/deploy/functions/release/sourceTokenScraper.spec.ts +++ b/src/deploy/functions/release/sourceTokenScraper.spec.ts @@ -42,6 +42,24 @@ describe("SourceTokenScraper", () => { await expect(scraper.getToken()).to.eventually.equal("magic token"); }); + it("returns early when configured with a timeout", async () => { + const scraper = new SourceTokenScraper(); + // Put the scraper into FETCHING state. + await scraper.getToken(); + + const start = Date.now(); + await expect(scraper.getToken({ timeoutMs: 5 })).to.eventually.be.undefined; + expect(Date.now() - start).to.be.lessThan(100); + + scraper.poller({ + metadata: { + sourceToken: "magic token", + target: "projects/p/locations/l/functions/f", + }, + }); + await expect(scraper.getToken()).to.eventually.equal("magic token"); + }); + it("refreshes token after timer expires", async () => { const scraper = new SourceTokenScraper(10); await expect(scraper.getToken()).to.eventually.be.undefined; diff --git a/src/deploy/functions/release/sourceTokenScraper.ts b/src/deploy/functions/release/sourceTokenScraper.ts index 6cdfc1c368d..3b33a96e9b9 100644 --- a/src/deploy/functions/release/sourceTokenScraper.ts +++ b/src/deploy/functions/release/sourceTokenScraper.ts @@ -3,6 +3,14 @@ import { assertExhaustive } from "../../../functional"; import { logger } from "../../../logger"; type TokenFetchState = "NONE" | "FETCHING" | "VALID"; +interface TokenFetchOptions { + /** + * Maximum time to wait for a token before returning undefined. + * Keeps callers from blocking the entire deploy when the backend + * never returns a source token (as is possible with some APIs). + */ + timeoutMs?: number; +} interface TokenFetchResult { token?: string; aborted: boolean; @@ -30,19 +38,25 @@ export class SourceTokenScraper { this.resolve({ aborted: true }); } - async getToken(): Promise { + async getToken(options: TokenFetchOptions = {}): Promise { if (this.fetchState === "NONE") { this.fetchState = "FETCHING"; return undefined; } else if (this.fetchState === "FETCHING") { - const tokenResult = await this.promise; + const tokenResult = await this.waitForToken(options.timeoutMs); + if (!tokenResult) { + return undefined; + } 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; + const tokenResult = await this.waitForToken(options.timeoutMs); + if (!tokenResult) { + return undefined; + } if (this.isTokenExpired()) { this.fetchState = "FETCHING"; this.promise = new Promise((resolve) => (this.resolve = resolve)); @@ -54,6 +68,16 @@ export class SourceTokenScraper { } } + private async waitForToken(timeoutMs?: number): Promise { + if (timeoutMs === undefined) { + return this.promise; + } + return Promise.race([ + this.promise, + new Promise((resolve) => setTimeout(resolve, timeoutMs)), + ]); + } + isTokenExpired(): boolean { if (this.expiry === undefined) { throw new FirebaseError(