NestJSMiddleCoding

Что такое модуль @nestjs/schedule и как планировать задачи?

Модуль @nestjs/schedule оборачивает библиотеку node-cron и позволяет декларативно запускать задачи по расписанию через декораторы @Cron, @Interval и @Timeout прямо внутри провайдеров NestJS.

Модуль @nestjs/schedule: планирование задач

Пакет @nestjs/schedule интегрирует планировщик node-cron в экосистему NestJS. Он регистрирует задачи как провайдеры и управляет их жизненным циклом вместе с приложением.

Установка и подключение

npm install @nestjs/schedule

Подключаем ScheduleModule в корневой модуль:

import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { TasksService } from './tasks.service';

@Module({
  imports: [ScheduleModule.forRoot()],
  providers: [TasksService],
})
export class AppModule {}

Три вида декораторов

  • @Cron(expression) — запускает метод по cron-выражению.
  • @Interval(milliseconds) — запускает метод каждые N миллисекунд (аналог setInterval).
  • @Timeout(milliseconds) — запускает метод один раз через N миллисекунд после старта приложения.

Пример сервиса с задачами

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

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

  // Встроенная константа: каждый день в 00:00
  @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
  handleDailyCleanup(): void {
    this.logger.log('Запуск ежедневной очистки...');
  }

  // Произвольное cron-выражение: каждые 15 минут
  @Cron('0 */15 * * * *', { name: 'fetchRates', timeZone: 'Europe/Moscow' })
  async fetchExchangeRates(): Promise<void> {
    this.logger.log('Обновление курсов валют');
    // await this.ratesService.update();
  }

  // Каждые 5 секунд
  @Interval(5000)
  handleHeartbeat(): void {
    this.logger.debug('Heartbeat tick');
  }

  // Один раз через 10 секунд после старта
  @Timeout(10000)
  handleStartupWarmup(): void {
    this.logger.log('Прогрев кэша после старта');
  }
}

Динамическое управление задачами

Для программного управления задачами инжектируем SchedulerRegistry:

import { Injectable } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { CronJob } from 'cron';

@Injectable()
export class DynamicTasksService {
  constructor(private readonly schedulerRegistry: SchedulerRegistry) {}

  pauseJob(name: string): void {
    const job = this.schedulerRegistry.getCronJob(name);
    job.stop(); // ставим на паузу
  }

  resumeJob(name: string): void {
    const job = this.schedulerRegistry.getCronJob(name);
    job.start();
  }

  addDynamicJob(name: string, cronTime: string, callback: () => void): void {
    const job = new CronJob(cronTime, callback);
    this.schedulerRegistry.addCronJob(name, job);
    job.start();
  }

  deleteJob(name: string): void {
    this.schedulerRegistry.deleteCronJob(name);
  }
}

Опции декоратора @Cron

@Cron('0 0 * * * *', {
  name: 'hourlySyncJob',      // имя для SchedulerRegistry
  timeZone: 'UTC',           // часовой пояс (важно для серверов без TZ-настройки)
  disabled: false,           // можно отключить задачу при инициализации
})
async hourlySync(): Promise<void> { /* ... */ }

Подводные камни

  • Несколько реплик — при горизонтальном масштабировании задача запустится на каждом инстансе одновременно. Необходима распределённая блокировка (Redis-lock через redlock или аналог).
  • Время выполнения превышает интервал@Interval не ждёт завершения предыдущего вызова. Если задача занимает больше времени, чем интервал, вызовы начнут накапливаться.
  • Необработанные исключения — ошибка внутри задачи не проксируется в глобальный фильтр исключений NestJS, поэтому нужно явно оборачивать тело метода в try/catch.
  • Часовой пояс сервера — если timeZone не указан явно, задача использует часовой пояс операционной системы контейнера, что может приводить к неожиданному поведению в разных окружениях.
  • ScheduleModule.forRoot() вызывается один раз — повторный импорт в feature-модулях вызовет ошибку регистрации дублирующихся задач.
  • Метод должен быть публичным — декораторы @Cron, @Interval и @Timeout не работают на private и protected методах из-за ограничений рефлексии TypeScript.
  • Тестирование — в unit-тестах ScheduleModule нужно мокировать через { provide: SchedulerRegistry, useValue: mockRegistry }, иначе тесты запускают реальные таймеры.

Common mistakes

  • Дает общий ответ про Node.js и не называет конкретные API NestJS.
  • Не объясняет, где в lifecycle находится @nestjs/schedule.
  • Не разделяет validation, authorization, business logic и persistence.
  • Игнорирует ошибки, лимиты входных данных, observability и тестирование.

What the interviewer is testing

  • Может объяснить @nestjs/schedule на примере кода.
  • Называет ключевые API: @Cron(), @Interval().
  • Использует точные API NestJS, а не вымышленные hooks/decorators/methods.
  • Видит production-риски: безопасность, отказоустойчивость, логирование и тесты.

Sources

Related topics