Skip to content

Commit d04add6

Browse files
authored
Improve performance and reliability when deploying multiple 2nd gen functions using single builds (#6376)
* enable single builds v2
1 parent 67f7480 commit d04add6

File tree

6 files changed

+209
-57
lines changed

6 files changed

+209
-57
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
- Improve performance and reliability when deploying multiple 2nd gen functions using single builds. (#6376)
12
- Fixed an issue where `emulators:export` did not check if the target folder is empty. (#6313)
23
- Fix "Could not find the next executable" on Next.js deployments (#6372)

src/deploy/functions/release/fabricator.ts

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -130,17 +130,22 @@ export class Fabricator {
130130
};
131131

132132
const upserts: Array<Promise<void>> = [];
133-
const scraper = new SourceTokenScraper();
133+
const scraperV1 = new SourceTokenScraper();
134+
const scraperV2 = new SourceTokenScraper();
134135
for (const endpoint of changes.endpointsToCreate) {
135136
this.logOpStart("creating", endpoint);
136-
upserts.push(handle("create", endpoint, () => this.createEndpoint(endpoint, scraper)));
137+
upserts.push(
138+
handle("create", endpoint, () => this.createEndpoint(endpoint, scraperV1, scraperV2))
139+
);
137140
}
138141
for (const endpoint of changes.endpointsToSkip) {
139142
utils.logSuccess(this.getLogSuccessMessage("skip", endpoint));
140143
}
141144
for (const update of changes.endpointsToUpdate) {
142145
this.logOpStart("updating", update.endpoint);
143-
upserts.push(handle("update", update.endpoint, () => this.updateEndpoint(update, scraper)));
146+
upserts.push(
147+
handle("update", update.endpoint, () => this.updateEndpoint(update, scraperV1, scraperV2))
148+
);
144149
}
145150
await utils.allSettled(upserts);
146151

@@ -167,31 +172,39 @@ export class Fabricator {
167172
return deployResults;
168173
}
169174

170-
async createEndpoint(endpoint: backend.Endpoint, scraper: SourceTokenScraper): Promise<void> {
175+
async createEndpoint(
176+
endpoint: backend.Endpoint,
177+
scraperV1: SourceTokenScraper,
178+
scraperV2: SourceTokenScraper
179+
): Promise<void> {
171180
endpoint.labels = { ...endpoint.labels, ...deploymentTool.labels() };
172181
if (endpoint.platform === "gcfv1") {
173-
await this.createV1Function(endpoint, scraper);
182+
await this.createV1Function(endpoint, scraperV1);
174183
} else if (endpoint.platform === "gcfv2") {
175-
await this.createV2Function(endpoint);
184+
await this.createV2Function(endpoint, scraperV2);
176185
} else {
177186
assertExhaustive(endpoint.platform);
178187
}
179188

180189
await this.setTrigger(endpoint);
181190
}
182191

183-
async updateEndpoint(update: planner.EndpointUpdate, scraper: SourceTokenScraper): Promise<void> {
192+
async updateEndpoint(
193+
update: planner.EndpointUpdate,
194+
scraperV1: SourceTokenScraper,
195+
scraperV2: SourceTokenScraper
196+
): Promise<void> {
184197
update.endpoint.labels = { ...update.endpoint.labels, ...deploymentTool.labels() };
185198
if (update.deleteAndRecreate) {
186199
await this.deleteEndpoint(update.deleteAndRecreate);
187-
await this.createEndpoint(update.endpoint, scraper);
200+
await this.createEndpoint(update.endpoint, scraperV1, scraperV2);
188201
return;
189202
}
190203

191204
if (update.endpoint.platform === "gcfv1") {
192-
await this.updateV1Function(update.endpoint, scraper);
205+
await this.updateV1Function(update.endpoint, scraperV1);
193206
} else if (update.endpoint.platform === "gcfv2") {
194-
await this.updateV2Function(update.endpoint);
207+
await this.updateV2Function(update.endpoint, scraperV2);
195208
} else {
196209
assertExhaustive(update.endpoint.platform);
197210
}
@@ -276,7 +289,7 @@ export class Fabricator {
276289
}
277290
}
278291

279-
async createV2Function(endpoint: backend.Endpoint): Promise<void> {
292+
async createV2Function(endpoint: backend.Endpoint, scraper: SourceTokenScraper): Promise<void> {
280293
const storageSource = this.sources[endpoint.codebase!]?.storage;
281294
if (!storageSource) {
282295
logger.debug("Precondition failed. Cannot create a GCFv2 function without storage");
@@ -351,14 +364,19 @@ export class Fabricator {
351364
while (!resultFunction) {
352365
resultFunction = await this.functionExecutor
353366
.run(async () => {
367+
apiFunction.buildConfig.sourceToken = await scraper.getToken();
354368
const op: { name: string } = await gcfV2.createFunction(apiFunction);
355369
return await poller.pollOperation<gcfV2.OutputCloudFunction>({
356370
...gcfV2PollerOptions,
357371
pollerName: `create-${endpoint.codebase}-${endpoint.region}-${endpoint.id}`,
358372
operationResourceName: op.name,
373+
onPoll: scraper.poller,
359374
});
360375
})
361376
.catch(async (err: any) => {
377+
// Abort waiting on source token so other concurrent calls don't get stuck
378+
scraper.abort();
379+
362380
// If the createFunction call returns RPC error code RESOURCE_EXHAUSTED (8),
363381
// we have exhausted the underlying Cloud Run API quota. To retry, we need to
364382
// first delete the GCF function resource, then call createFunction again.
@@ -463,7 +481,7 @@ export class Fabricator {
463481
}
464482
}
465483

466-
async updateV2Function(endpoint: backend.Endpoint): Promise<void> {
484+
async updateV2Function(endpoint: backend.Endpoint, scraper: SourceTokenScraper): Promise<void> {
467485
const storageSource = this.sources[endpoint.codebase!]?.storage;
468486
if (!storageSource) {
469487
logger.debug("Precondition failed. Cannot update a GCFv2 function without storage");
@@ -482,16 +500,22 @@ export class Fabricator {
482500
const resultFunction = await this.functionExecutor
483501
.run(
484502
async () => {
503+
apiFunction.buildConfig.sourceToken = await scraper.getToken();
485504
const op: { name: string } = await gcfV2.updateFunction(apiFunction);
486505
return await poller.pollOperation<gcfV2.OutputCloudFunction>({
487506
...gcfV2PollerOptions,
488507
pollerName: `update-${endpoint.codebase}-${endpoint.region}-${endpoint.id}`,
489508
operationResourceName: op.name,
509+
onPoll: scraper.poller,
490510
});
491511
},
492512
{ retryCodes: [...DEFAULT_RETRY_CODES, CLOUD_RUN_RESOURCE_EXHAUSTED_CODE] }
493513
)
494-
.catch(rethrowAs<gcfV2.OutputCloudFunction>(endpoint, "update"));
514+
.catch((err: any) => {
515+
scraper.abort();
516+
logger.error((err as Error).message);
517+
throw new reporter.DeploymentError(endpoint, "update", err);
518+
});
495519

496520
endpoint.uri = resultFunction.serviceConfig?.uri;
497521
const serviceName = resultFunction.serviceConfig?.service;

src/deploy/functions/release/sourceTokenScraper.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { assertExhaustive } from "../../../functional";
33
import { logger } from "../../../logger";
44

55
type TokenFetchState = "NONE" | "FETCHING" | "VALID";
6+
interface TokenFetchResult {
7+
token?: string;
8+
aborted: boolean;
9+
}
610

711
/**
812
* GCF v1 deploys support reusing a build between function deploys.
@@ -11,8 +15,8 @@ type TokenFetchState = "NONE" | "FETCHING" | "VALID";
1115
*/
1216
export class SourceTokenScraper {
1317
private tokenValidDurationMs;
14-
private resolve!: (token?: string) => void;
15-
private promise: Promise<string | undefined>;
18+
private resolve!: (token: TokenFetchResult) => void;
19+
private promise: Promise<TokenFetchResult>;
1620
private expiry: number | undefined;
1721
private fetchState: TokenFetchState;
1822

@@ -22,19 +26,29 @@ export class SourceTokenScraper {
2226
this.fetchState = "NONE";
2327
}
2428

29+
abort(): void {
30+
this.resolve({ aborted: true });
31+
}
32+
2533
async getToken(): Promise<string | undefined> {
2634
if (this.fetchState === "NONE") {
2735
this.fetchState = "FETCHING";
2836
return undefined;
2937
} else if (this.fetchState === "FETCHING") {
30-
return this.promise; // wait until we get a source token
38+
const tokenResult = await this.promise;
39+
if (tokenResult.aborted) {
40+
this.promise = new Promise((resolve) => (this.resolve = resolve));
41+
return undefined;
42+
}
43+
return tokenResult.token;
3144
} else if (this.fetchState === "VALID") {
45+
const tokenResult = await this.promise;
3246
if (this.isTokenExpired()) {
3347
this.fetchState = "FETCHING";
3448
this.promise = new Promise((resolve) => (this.resolve = resolve));
3549
return undefined;
3650
}
37-
return this.promise;
51+
return tokenResult.token;
3852
} else {
3953
assertExhaustive(this.fetchState);
4054
}
@@ -58,7 +72,10 @@ export class SourceTokenScraper {
5872
const [, , , /* projects*/ /* project*/ /* regions*/ region] =
5973
op.metadata?.target?.split("/") || [];
6074
logger.debug(`Got source token ${op.metadata?.sourceToken} for region ${region as string}`);
61-
this.resolve(op.metadata?.sourceToken);
75+
this.resolve({
76+
token: op.metadata?.sourceToken,
77+
aborted: false,
78+
});
6279
this.fetchState = "VALID";
6380
this.expiry = Date.now() + this.tokenValidDurationMs;
6481
}

src/gcp/cloudfunctionsv2.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface BuildConfig {
4141
runtime: runtimes.Runtime;
4242
entryPoint: string;
4343
source: Source;
44+
sourceToken?: string;
4445
environmentVariables: Record<string, string>;
4546

4647
// Output only
@@ -320,6 +321,11 @@ export async function createFunction(cloudFunction: InputCloudFunction): Promise
320321
GOOGLE_NODE_RUN_SCRIPTS: "",
321322
};
322323

324+
cloudFunction.serviceConfig.environmentVariables = {
325+
...cloudFunction.serviceConfig.environmentVariables,
326+
FUNCTION_TARGET: functionId,
327+
};
328+
323329
try {
324330
const res = await client.post<typeof cloudFunction, Operation>(
325331
components.join("/"),
@@ -404,6 +410,8 @@ async function listFunctionsInternal(
404410
* Customers can force a field to be deleted by setting that field to `undefined`
405411
*/
406412
export async function updateFunction(cloudFunction: InputCloudFunction): Promise<Operation> {
413+
const components = cloudFunction.name.split("/");
414+
const functionId = components.splice(-1, 1)[0];
407415
// Keys in labels and environmentVariables and secretEnvironmentVariables are user defined, so we don't recurse
408416
// for field masks.
409417
const fieldMasks = proto.fieldMasks(
@@ -420,6 +428,12 @@ export async function updateFunction(cloudFunction: InputCloudFunction): Promise
420428
GOOGLE_NODE_RUN_SCRIPTS: "",
421429
};
422430
fieldMasks.push("buildConfig.buildEnvironmentVariables");
431+
432+
cloudFunction.serviceConfig.environmentVariables = {
433+
...cloudFunction.serviceConfig.environmentVariables,
434+
FUNCTION_TARGET: functionId,
435+
};
436+
423437
try {
424438
const queryParams = {
425439
updateMask: fieldMasks.join(","),

0 commit comments

Comments
 (0)