Search code examples
javascriptscheduled-tasksnestjs

Is it possible to keep scheduled tasks from overlapping in NestJS?


Currently, long running tasks will overlap (same task runs multiple instances at the same time) if the time necessary to finish the ask is greater than the interval. example NestJS service below

import { Injectable, Logger } from '@nestjs/common';
import { Interval } from '@nestjs/schedule';

function timeout(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

let c = 1

@Injectable()
export class TasksService {
  private readonly logger = new Logger(TasksService.name);

  @Interval(1000)
  async handleCron() {
    this.logger.debug(`start ${c}`);
    await timeout(3000)
    this.logger.debug(`end ${c}`);
    c += 1
  }
}

Is it possible to keep these tasks from overlapping and only calling the task with one instance at a time? Technically, we could keep track of a lock variable, but this would only allow us to skip an instance, if one is already running. Ideally, we could call set an option to allow intervals based on task end-time, rather than fixed intervals (aka start-time).


Solution

  • It is easily done in NestJS, but you will need to refactor your code.

    Instead of using @Interval, use the @Cron since it has more options that we will use in the next lines.

    Let us replace first the interval decorator:

    @Interval(1000)
      async handleCron() {
        this.logger.debug(`start ${c}`);
        await timeout(3000)
        this.logger.debug(`end ${c}`);
        c += 1
      }
    

    becomes:

    @Cron(CronExpression.EVERY_SECOND, {
         name: 'cron_job_name_here',
       })
    async handleCron() {
            this.logger.debug(`start ${c}`);
            await timeout(3000)
            this.logger.debug(`end ${c}`);
            c += 1
       }
    

    What we have done here, is that we used the cron expression which is more expressive, even non programmers could understand that this job will run every second. Also, we named the cron job, so now we can manipulate the cron job through the scheduler's module APIs.

    Now that our cron job has a name, we can start/stop it easily using the scheduler registry. So you function becomes:

    @Cron(CronExpression.EVERY_SECOND, {
         name: 'cron_job_name_here',
       })
    async handleCron() {
            const job = this.schedulerRegistry.getCronJob('cron_job_name_here');
            job.stop(); // pausing the cron job
    
            this.logger.debug(`start ${c}`);
            await timeout(3000)
            this.logger.debug(`end ${c}`);
            c += 1;
    
            job.start(); // restarting the cron job
       }
    

    Do not forget to inject the scheduler registry service into the constructor of your service.

    private schedulerRegistry: SchedulerRegistry,
    

    and also the imports:

    import { Cron, CronExpression, SchedulerRegistry } from '@nestjs/schedule'