☰ Mục lục series
Tài liệu NestJS tiếng Việt
  1. Nhập môn (Overview)
    1. Giới thiệu
    2. First steps
    3. Controllers
    4. Providers
    5. Modules
    6. Middleware
    7. Exception filters
    8. Pipes
    9. Guards
    10. Interceptors
    11. Custom decorators
  2. Nền tảng (Fundamentals)
    1. Custom providers
    2. Asynchronous providers
    3. Dynamic modules
    4. Injection scopes
    5. Circular dependency
    6. Module reference
    7. Lazy-loading modules
    8. Execution context
    9. Lifecycle events
    10. Discovery service
    11. Platform agnosticism
    12. Testing
  3. Kỹ thuật (Techniques)
    1. Configuration
    2. Database (SQL)
    3. MongoDB
    4. Validation
    5. Caching
    6. Serialization
    7. Versioning
    8. Task scheduling
    9. Queues
    10. Logging
    11. Cookies
    12. Events
    13. Compression
    14. File upload
    15. Streaming files
    16. HTTP module
    17. Session
    18. MVC
    19. Performance (Fastify)
    20. Server-Sent Events
  4. Bảo mật (Security)
    1. Authentication
    2. Authorization
    3. Encryption & Hashing
    4. Helmet
    5. CORS
    6. CSRF
    7. Rate limiting
    8. Vòng đời request
  5. OpenAPI (Swagger)
    1. Introduction
    2. Types and parameters
    3. Operations
    4. Security
    5. Mapped types
    6. Decorators
    7. CLI plugin
    8. Other features
  6. Recipes
    1. REPL
    2. CRUD generator
    3. SWC (fast compiler)
    4. Passport (auth)
    5. Hot reload
    6. MikroORM
    7. Router module
    8. Health checks (Terminus)
    9. CQRS
    10. Prisma
    11. Serve static
    12. Commander
    13. Async local storage
  7. WebSockets
    1. Gateways
    2. Exception filters
    3. Pipes
    4. Guards
    5. Interceptors
    6. Adapters
  8. Microservices
    1. Overview
    2. Redis
    3. MQTT
    4. NATS
    5. RabbitMQ
    6. Kafka
    7. gRPC
    8. Custom transporters
    9. Exception filters
    10. Pipes
    11. Pre-request hooks
    12. Guards
    13. Interceptors
  9. GraphQL
    1. Quick start
    2. Resolvers
    3. Mutations
    4. Subscriptions
    5. Scalars
    6. Directives
    7. Interfaces
    8. Unions and enums
    9. Field middleware
    10. Mapped types
    11. Plugins
    12. Complexity
    13. Extensions
    14. CLI plugin
    15. Generating SDL
    16. Sharing models
    17. Other features
    18. Federation
  10. CLI & triển khai
    1. CLI overview
    2. Workspaces
    3. Libraries
    4. Usage
    5. Scripts
    6. Deployment
    7. Standalone apps
  11. FAQ
    1. Serverless
    2. HTTP adapter
    3. Keep-Alive connections
    4. Global path prefix
    5. Raw body
    6. Hybrid application
    7. HTTPS & multiple servers
    8. Common errors
  12. Phụ lục
    1. Bảng thuật ngữ
Kỹ thuật (Techniques)

Lập lịch tác vụ (Task scheduling)

Bản dịch từ docs.nestjs.com/techniques/task-scheduling

Lập lịch tác vụ cho phép bạn lên lịch chạy code tùy ý (method/function) vào một ngày/giờ cố định, theo chu kỳ lặp lại, hoặc chỉ một lần sau một khoảng thời gian nhất định. Trong thế giới Linux, việc này thường được xử lý bởi các package như cron ở tầng hệ điều hành. Với ứng dụng Node.js, có nhiều package mô phỏng lại chức năng kiểu cron. Nest cung cấp package @nestjs/schedule, tích hợp với package cron phổ biến của Node.js. Chương này sẽ giới thiệu package đó.

Ghi chú người dịch Nếu bạn từng làm Laravel, đây tương đương với Task Scheduling (Schedule::command()... trong Kernel.php) — cùng ý tưởng khai báo lịch chạy trong code thay vì cấu hình crontab của OS trực tiếp. Điểm khác là Nest chạy job ngay trong tiến trình Node.js của ứng dụng (dùng setTimeout/setInterval bên dưới cho phần lớn trường hợp), nên nếu bạn scale ứng dụng ra nhiều instance mà không xử lý gì thêm, job sẽ chạy lặp lại trên từng instance — cần cơ chế lock/leader election riêng nếu muốn job chỉ chạy một lần.

Cài đặt

Để bắt đầu sử dụng, trước tiên ta cài các dependency cần thiết.

$ npm install --save @nestjs/schedule

Để kích hoạt việc lập lịch job, import ScheduleModule vào AppModule gốc và chạy static method forRoot() như dưới đây:

import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';

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

Lời gọi .forRoot() khởi tạo scheduler và đăng ký mọi cron job, timeout và interval khai báo (declarative) tồn tại trong app của bạn. Việc đăng ký diễn ra khi lifecycle hook onApplicationBootstrap được gọi, đảm bảo mọi module đã được load xong và các job đã khai báo đều đã sẵn sàng.

Cron job khai báo (declarative)

Một cron job lên lịch chạy tự động cho một function tùy ý (lời gọi method). Cron job có thể chạy:

  • Một lần, vào một ngày/giờ cụ thể.
  • Theo chu kỳ lặp lại; job lặp có thể chạy vào một thời điểm cụ thể trong một khoảng lặp (ví dụ: mỗi giờ một lần, mỗi tuần một lần, mỗi 5 phút một lần)

Khai báo một cron job bằng decorator @Cron() đặt trước định nghĩa method chứa code cần thực thi, như sau:

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

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

  @Cron('45 * * * * *')
  handleCron() {
    this.logger.debug('Called when the current second is 45');
  }
}

Trong ví dụ này, method handleCron() sẽ được gọi mỗi khi giây hiện tại là 45. Nói cách khác, method này chạy một lần mỗi phút, tại mốc giây thứ 45.

Decorator @Cron() hỗ trợ các cron pattern chuẩn sau:

  • Dấu sao (vd: *)
  • Khoảng giá trị (vd: 1-3,5)
  • Bước nhảy (vd: */2)

Trong ví dụ trên, ta truyền 45 * * * * * cho decorator. Bảng dưới đây cho biết cách diễn giải từng vị trí trong chuỗi cron pattern:


* * * * * *
| | | | | |
| | | | | day of week
| | | | months
| | | day of month
| | hours
| minutes
seconds (optional)

Một số mẫu cron pattern ví dụ:

* * * * * * mỗi giây
45 * * * * * mỗi phút, vào giây thứ 45
0 10 * * * * mỗi giờ, vào đầu phút thứ 10
0 */30 9-17 * * * mỗi 30 phút, trong khoảng 9h sáng đến 5h chiều
0 30 11 * * 1-5 Thứ Hai đến thứ Sáu, lúc 11:30 sáng

Package @nestjs/schedule cung cấp một enum tiện lợi chứa các cron pattern thường dùng. Bạn có thể dùng enum này như sau:

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

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

  @Cron(CronExpression.EVERY_30_SECONDS)
  handleCron() {
    this.logger.debug('Called every 30 seconds');
  }
}

Trong ví dụ này, method handleCron() sẽ được gọi mỗi 30 giây. Nếu có exception xảy ra, nó sẽ được log ra console, vì mọi method được đánh dấu @Cron() đều tự động được bọc trong một khối try-catch.

Ngoài ra, bạn có thể truyền một object Date của JavaScript cho decorator @Cron(). Khi đó job sẽ chỉ chạy đúng một lần, vào thời điểm được chỉ định.

Gợi ý Dùng phép toán ngày tháng của JavaScript để lên lịch job tương đối so với thời điểm hiện tại. Ví dụ: @Cron(new Date(Date.now() + 10 * 1000)) để lên lịch job chạy 10 giây sau khi app khởi động.

Bên cạnh đó, bạn có thể truyền thêm các tùy chọn khác làm tham số thứ hai cho decorator @Cron().

name Hữu ích để truy cập và điều khiển một cron job sau khi nó đã được khai báo.
timeZone Chỉ định múi giờ cho việc thực thi. Việc này sẽ thay đổi thời gian thực tế theo múi giờ của bạn. Nếu múi giờ không hợp lệ, một lỗi sẽ được ném ra. Bạn có thể xem toàn bộ danh sách múi giờ khả dụng tại trang Moment Timezone.
utcOffset Cho phép bạn chỉ định độ lệch (offset) của múi giờ thay vì dùng tham số timeZone.
waitForCompletion Nếu là true, sẽ không có instance nào khác của cron job được chạy cho đến khi callback onTick hiện tại hoàn thành. Mọi lần chạy đã lên lịch xảy ra trong lúc cron job hiện tại đang chạy sẽ bị bỏ qua hoàn toàn.
disabled Cho biết job này có được thực thi hay không.
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

@Injectable()
export class NotificationService {
  @Cron('* * 0 * * *', {
    name: 'notifications',
    timeZone: 'Europe/Paris',
  })
  triggerNotifications() {}
}

Bạn có thể truy cập và điều khiển một cron job sau khi nó đã được khai báo, hoặc tạo cron job động (mà cron pattern của nó được định nghĩa lúc runtime) bằng Dynamic API. Để truy cập một cron job khai báo (declarative) thông qua API, bạn phải gắn job đó với một tên bằng cách truyền thuộc tính name trong object options (tùy chọn) làm tham số thứ hai của decorator.

Interval khai báo (declarative)

Để khai báo rằng một method sẽ chạy theo một khoảng lặp (interval) xác định, đặt decorator @Interval() trước định nghĩa method đó. Truyền giá trị interval, dưới dạng số mili-giây, cho decorator như dưới đây:

@Interval(10000)
handleInterval() {
  this.logger.debug('Called every 10 seconds');
}

Gợi ý Cơ chế này dùng hàm setInterval() của JavaScript bên dưới. Bạn cũng có thể dùng cron job để lên lịch các job lặp.

Nếu muốn điều khiển interval khai báo của bạn từ bên ngoài class khai báo nó thông qua Dynamic API, hãy gắn interval đó với một tên bằng cú pháp sau:

@Interval('notifications', 2500)
handleInterval() {}

Nếu có exception xảy ra, nó sẽ được log ra console, vì mọi method được đánh dấu @Interval() đều tự động được bọc trong một khối try-catch.

Dynamic API cũng cho phép tạo interval động, với các thuộc tính được định nghĩa lúc runtime, và liệt kê, xóa chúng.

Timeout khai báo (declarative)

Để khai báo rằng một method sẽ chạy (một lần) sau một khoảng thời gian xác định, đặt decorator @Timeout() trước định nghĩa method đó. Truyền độ trễ tương đối (tính bằng mili-giây), tính từ lúc ứng dụng khởi động, cho decorator như dưới đây:

@Timeout(5000)
handleTimeout() {
  this.logger.debug('Called once after 5 seconds');
}

Gợi ý Cơ chế này dùng hàm setTimeout() của JavaScript bên dưới.

Nếu có exception xảy ra, nó sẽ được log ra console, vì mọi method được đánh dấu @Timeout() đều tự động được bọc trong một khối try-catch.

Nếu muốn điều khiển timeout khai báo của bạn từ bên ngoài class khai báo nó thông qua Dynamic API, hãy gắn timeout đó với một tên bằng cú pháp sau:

@Timeout('notifications', 2500)
handleTimeout() {}

Dynamic API cũng cho phép tạo timeout động, với các thuộc tính được định nghĩa lúc runtime, và liệt kê, xóa chúng.

Dynamic schedule module API

Module @nestjs/schedule cung cấp một dynamic API cho phép bạn quản lý các cron job, timeout và interval khai báo. API này cũng cho phép bạn tạo và quản lý cron job, timeout và interval động (dynamic), với các thuộc tính được định nghĩa lúc runtime.

Cron job động (dynamic)

Lấy tham chiếu đến một instance CronJob theo tên từ bất kỳ đâu trong code bằng SchedulerRegistry API. Trước tiên, inject SchedulerRegistry bằng constructor injection tiêu chuẩn:

constructor(private schedulerRegistry: SchedulerRegistry) {}

Gợi ý Import SchedulerRegistry từ package @nestjs/schedule.

Sau đó dùng nó trong một class như sau. Giả sử một cron job đã được tạo bằng khai báo sau:

@Cron('* * 8 * * *', {
  name: 'notifications',
})
triggerNotifications() {}

Truy cập job này bằng:

const job = this.schedulerRegistry.getCronJob('notifications');

job.stop();
console.log(job.lastDate());

Method getCronJob() trả về cron job theo tên. Object CronJob trả về có các method sau:

  • stop() - dừng một job đang được lên lịch chạy.
  • start() - khởi động lại một job đã bị dừng.
  • setTime(time: CronTime) - dừng job, đặt thời gian mới cho nó, rồi khởi động lại
  • lastDate() - trả về biểu diễn DateTime của ngày mà job được thực thi lần cuối.
  • nextDate() - trả về biểu diễn DateTime của ngày mà job sẽ được thực thi tiếp theo.
  • nextDates(count: number) - Cung cấp một mảng (kích thước count) các biểu diễn DateTime cho tập ngày tiếp theo sẽ kích hoạt job chạy. count mặc định là 0, trả về mảng rỗng.

Gợi ý Dùng toJSDate() trên object DateTime để chuyển nó thành một Date tương đương của JavaScript.

Tạo một cron job mới lúc runtime bằng method SchedulerRegistry#addCronJob, như sau:

addCronJob(name: string, seconds: string) {
  const job = new CronJob(`${seconds} * * * * *`, () => {
    this.logger.warn(`time (${seconds}) for job ${name} to run!`);
  });

  this.schedulerRegistry.addCronJob(name, job);
  job.start();

  this.logger.warn(
    `job ${name} added for each minute at ${seconds} seconds!`,
  );
}

Trong đoạn code này, ta dùng object CronJob từ package cron để tạo cron job. Constructor của CronJob nhận một cron pattern (giống như decorator @Cron()) làm tham số đầu tiên, và một callback được thực thi khi timer của cron kích hoạt làm tham số thứ hai. Method SchedulerRegistry#addCronJob nhận hai tham số: tên cho CronJob, và chính object CronJob đó.

Cảnh báo Nhớ inject SchedulerRegistry trước khi truy cập nó. Import CronJob từ package cron.

Xóa một cron job theo tên bằng method SchedulerRegistry#deleteCronJob, như sau:

deleteCron(name: string) {
  this.schedulerRegistry.deleteCronJob(name);
  this.logger.warn(`job ${name} deleted!`);
}

Liệt kê tất cả cron job bằng method SchedulerRegistry#getCronJobs như sau:

getCrons() {
  const jobs = this.schedulerRegistry.getCronJobs();
  jobs.forEach((value, key, map) => {
    let next;
    try {
      next = value.nextDate().toJSDate();
    } catch (e) {
      next = 'error: next fire date is in the past!';
    }
    this.logger.log(`job: ${key} -> next: ${next}`);
  });
}

Method getCronJobs() trả về một map. Trong đoạn code này, ta duyệt qua map và thử truy cập method nextDate() của từng CronJob. Trong API của CronJob, nếu một job đã chạy xong và không còn ngày chạy nào trong tương lai, nó sẽ ném ra một exception.

Interval động (dynamic)

Lấy tham chiếu đến một interval bằng method SchedulerRegistry#getInterval. Tương tự như trên, inject SchedulerRegistry bằng constructor injection tiêu chuẩn:

constructor(private schedulerRegistry: SchedulerRegistry) {}

Và dùng nó như sau:

const interval = this.schedulerRegistry.getInterval('notifications');
clearInterval(interval);

Tạo một interval mới lúc runtime bằng method SchedulerRegistry#addInterval, như sau:

addInterval(name: string, milliseconds: number) {
  const callback = () => {
    this.logger.warn(`Interval ${name} executing at time (${milliseconds})!`);
  };

  const interval = setInterval(callback, milliseconds);
  this.schedulerRegistry.addInterval(name, interval);
}

Trong đoạn code này, ta tạo một interval JavaScript tiêu chuẩn, rồi truyền nó vào method SchedulerRegistry#addInterval. Method đó nhận hai tham số: tên cho interval, và chính interval đó.

Xóa một interval theo tên bằng method SchedulerRegistry#deleteInterval, như sau:

deleteInterval(name: string) {
  this.schedulerRegistry.deleteInterval(name);
  this.logger.warn(`Interval ${name} deleted!`);
}

Liệt kê tất cả interval bằng method SchedulerRegistry#getIntervals như sau:

getIntervals() {
  const intervals = this.schedulerRegistry.getIntervals();
  intervals.forEach(key => this.logger.log(`Interval: ${key}`));
}

Timeout động (dynamic)

Lấy tham chiếu đến một timeout bằng method SchedulerRegistry#getTimeout. Tương tự như trên, inject SchedulerRegistry bằng constructor injection tiêu chuẩn:

constructor(private readonly schedulerRegistry: SchedulerRegistry) {}

Và dùng nó như sau:

const timeout = this.schedulerRegistry.getTimeout('notifications');
clearTimeout(timeout);

Tạo một timeout mới lúc runtime bằng method SchedulerRegistry#addTimeout, như sau:

addTimeout(name: string, milliseconds: number) {
  const callback = () => {
    this.logger.warn(`Timeout ${name} executing after (${milliseconds})!`);
  };

  const timeout = setTimeout(callback, milliseconds);
  this.schedulerRegistry.addTimeout(name, timeout);
}

Trong đoạn code này, ta tạo một timeout JavaScript tiêu chuẩn, rồi truyền nó vào method SchedulerRegistry#addTimeout. Method đó nhận hai tham số: tên cho timeout, và chính timeout đó.

Xóa một timeout theo tên bằng method SchedulerRegistry#deleteTimeout, như sau:

deleteTimeout(name: string) {
  this.schedulerRegistry.deleteTimeout(name);
  this.logger.warn(`Timeout ${name} deleted!`);
}

Liệt kê tất cả timeout bằng method SchedulerRegistry#getTimeouts như sau:

getTimeouts() {
  const timeouts = this.schedulerRegistry.getTimeouts();
  timeouts.forEach(key => this.logger.log(`Timeout: ${key}`));
}

Biết được một cron job thực sự đã chạy

Một job đã lên lịch mà ném lỗi là vấn đề bạn sẽ được nghe báo lại. Nhưng một job đã lên lịch mà âm thầm ngừng được lên lịch — tiến trình bị crash, container bị dừng, một lần deploy đưa lên một biểu thức @Cron() bị gõ sai — lại là vấn đề không ai biết đến cho tới khi báo cáo mà nó lẽ ra phải sinh ra bị thiếu.

Đây chính là kiểu lỗi mà cron monitoring sinh ra để giải quyết, và NestJS Observe xử lý việc này mà không cần một dịch vụ ping bên thứ ba hay một heartbeat URL để job gọi tới trước khi kết thúc. Các lần chạy đã lên lịch được báo cáo dưới dạng job, cùng với các queue consumer — mỗi cái kèm thời lượng chạy, kết quả, và lý do thất bại (nếu có) — và một quy tắc cảnh báo job im lặng (job silence) sẽ kích hoạt khi một job có tên cụ thể không báo cáo trong khoảng dung sai bạn đặt, từ 2 phút đến 7 ngày — “báo cho tôi nếu daily-invoices không báo cáo trong 26 giờ qua”.

Có hai chi tiết khiến cơ chế này thực dụng thay vì gây nhiễu. Một scope chưa từng báo cáo gì thì sẽ không kích hoạt, nên bạn có thể thêm quy tắc trước khi job được deploy mà không lo bị báo động vì thứ chưa được tích hợp. Và các quy tắc có lịch tắt tiếng (mute) định kỳ, nên một job hợp lệ không chạy vào cuối tuần sẽ không làm ai bị đánh thức vào thứ Bảy.

Một handler cần tự báo cáo tiến trình của nó có thể inject TracerService và ghi lại span hoặc custom metric ngay trong lúc chạy — metric nói riêng thì không gắn với trace, nên chúng có thể được báo cáo từ cả cron job lẫn lifecycle hook. Xem Manual instrumentation để biết thêm, và chương Observability để biết cách thiết lập.

Ghi chú người dịch Nếu bạn quen PHP, mô hình này gần giống health-check kiểu “dead man’s switch” của các dịch vụ như Healthchecks.io hay Laravel Scheduler’s ->pingOnSuccess(), chỉ khác là ở đây nó được tích hợp sẵn thành một sản phẩm quan sát (observability) của hệ sinh thái Nest thay vì bạn tự gọi HTTP request cuối mỗi job.

Ví dụ

Một ví dụ hoạt động đầy đủ có sẵn tại đây.

Nội dung gốc: NestJS Documentation © Kamil Myśliwiec, giấy phép MIT. Bản dịch và ghi chú: Lê Văn Dũng.