diff --git a/src/deploy/functions/backend.ts b/src/deploy/functions/backend.ts index 411ed90d466..592bb1a46d7 100644 --- a/src/deploy/functions/backend.ts +++ b/src/deploy/functions/backend.ts @@ -26,6 +26,7 @@ export interface ScheduleTrigger { schedule?: string; timeZone?: string | null; retryConfig?: ScheduleRetryConfig | null; + attemptDeadlineSeconds?: number | null; } /** Something that has a ScheduleTrigger */ diff --git a/src/deploy/functions/build.ts b/src/deploy/functions/build.ts index c786a22c907..2013e5a23da 100644 --- a/src/deploy/functions/build.ts +++ b/src/deploy/functions/build.ts @@ -147,6 +147,7 @@ export interface ScheduleTrigger { schedule: string | Expression; timeZone?: Field; retryConfig?: ScheduleRetryConfig | null; + attemptDeadlineSeconds?: Field; } export type HttpsTriggered = { httpsTrigger: HttpsTrigger }; diff --git a/src/deploy/functions/runtimes/discovery/v1alpha1.ts b/src/deploy/functions/runtimes/discovery/v1alpha1.ts index 8afaeb1f016..8bfd9cea5ec 100644 --- a/src/deploy/functions/runtimes/discovery/v1alpha1.ts +++ b/src/deploy/functions/runtimes/discovery/v1alpha1.ts @@ -222,6 +222,7 @@ function assertBuildEndpoint(ep: WireEndpoint, id: string): void { schedule: "Field", timeZone: "Field?", retryConfig: "object?", + attemptDeadlineSeconds: "Field?", }); if (ep.scheduleTrigger.retryConfig) { assertKeyTypes(prefix + ".scheduleTrigger.retryConfig", ep.scheduleTrigger.retryConfig, { diff --git a/src/gcp/cloudscheduler.ts b/src/gcp/cloudscheduler.ts index 50608d6e588..97325e15f54 100644 --- a/src/gcp/cloudscheduler.ts +++ b/src/gcp/cloudscheduler.ts @@ -267,21 +267,27 @@ export async function jobFromEndpoint( } job.schedule = endpoint.scheduleTrigger.schedule; if (endpoint.platform === "gcfv2" || endpoint.platform === "run") { - proto.convertIfPresent(job, endpoint, "attemptDeadline", "timeoutSeconds", (timeout) => { - if (timeout === null) { - return null; - } - // Cloud Scheduler has an attempt deadline range of [15s, 1800s], and defaults to 180s. - // We floor at 180s to be safe, even if the function timeout is shorter. - // This is because GCF/Cloud Run will already terminate the function at its configured timeout, - // so Cloud Scheduler won't actually wait the full 180s unless GCF itself fails to respond. - // Setting it shorter than 180s might cause premature retries due to network latency. - const attemptDeadlineSeconds = Math.max( - Math.min(timeout, MAX_V2_SCHEDULE_ATTEMPT_DEADLINE_SECONDS), - DEFAULT_V2_SCHEDULE_ATTEMPT_DEADLINE_SECONDS, + if (endpoint.scheduleTrigger.attemptDeadlineSeconds) { + job.attemptDeadline = proto.durationFromSeconds( + endpoint.scheduleTrigger.attemptDeadlineSeconds, ); - return proto.durationFromSeconds(attemptDeadlineSeconds); - }); + } else { + proto.convertIfPresent(job, endpoint, "attemptDeadline", "timeoutSeconds", (timeout) => { + if (timeout === null) { + return null; + } + // Cloud Scheduler has an attempt deadline range of [15s, 1800s], and defaults to 180s. + // We floor at 180s to be safe, even if the function timeout is shorter. + // This is because GCF/Cloud Run will already terminate the function at its configured timeout, + // so Cloud Scheduler won't actually wait the full 180s unless GCF itself fails to respond. + // Setting it shorter than 180s might cause premature retries due to network latency. + const attemptDeadlineSeconds = Math.max( + Math.min(timeout, MAX_V2_SCHEDULE_ATTEMPT_DEADLINE_SECONDS), + DEFAULT_V2_SCHEDULE_ATTEMPT_DEADLINE_SECONDS, + ); + return proto.durationFromSeconds(attemptDeadlineSeconds); + }); + } } if (endpoint.scheduleTrigger.retryConfig) { job.retryConfig = {};