Skip to content

Commit aef0eb8

Browse files
committed
feat: Support and validate attemptDeadlineSeconds for GCFv2/Cloud Run scheduled functions.
1 parent 403d0f5 commit aef0eb8

File tree

5 files changed

+96
-15
lines changed

5 files changed

+96
-15
lines changed

src/deploy/functions/backend.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,20 @@ export function isValidMemoryOption(mem: unknown): mem is MemoryOptions {
180180
return allMemoryOptions.includes(mem as MemoryOptions);
181181
}
182182

183-
/**
184-
* Is a given string a valid VpcEgressSettings?
185-
*/
186183
export function isValidEgressSetting(egress: unknown): egress is VpcEgressSettings {
187184
return egress === "PRIVATE_RANGES_ONLY" || egress === "ALL_TRAFFIC";
188185
}
189186

187+
export const MIN_ATTEMPT_DEADLINE_SECONDS = 15;
188+
export const MAX_ATTEMPT_DEADLINE_SECONDS = 1800; // 30 mins
189+
190+
/**
191+
* Is a given number a valid attempt deadline?
192+
*/
193+
export function isValidAttemptDeadline(seconds: number): boolean {
194+
return seconds >= MIN_ATTEMPT_DEADLINE_SECONDS && seconds <= MAX_ATTEMPT_DEADLINE_SECONDS;
195+
}
196+
190197
/** Returns a human-readable name with MB or GB suffix for a MemoryOption (MB). */
191198
export function memoryOptionDisplayName(option: MemoryOptions): string {
192199
return {

src/deploy/functions/build.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,44 @@ describe("toBackend", () => {
224224
expect(endpointDef.func.serviceAccount).to.equal("service-account-1@");
225225
}
226226
});
227+
228+
it("throws if attemptDeadlineSeconds is out of range", () => {
229+
const desiredBuild: build.Build = build.of({
230+
func: {
231+
platform: "gcfv2",
232+
region: ["us-central1"],
233+
project: "project",
234+
runtime: "nodejs16",
235+
entryPoint: "func",
236+
scheduleTrigger: {
237+
schedule: "every 1 minutes",
238+
attemptDeadlineSeconds: 10, // Invalid: < 15
239+
},
240+
},
241+
});
242+
expect(() => build.toBackend(desiredBuild, {})).to.throw(
243+
FirebaseError,
244+
/attemptDeadlineSeconds must be between 15 and 1800 seconds/,
245+
);
246+
247+
const desiredBuild2: build.Build = build.of({
248+
func: {
249+
platform: "gcfv2",
250+
region: ["us-central1"],
251+
project: "project",
252+
runtime: "nodejs16",
253+
entryPoint: "func",
254+
scheduleTrigger: {
255+
schedule: "every 1 minutes",
256+
attemptDeadlineSeconds: 1801, // Invalid: > 1800
257+
},
258+
},
259+
});
260+
expect(() => build.toBackend(desiredBuild2, {})).to.throw(
261+
FirebaseError,
262+
/attemptDeadlineSeconds must be between 15 and 1800 seconds/,
263+
);
264+
});
227265
});
228266

229267
describe("envWithType", () => {

src/deploy/functions/build.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -605,9 +605,16 @@ function discoverTrigger(endpoint: Endpoint, region: string, r: Resolver): backe
605605
bkSchedule.retryConfig = null;
606606
}
607607
if (typeof endpoint.scheduleTrigger.attemptDeadlineSeconds !== "undefined") {
608-
bkSchedule.attemptDeadlineSeconds = r.resolveInt(
609-
endpoint.scheduleTrigger.attemptDeadlineSeconds,
610-
);
608+
const attemptDeadlineSeconds = r.resolveInt(endpoint.scheduleTrigger.attemptDeadlineSeconds);
609+
if (
610+
attemptDeadlineSeconds !== null &&
611+
!backend.isValidAttemptDeadline(attemptDeadlineSeconds)
612+
) {
613+
throw new FirebaseError(
614+
`attemptDeadlineSeconds must be between ${backend.MIN_ATTEMPT_DEADLINE_SECONDS} and ${backend.MAX_ATTEMPT_DEADLINE_SECONDS} seconds (inclusive).`,
615+
);
616+
}
617+
bkSchedule.attemptDeadlineSeconds = attemptDeadlineSeconds;
611618
}
612619
return { scheduleTrigger: bkSchedule };
613620
} else if ("taskQueueTrigger" in endpoint) {

src/gcp/cloudscheduler.spec.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ describe("cloudscheduler", () => {
284284
});
285285
});
286286

287-
it("should copy attemptDeadlineSeconds for v1 endpoints", async () => {
287+
it("should not copy attemptDeadlineSeconds for v1 endpoints", async () => {
288288
expect(
289289
await cloudscheduler.jobFromEndpoint(
290290
{
@@ -301,7 +301,6 @@ describe("cloudscheduler", () => {
301301
name: "projects/project/locations/appEngineLocation/jobs/firebase-schedule-id-region",
302302
schedule: "every 1 minutes",
303303
timeZone: "America/Los_Angeles",
304-
attemptDeadline: "300s",
305304
pubsubTarget: {
306305
topicName: "projects/project/topics/firebase-schedule-id-region",
307306
attributes: {
@@ -310,5 +309,33 @@ describe("cloudscheduler", () => {
310309
},
311310
});
312311
});
312+
313+
it("should copy attemptDeadlineSeconds for v2 endpoints", async () => {
314+
expect(
315+
await cloudscheduler.jobFromEndpoint(
316+
{
317+
...V2_ENDPOINT,
318+
scheduleTrigger: {
319+
schedule: "every 1 minutes",
320+
attemptDeadlineSeconds: 300,
321+
},
322+
},
323+
V2_ENDPOINT.region,
324+
"1234567",
325+
),
326+
).to.deep.equal({
327+
name: "projects/project/locations/region/jobs/firebase-schedule-id-region",
328+
schedule: "every 1 minutes",
329+
timeZone: "UTC",
330+
attemptDeadline: "300s",
331+
httpTarget: {
332+
uri: "https://my-uri.com",
333+
httpMethod: "POST",
334+
oidcToken: {
335+
serviceAccountEmail: "1234567-compute@developer.gserviceaccount.com",
336+
},
337+
},
338+
});
339+
});
313340
});
314341
});

src/gcp/cloudscheduler.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -262,13 +262,15 @@ export async function jobFromEndpoint(
262262
);
263263
}
264264
job.schedule = endpoint.scheduleTrigger.schedule;
265-
proto.convertIfPresent(
266-
job,
267-
endpoint.scheduleTrigger,
268-
"attemptDeadline",
269-
"attemptDeadlineSeconds",
270-
nullsafeVisitor(proto.durationFromSeconds),
271-
);
265+
if (endpoint.platform === "gcfv2" || endpoint.platform === "run") {
266+
proto.convertIfPresent(
267+
job,
268+
endpoint.scheduleTrigger,
269+
"attemptDeadline",
270+
"attemptDeadlineSeconds",
271+
nullsafeVisitor(proto.durationFromSeconds),
272+
);
273+
}
272274
if (endpoint.scheduleTrigger.retryConfig) {
273275
job.retryConfig = {};
274276
proto.copyIfPresent(

0 commit comments

Comments
 (0)