From 66e468ce40d34929e907e71f56cfd9687d8e7111 Mon Sep 17 00:00:00 2001 From: Thomas Schaaf Date: Wed, 22 Jan 2025 14:47:23 +0100 Subject: [PATCH 1/3] feat: add waitForCompletion for cronjobs to disable concurrent running cronjobs --- lib/decorators/cron.decorator.ts | 6 ++++++ tests/e2e/cron-jobs.spec.ts | 26 ++++++++++++++++++++++++++ tests/src/cron.service.ts | 14 ++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/lib/decorators/cron.decorator.ts b/lib/decorators/cron.decorator.ts index a98a4059..ff560922 100644 --- a/lib/decorators/cron.decorator.ts +++ b/lib/decorators/cron.decorator.ts @@ -30,6 +30,12 @@ export type CronOptions = { */ unrefTimeout?: boolean; + /** + * If true, no additional instances of cronjob will run until the current onTick callback has completed. + * Any new scheduled executions that occur while the current cronjob is running will be skipped entirely. + */ + waitForCompletion?: boolean; + /** * This flag indicates whether the job will be executed at all. * @default false diff --git a/tests/e2e/cron-jobs.spec.ts b/tests/e2e/cron-jobs.spec.ts index ce0bf93a..f8b9c8e8 100644 --- a/tests/e2e/cron-jobs.spec.ts +++ b/tests/e2e/cron-jobs.spec.ts @@ -82,6 +82,31 @@ describe('Cron', () => { expect(job.running).toBe(false); }); + it(`should run "cron" once after 30 seconds`, async () => { + const service = app.get(CronService); + + await app.init(); + const registry = app.get(SchedulerRegistry); + const job = registry.getCronJob('WAIT_FOR_COMPLETION'); + deleteAllRegisteredJobsExceptOne(registry, 'WAIT_FOR_COMPLETION'); + + expect(job.running).toBe(true); + expect(service.callsCount).toEqual(0); + + clock.tick('30'); + expect(service.callsCount).toEqual(1); + expect(job.lastDate()).toEqual(new Date('2020-01-01T00:00:30.000Z')); + + clock.tick('31'); + expect(service.callsCount).toEqual(1); + expect(job.lastDate()).toEqual(new Date('2020-01-01T00:00:30.000Z')); + + clock.tick('32'); + expect(service.callsCount).toEqual(2); + expect(job.lastDate()).toEqual(new Date('2020-01-01T00:00:32.000Z')); + expect(job.running).toBe(false); + }); + it(`should run "cron" 3 times every 60 seconds`, async () => { const service = app.get(CronService); @@ -118,6 +143,7 @@ describe('Cron', () => { expect(job.running).toBe(false); }); + it(`should not run "cron" at all`, async () => { const service = app.get(CronService); diff --git a/tests/src/cron.service.ts b/tests/src/cron.service.ts index d32e9ee9..b692c406 100644 --- a/tests/src/cron.service.ts +++ b/tests/src/cron.service.ts @@ -75,6 +75,20 @@ export class CronService { return job; } + @Cron(CronExpression.EVERY_SECOND, { + name: 'WAIT_FOR_COMPLETION', + waitForCompletion: true, + }) + async handleLongRunningCron() { + ++this.callsCount; + await new Promise(r => setTimeout(r, 1000)); + + if (this.callsCount > 2) { + const ref = this.schedulerRegistry.getCronJob('WAIT_FOR_COMPLETION'); + ref!.stop(); + } + } + doesExist(name: string): boolean { return this.schedulerRegistry.doesExist('cron', name); } From 47d998738d8f7fc50aa9654f25b839acb50e3654 Mon Sep 17 00:00:00 2001 From: Thomas Schaaf Date: Wed, 22 Jan 2025 16:01:36 +0100 Subject: [PATCH 2/3] tests: fix waitForCompletion test --- tests/e2e/cron-jobs.spec.ts | 64 ++++++++++++++++++++++++++++++++----- tests/src/cron.service.ts | 8 +++-- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/tests/e2e/cron-jobs.spec.ts b/tests/e2e/cron-jobs.spec.ts index f8b9c8e8..c9aea358 100644 --- a/tests/e2e/cron-jobs.spec.ts +++ b/tests/e2e/cron-jobs.spec.ts @@ -82,7 +82,14 @@ describe('Cron', () => { expect(job.running).toBe(false); }); - it(`should run "cron" once after 30 seconds`, async () => { + it.only(`should wait for "cron" to complete`, async () => { + // run every minute for 61 seconds + // 00:01:00 - 00:02:01 + // 00:02:00 - skipped + // 00:03:00 - 00:04:01 + // 00:04:00 - skipped + // 00:05:00 - 00:06:01 + const service = app.get(CronService); await app.init(); @@ -93,17 +100,59 @@ describe('Cron', () => { expect(job.running).toBe(true); expect(service.callsCount).toEqual(0); - clock.tick('30'); + await clock.tickAsync('01:00'); + // 00:01:00 expect(service.callsCount).toEqual(1); - expect(job.lastDate()).toEqual(new Date('2020-01-01T00:00:30.000Z')); + expect(service.callsFinishedCount).toEqual(0); + expect(job.lastDate()).toEqual(new Date('2020-01-01T00:01:00.000Z')); - clock.tick('31'); + await clock.tickAsync('00:01'); + // 00:01:01 expect(service.callsCount).toEqual(1); - expect(job.lastDate()).toEqual(new Date('2020-01-01T00:00:30.000Z')); + expect(service.callsFinishedCount).toEqual(0); - clock.tick('32'); + await clock.tickAsync('00:59'); + // 00:02:00 + expect(service.callsCount).toEqual(1); + expect(service.callsFinishedCount).toEqual(0); + + await clock.tickAsync('00:01'); + // 00:02:01 + expect(service.callsCount).toEqual(1); + expect(service.callsFinishedCount).toEqual(1); + expect(job.lastDate()).toEqual(new Date('2020-01-01T00:02:00.000Z')); + + await clock.tickAsync('00:59'); + // 00:03:00 expect(service.callsCount).toEqual(2); - expect(job.lastDate()).toEqual(new Date('2020-01-01T00:00:32.000Z')); + expect(service.callsFinishedCount).toEqual(1); + + await clock.tickAsync('00:01'); + // 00:03:01 + expect(service.callsCount).toEqual(2); + expect(service.callsFinishedCount).toEqual(1); + expect(job.lastDate()).toEqual(new Date('2020-01-01T00:03:00.000Z')); + + await clock.tickAsync('00:59'); + // 00:04:00 + expect(service.callsCount).toEqual(2); + expect(service.callsFinishedCount).toEqual(1); + + await clock.tickAsync('00:01'); + // 00:04:01 + expect(service.callsCount).toEqual(2); + expect(service.callsFinishedCount).toEqual(2); + expect(job.lastDate()).toEqual(new Date('2020-01-01T00:04:00.000Z')); + + await clock.tickAsync('00:59'); + // 00:05:00 + expect(service.callsCount).toEqual(3); + expect(service.callsFinishedCount).toEqual(2); + + await clock.tickAsync('01:01'); + // 00:06:01 + expect(service.callsCount).toEqual(3); + expect(service.callsFinishedCount).toEqual(3); expect(job.running).toBe(false); }); @@ -143,7 +192,6 @@ describe('Cron', () => { expect(job.running).toBe(false); }); - it(`should not run "cron" at all`, async () => { const service = app.get(CronService); diff --git a/tests/src/cron.service.ts b/tests/src/cron.service.ts index b692c406..648fd5c4 100644 --- a/tests/src/cron.service.ts +++ b/tests/src/cron.service.ts @@ -7,6 +7,7 @@ import { CronJob } from 'cron'; @Injectable() export class CronService { callsCount = 0; + callsFinishedCount = 0; dynamicCallsCount = 0; constructor(private readonly schedulerRegistry: SchedulerRegistry) {} @@ -75,14 +76,15 @@ export class CronService { return job; } - @Cron(CronExpression.EVERY_SECOND, { + @Cron(CronExpression.EVERY_MINUTE, { name: 'WAIT_FOR_COMPLETION', waitForCompletion: true, }) async handleLongRunningCron() { ++this.callsCount; - await new Promise(r => setTimeout(r, 1000)); - + await new Promise((r) => setTimeout(r, 61 * 1000)); + ++this.callsFinishedCount; + if (this.callsCount > 2) { const ref = this.schedulerRegistry.getCronJob('WAIT_FOR_COMPLETION'); ref!.stop(); From fa58ebe7763157be6791905b052c1df58359a61c Mon Sep 17 00:00:00 2001 From: Kamil Mysliwiec Date: Thu, 23 Jan 2025 09:56:38 +0100 Subject: [PATCH 3/3] Update tests/e2e/cron-jobs.spec.ts --- tests/e2e/cron-jobs.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/cron-jobs.spec.ts b/tests/e2e/cron-jobs.spec.ts index c9aea358..071de19f 100644 --- a/tests/e2e/cron-jobs.spec.ts +++ b/tests/e2e/cron-jobs.spec.ts @@ -82,7 +82,7 @@ describe('Cron', () => { expect(job.running).toBe(false); }); - it.only(`should wait for "cron" to complete`, async () => { + it(`should wait for "cron" to complete`, async () => { // run every minute for 61 seconds // 00:01:00 - 00:02:01 // 00:02:00 - skipped