Skip to content

Scheduler

Contract

@modularityjs/scheduler defines the ScheduledJob interface, SchedulerService abstract class, and pool-based job discovery.

typescript
interface ScheduledJob {
  readonly name: string;
  readonly schedule: string; // Cron expression
  readonly description?: string;
  readonly lockTtlMs?: number; // Override default lock TTL
  execute(): void | Promise<void>;
}
typescript
abstract class SchedulerService {
  abstract register(job: ScheduledJob): void;
  abstract start(): void;
  abstract stop(): Promise<void>;
  abstract getJobNames(): string[];
  abstract getJobs(): ReadonlyMap<string, JobInfo>;
  abstract getNextRun(name: string): Date | null;
  abstract runJob(name: string): Promise<void>;
}

Drivers

Croner (@modularityjs/scheduler-croner)

Cron-based scheduler using Croner with distributed locking via LockService. Prevents concurrent execution of the same job across instances.

typescript
import { SchedulerModule } from '@modularityjs/scheduler';
import { SchedulerCronerModule } from '@modularityjs/scheduler-croner';
import { LockModule } from '@modularityjs/lock';
import { LockRedisModule } from '@modularityjs/lock-redis';
import { RedisModule } from '@modularityjs/redis';

const modules = [
  RedisModule.forRoot({ host: 'localhost' }),
  LockModule,
  LockRedisModule,
  SchedulerModule,
  SchedulerCronerModule,
];

The Croner driver requires a LockService provider. In single-instance setups, LockMemoryModule works. For multi-instance deployments, use a distributed lock driver like LockRedisModule.

Defining Jobs

Implement the ScheduledJob interface and register via ScheduledJobsPool:

typescript
import { Injectable } from '@modularityjs/di';
import { ScheduledJobsPool, SchedulerModule } from '@modularityjs/scheduler';
import type { ScheduledJob } from '@modularityjs/scheduler';
import { Module } from '@modularityjs/modularity';

@Injectable()
class BackupJob implements ScheduledJob {
  readonly name = 'daily-backup';
  readonly schedule = '0 2 * * *'; // 2:00 AM daily
  readonly description = 'Daily database backup';
  readonly lockTtlMs = 120_000; // 2-minute lock (default: 60s)

  async execute() {
    await performBackup();
  }
}

@Module({
  name: 'backup',
  imports: [SchedulerModule],
  providers: [BackupJob],
  pools: [
    { pool: ScheduledJobsPool, key: 'daily-backup', useClass: BackupJob },
  ],
})
class BackupModule {}

Distributed Locking

The Croner driver acquires a distributed lock before executing each job:

  1. Lock acquired -> job executes, lock released in finally
  2. Lock not acquired -> job skips (another instance is running it)
  3. Lock TTL expires -> lock auto-releases (safety against crashed instances)

Default lock TTL is 60 seconds. Override per job with lockTtlMs.

Programmatic Access

typescript
import { Inject, Injectable } from '@modularityjs/di';
import { SchedulerService } from '@modularityjs/scheduler';

@Injectable()
class AdminService {
  constructor(
    @Inject(SchedulerService) private readonly scheduler: SchedulerService,
  ) {}

  listJobs() {
    return this.scheduler.getJobs();
    // Map<string, { name, schedule, description, running }>
  }

  nextRun(name: string) {
    return this.scheduler.getNextRun(name);
  }

  async triggerManually(name: string) {
    await this.scheduler.runJob(name);
  }
}

Graceful Shutdown

The driver waits up to 30 seconds for running jobs to complete before shutting down. Jobs that exceed this timeout are abandoned.