diff --git a/packages/core/src/ScheduledTaskManager.ts b/packages/core/src/ScheduledTaskManager.ts index ce656f3716303..eb519a60a74bf 100644 --- a/packages/core/src/ScheduledTaskManager.ts +++ b/packages/core/src/ScheduledTaskManager.ts @@ -1,13 +1,24 @@ import { Service } from 'typedi'; import { CronJob } from 'cron'; import type { CronExpression, Workflow } from 'n8n-workflow'; +import { InstanceSettings } from './InstanceSettings'; @Service() export class ScheduledTaskManager { + constructor(private readonly instanceSettings: InstanceSettings) {} + readonly cronJobs = new Map(); registerCron(workflow: Workflow, cronExpression: CronExpression, onTick: () => void) { - const cronJob = new CronJob(cronExpression, onTick, undefined, true, workflow.timezone); + const cronJob = new CronJob( + cronExpression, + () => { + if (this.instanceSettings.isLeader) onTick(); + }, + undefined, + true, + workflow.timezone, + ); const cronJobsForWorkflow = this.cronJobs.get(workflow.id); if (cronJobsForWorkflow) { cronJobsForWorkflow.push(cronJob); diff --git a/packages/core/test/ScheduledTaskManager.test.ts b/packages/core/test/ScheduledTaskManager.test.ts index df7fb9b77e555..15d5f7d487052 100644 --- a/packages/core/test/ScheduledTaskManager.test.ts +++ b/packages/core/test/ScheduledTaskManager.test.ts @@ -1,9 +1,11 @@ import type { Workflow } from 'n8n-workflow'; import { mock } from 'jest-mock-extended'; +import type { InstanceSettings } from '@/InstanceSettings'; import { ScheduledTaskManager } from '@/ScheduledTaskManager'; describe('ScheduledTaskManager', () => { + const instanceSettings = mock({ isLeader: true }); const workflow = mock({ timezone: 'GMT' }); const everyMinute = '0 * * * * *'; const onTick = jest.fn(); @@ -13,7 +15,7 @@ describe('ScheduledTaskManager', () => { beforeEach(() => { jest.clearAllMocks(); jest.useFakeTimers(); - scheduledTaskManager = new ScheduledTaskManager(); + scheduledTaskManager = new ScheduledTaskManager(instanceSettings); }); it('should throw when workflow timezone is invalid', () => { @@ -41,6 +43,15 @@ describe('ScheduledTaskManager', () => { expect(onTick).toHaveBeenCalledTimes(10); }); + it('should should not invoke on follower instances', async () => { + scheduledTaskManager = new ScheduledTaskManager(mock({ isLeader: false })); + scheduledTaskManager.registerCron(workflow, everyMinute, onTick); + + expect(onTick).not.toHaveBeenCalled(); + jest.advanceTimersByTime(10 * 60 * 1000); // 10 minutes + expect(onTick).not.toHaveBeenCalled(); + }); + it('should deregister CronJobs for a workflow', async () => { scheduledTaskManager.registerCron(workflow, everyMinute, onTick); scheduledTaskManager.registerCron(workflow, everyMinute, onTick); diff --git a/packages/nodes-base/nodes/Schedule/tests/ScheduleTrigger.node.test.ts b/packages/nodes-base/nodes/Schedule/tests/ScheduleTrigger.node.test.ts index fa1d2cd615c9c..0693806f4c009 100644 --- a/packages/nodes-base/nodes/Schedule/tests/ScheduleTrigger.node.test.ts +++ b/packages/nodes-base/nodes/Schedule/tests/ScheduleTrigger.node.test.ts @@ -1,6 +1,6 @@ import * as n8nWorkflow from 'n8n-workflow'; import type { INode, ITriggerFunctions, Workflow } from 'n8n-workflow'; -import { returnJsonArray } from 'n8n-core'; +import { type InstanceSettings, returnJsonArray } from 'n8n-core'; import { ScheduledTaskManager } from 'n8n-core/dist/ScheduledTaskManager'; import { mock } from 'jest-mock-extended'; import { ScheduleTrigger } from '../ScheduleTrigger.node'; @@ -18,7 +18,8 @@ describe('ScheduleTrigger', () => { const node = mock({ typeVersion: 1 }); const workflow = mock({ timezone }); - const scheduledTaskManager = new ScheduledTaskManager(); + const instanceSettings = mock({ isLeader: true }); + const scheduledTaskManager = new ScheduledTaskManager(instanceSettings); const helpers = mock({ returnJsonArray, registerCron: (cronExpression, onTick) =>