Tạm biệt Cron Job: Xây dựng Durable Workflow với Temporal.io trong Node.js

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Cơn ác mộng mang tên Cron Job và những đêm trực chiến

Cơn ác mộng bắt đầu khi hệ thống xử lý hóa đơn cho 10.000 khách hàng của mình “đổ bệnh” ngay đêm 30. Ban đầu, mình dùng một Cron Job đơn giản chạy lúc 1 giờ sáng. Mọi chuyện êm đẹp cho đến khi API của Stripe gặp sự cố đúng lúc job đang chạy. Kết quả thật thảm khốc: 5.000 khách hàng chưa được thanh toán, số còn lại bị trừ tiền hai lần do mình restart job thủ công mà không nắm rõ trạng thái hiện tại.

Vấn đề cốt lõi của Cron Job hay các thư viện Queue như BullMQ là chúng rất khó quản lý trạng thái (state). Khi code bị crash giữa chừng, việc khôi phục đúng bước đang chạy dở là một cực hình. Bạn phải tự viết logic lưu state vào database, tự handle retry và tự lo đối soát dữ liệu.

Đó là lúc mình tìm thấy Temporal.io. Nó không chỉ là công cụ lập lịch. Đây là một nền tảng Durable Execution (thực thi bền vững). Hiểu đơn giản: Code của bạn sẽ chạy đến cùng. Bất kể server sập, network đứt hay database bảo trì, Temporal vẫn kiên trì hoàn thành nhiệm vụ.

Đặt lên bàn cân: Temporal vs Phần còn lại

Hãy nhìn vào thực tế để thấy tại sao các giải pháp truyền thống thường hụt hơi trong các dự án lớn:

  • Cron Job: Dễ dùng nhưng thuộc dạng “bắn xong rồi quên”. Nó thiếu cơ chế retry cho từng task nhỏ và không có dashboard để bạn biết chuyện gì đang xảy ra.
  • Message Queues (BullMQ, RabbitMQ): Khá hơn nhờ cơ chế Ack/Retry. Tuy nhiên, nếu quy trình có 5 bước lồng ghép logic phức tạp, bạn sẽ sớm rơi vào cái bẫy “Callback Hell” hoặc một state machine rối rắm.
  • Temporal.io: Bạn viết code như thể lỗi không bao giờ tồn tại. Temporal tự động ghi lại nhật ký thực thi của từng dòng code. Nếu server chết ở dòng số 10, khi sống lại, nó sẽ chạy tiếp ngay từ dòng số 11 thay vì quay lại vạch xuất phát.

Tại sao dự án Node.js của bạn cần Temporal?

Khi refactor hệ thống cũ, mình nhận ra Temporal giúp cắt giảm tới 80% code xử lý lỗi rườm rà nhờ 3 đặc điểm:

  1. Tin cậy tuyệt đối: Tự động retry với cơ chế exponential backoff. Bạn không cần viết một dòng logic catch-error nào cho các lỗi tạm thời.
  2. Quan sát trực quan: Dashboard cho phép soi rõ từng biến số tại thời điểm workflow tạm dừng. Việc debug trở nên nhẹ nhàng hơn bao giờ hết.
  3. Tác vụ dài hơi: Bạn có thể dùng hàm sleep để bắt Workflow đợi… vài tháng. Hệ thống không hề tốn RAM hay CPU trong thời gian chờ đợi này.

Triển khai Temporal với Node.js trong 5 bước

Bước 1: Dựng môi trường với Docker

Cách nhanh nhất để trải nghiệm là dùng Docker Compose để kéo toàn bộ stack Temporal Server và UI về máy:

curl -sL https://temporal.io/docker-compose.yml -o docker-compose.yml
docker-compose up

Sau vài phút, hãy truy cập http://localhost:8080. Chào mừng bạn đến với trung tâm điều khiển Workflow.

Bước 2: Cài đặt SDK

Mở terminal và thêm các package cần thiết vào dự án Node.js của bạn:

npm install @temporalio/workflow @temporalio/activity @temporalio/client @temporalio/worker

Bước 3: Định nghĩa Activity (Tác vụ thực thi)

Activity là nơi chứa các logic có rủi ro cao như gọi API bên thứ ba hoặc truy vấn Database.

// activities.ts
export async function sendWelcomeEmail(email: string): Promise<string> {
  // Giả lập gọi API SendGrid hoặc Mailchimp
  console.log(`Đang gửi mail tới ${email}...`);
  return `Success: ${email}`;
}

Bước 4: Xây dựng Workflow (Bộ não điều phối)

Workflow sẽ điều phối các Activity. Lưu ý: Code ở đây phải là deterministic (nhất quán).

// workflows.ts
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';

const { sendWelcomeEmail } = proxyActivities<typeof activities>({
  startToCloseTimeout: '1 minute',
  retry: { maximumAttempts: 10 },
});

export async function welcomeWorkflow(email: string): Promise<string> {
  return await sendWelcomeEmail(email);
}

Bước 5: Vận hành Worker

Worker là process thực thi code, còn Client là nơi bạn kích hoạt workflow.

// worker.ts
import { Worker } from '@temporalio/worker';
import * as activities from './activities';

async function run() {
  const worker = await Worker.create({
    workflowsPath: require.resolve('./workflows'),
    activities,
    taskQueue: 'billing-queue',
  });
  await worker.run();
}
run().catch(console.error);

Bài học xương máu: Đừng phá vỡ tính Determinism

Lỗi sơ đẳng nhất của người mới là dùng Math.random() hoặc new Date() trực tiếp trong Workflow. Temporal hoạt động bằng cách phát lại (replay) các sự kiện. Nếu mỗi lần chạy lại cho ra một kết quả khác nhau, hệ thống sẽ báo lỗi ngay lập tức.

Giải pháp: Hãy dùng workflow.now() thay cho Date.now(). Mọi tác vụ tương tác với thế giới bên ngoài (API, DB) buộc phải nằm trong Activity. Tuyệt đối không gọi trực tiếp trong Workflow.

Đánh giá công tâm sau 6 tháng sử dụng

Thành quả ngọt ngào: Mình không còn lo lắng mỗi khi deploy hay bảo trì server. Khả năng test workflow bằng cách giả lập thời gian trôi qua (time-skipping) giúp tiết kiệm hàng giờ chờ đợi kiểm thử.

Rào cản cần lưu ý: Bạn sẽ tốn thêm chi phí vận hành cụm Temporal Server. Ngoài ra, tư duy tách biệt Workflow và Activity ban đầu có thể khiến code trông rườm rà hơn mức cần thiết.

Lời kết: Khi nào bạn nên chuyển đổi?

Nếu ứng dụng chỉ có vài task chạy ngầm đơn giản, hãy cứ trung thành với Cron Job cho nhẹ máy. Nhưng nếu bạn đang xây dựng hệ thống thanh toán, đặt vé, hoặc các quy trình nghiệp vụ cần độ chính xác 100%, Temporal là khoản đầu tư hời nhất. Đừng tốn thời gian viết code xử lý lỗi. Hãy để Temporal lo hạ tầng, việc của bạn là tập trung vào logic nghiệp vụ.

Share: