diff --git a/packages/@n8n/config/src/configs/__tests__/scheduler.config.test.ts b/packages/@n8n/config/src/configs/__tests__/scheduler.config.test.ts index b09770ca8d2..bcf9d3944ed 100644 --- a/packages/@n8n/config/src/configs/__tests__/scheduler.config.test.ts +++ b/packages/@n8n/config/src/configs/__tests__/scheduler.config.test.ts @@ -43,7 +43,7 @@ describe('SchedulerConfig', () => { expect(scheduler.allowSkipDurableScheduler).toBe(false); expect(scheduler.maxAttempts).toBe(5); expect(scheduler.enabledForPollTriggers).toBe(false); - expect(scheduler.pollTimeoutSeconds).toBe(60); + expect(scheduler.pollTimeoutSeconds).toBe(45); }); }); diff --git a/packages/@n8n/config/src/configs/scheduler.config.ts b/packages/@n8n/config/src/configs/scheduler.config.ts index 8d4a8869189..933d4ef8eb7 100644 --- a/packages/@n8n/config/src/configs/scheduler.config.ts +++ b/packages/@n8n/config/src/configs/scheduler.config.ts @@ -270,20 +270,22 @@ export class SchedulerConfig { /** * How long, in seconds, a single poll of an external source (an inbox, an API) - * may take before it is abandoned. Defaults to 60 seconds. + * may take before it is abandoned. Defaults to 45 seconds. * * An abandoned poll skips no data: its position in the source is left where it * was and the next scheduled poll covers the same ground. It counts as a poll * failure, so a source that keeps timing out is polled at a widening interval. * Guards against a poll stuck on an unresponsive source running indefinitely. * - * Keep it at or below {@link leaseDurationSeconds}: a poll allowed to run - * longer than the claim on its run can still be in flight when that claim - * expires and another instance takes the run over. The scheduler warns at - * startup if it doesn't. Must be greater than 0. + * Keep it below {@link leaseDurationSeconds}: the deadline only starts after + * the occurrence's setup reads, so a poll allowed to run as long as the claim + * on its run can still be in flight when that claim expires and another + * instance takes the run over. The default leaves that headroom, and the + * scheduler warns at startup when the timeout reaches the lease duration. + * Must be greater than 0. */ @Env('N8N_SCHEDULER_POLL_TIMEOUT', positiveIntSchema) - pollTimeoutSeconds: number = Time.minutes.toSeconds; + pollTimeoutSeconds: number = 45; /** * Whether a poll trigger's cursor advance and the execution it produced are saved diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index 98810659acb..75c7ec6ba96 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -478,7 +478,7 @@ describe('GlobalConfig', () => { maxConcurrentPasses: 10, triggerNodeMode: 'legacy', enabledForPollTriggers: false, - pollTimeoutSeconds: 60, + pollTimeoutSeconds: 45, allowSkipDurableScheduler: false, maxAttempts: 5, misfireGraceSeconds: 60, diff --git a/packages/cli/src/scheduling/__tests__/durable-scheduler.test.ts b/packages/cli/src/scheduling/__tests__/durable-scheduler.test.ts index 05b249123ad..49580434cd4 100644 --- a/packages/cli/src/scheduling/__tests__/durable-scheduler.test.ts +++ b/packages/cli/src/scheduling/__tests__/durable-scheduler.test.ts @@ -32,7 +32,7 @@ describe('DurableScheduler', () => { materializationWindowSeconds = 60, misfireGraceSeconds = 60, enabledForPollTriggers = false, - pollTimeoutSeconds = 60, + pollTimeoutSeconds = 45, leaseDurationSeconds = 60, } = {}) { const inner = mock(); @@ -185,13 +185,28 @@ describe('DurableScheduler', () => { ); }); - it('does not warn when the timeout fits inside the lease', () => { + // The poll deadline starts after the occurrence's setup reads, so a timeout + // equal to the lease already lets a full-length poll outlive it. + it('warns when the timeout equals the lease', () => { const { logger } = makeScheduler({ enabledForPollTriggers: true, pollTimeoutSeconds: 60, leaseDurationSeconds: 60, }); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining('poll timeout'), + expect.objectContaining({ pollTimeoutSeconds: 60, leaseDurationSeconds: 60 }), + ); + }); + + it('does not warn when the timeout fits inside the lease', () => { + const { logger } = makeScheduler({ + enabledForPollTriggers: true, + pollTimeoutSeconds: 45, + leaseDurationSeconds: 60, + }); + expect(logger.warn).not.toHaveBeenCalledWith( expect.stringContaining('poll timeout'), expect.anything(), diff --git a/packages/cli/src/scheduling/durable-scheduler.ts b/packages/cli/src/scheduling/durable-scheduler.ts index 74ea5b2df98..d0bae4ae1c8 100644 --- a/packages/cli/src/scheduling/durable-scheduler.ts +++ b/packages/cli/src/scheduling/durable-scheduler.ts @@ -166,13 +166,14 @@ function warnOnDrainRate(logger: Logger, config: GlobalConfig['scheduler']): voi /** * Warn when a poll may still be in flight after the lease on its occurrence has * expired: the reaper can then reclaim the occurrence and another instance can - * start the same poll while the first one is still running. + * start the same poll while the first one is still running. Equality counts + * too, since the poll deadline only starts after the occurrence's setup reads. */ function warnOnPollTimeout(logger: Logger, config: GlobalConfig['scheduler']): void { const { enabledForPollTriggers, pollTimeoutSeconds, leaseDurationSeconds } = config; - if (enabledForPollTriggers && pollTimeoutSeconds > leaseDurationSeconds) { + if (enabledForPollTriggers && pollTimeoutSeconds >= leaseDurationSeconds) { logger.warn( - 'Scheduler poll timeout is above the lease duration; a poll can still be running when its lease expires and another instance takes the run over', + 'Scheduler poll timeout reaches the lease duration; a poll can still be running when its lease expires and another instance takes the run over', { pollTimeoutSeconds, leaseDurationSeconds }, ); }