0

[Series Thực Chiến] Chinh phục Queue - Phần cuối: Xử lý Failed Jobs và nghệ thuật "Hồi sinh" công việc

Chào anh em, vậy là chúng ta đã đi đến chặng cuối của hành trình "Chinh phục Queue".

Nếu ở bài trước, chúng ta đã xây dựng được một đội ngũ "Worker" hùng hậu, chạy ngầm 24/7 dưới sự giám sát của Supervisor, thì ở bài này, chúng ta sẽ học cách đối mặt với mặt tối của hệ thống: Thất bại.

Trong thực tế, không có hệ thống nào là hoàn hảo 100%. API bên thứ ba có thể sập, database có thể bị lock, hoặc đơn giản là code của chúng ta có bug "lạ". Khi một Job thất bại sau tất cả các nỗ lực thử lại (retry), nó sẽ đi đâu? Làm sao để chúng ta cứu vãn tình hình? Chào mừng anh em đến với Phần cuối: Xử lý Failed Jobs - "Bệnh viện" cho những công việc lỗi.

1. Khi nào một Job chính thức được coi là "Thất bại"?

Như anh em đã biết, chúng ta thường cấu hình tries = 3 cho một Job.

  • Lần 1 lỗi -> Chờ (backoff) -> Thử lại lần 2.
  • Lần 2 lỗi -> Chờ -> Thử lại lần 3.
  • Lần 3 vẫn lỗi -> FAILED.

Lúc này, Worker sẽ không thử lại nữa vì nó không muốn tốn tài nguyên vô ích. Nó sẽ bắn ra một Failed Job Event và đưa Job này vào một khu vực lưu trữ riêng (thường là bảng failed_jobs trong database).

2. Failed Job Events: Tiếng chuông cảnh báo

Đừng để Job chết trong im lặng. Bạn cần biết ngay lập tức khi có sự cố để còn kịp thời can thiệp.

Có hai cách để "bắt" sự kiện này:

Cách A: Viết hàm failed() ngay trong Job Class

Đây là cách "local", giúp bạn xử lý logic riêng biệt cho từng loại Job.

export class SendWelcomeEmailJob implements JobInterface {
    // ... logic handle ...

    // Hàm này sẽ tự động được gọi khi Job thất bại hoàn toàn sau n lần thử lại
    public async failed(error: Error): Promise<void> {
        console.log(`[FAILED] Gửi mail cho ${this.userEmail} thất bại hoàn toàn!`);
        
        // Cập nhật trạng thái vào DB để Admin biết
        await User.update({ email_status: 'failed' }, { where: { email: this.userEmail } });
        
        // Bắn alert về Telegram
        await AlertService.notify(`User ${this.userEmail} không nhận được mail chào mừng. Check ngay!`);
    }
}

Cách B: Lắng nghe Global Event Dùng cho mục đích log tập trung hoặc theo dõi sức khỏe hệ thống.

Queue.on('failed', (job, error) => {
    // Lưu vào hệ thống Log tập trung (như ELK hoặc Sentry)
    Sentry.captureException(error, { extra: { jobData: job.data } });
});

3. Quản lý và Thử lại (Retry) Failed Jobs

Đừng vội xóa những Job lỗi này. Đa phần các lỗi đến từ việc Network chập chờn hoặc API bên thứ ba bảo trì. Khi mọi thứ ổn định trở lại, bạn chỉ cần một nút bấm để "hồi sinh" hàng ngàn Job lỗi.

Lưu trữ trong Database Thông thường, một bản ghi trong bảng failed_jobs sẽ chứa:

  • Connection/Queue: Job đó thuộc hàng đợi nào.
  • Payload: Toàn bộ data JSON của Job đó (Để bạn có thể biết nó định làm gì).
  • Exception: Nội dung lỗi (Stack trace) để bạn biết tại sao nó chết.
  • Failed At: Thời điểm tử vong.

Lệnh hồi sinh (The Resurrection) Thay vì phải code tay, các Framework thường cung cấp sẵn công cụ CLI:

# Xem danh sách các Job đang nằm trong "nhà xác"
php artisan queue:failed

# Thử lại một Job cụ thể theo ID
php artisan queue:retry 105

# Thử lại TẤT CẢ các Job lỗi (Khi server mail đã sống lại)
php artisan queue:retry all

# Xóa các Job lỗi quá cũ
php artisan queue:flush

4. Bí kíp Senior: Tính Idempotency (Tính bất biến)

Đây là lưu ý quan trọng nhất khi xử lý Retry. Idempotency nghĩa là: Dù bạn chạy một Job 1 lần hay 10 lần, kết quả cuối cùng vẫn không thay đổi và không gây ra tác dụng phụ ngoài ý muốn.

Ví dụ kinh điển: Job "Thanh toán đơn hàng".

  • Worker đang trừ tiền user xong thì... mất mạng.
  • Job chưa kịp báo thành công nên bị đưa vào failed_jobs.
  • Bạn ấn retry. Nếu code không kiểm tra, user sẽ bị trừ tiền 2 lần. 😱

Giải pháp: Luôn kiểm tra trạng thái trước khi làm.

public async handle() {
    const order = await Order.find(this.orderId);
    if (order.is_paid) return; // Nếu đã thanh toán rồi thì thoát luôn, coi như xong.
    
    await PaymentGateway.charge(order.amount);
    await order.update({ is_paid: true });
}

Để giúp anh em hình dung rõ hơn về quy trình "Cấp cứu" Job, mình đã chuẩn bị một Dashboard mô phỏng dưới đây. Anh em có thể thử tạo lỗi, xem Log và thực hiện Retry để thấy hệ thống vận hành ra sao nhé!

{
    "component": "LlmGeneratedComponent",
        "props": {
        "height": "700px",
        "prompt": "Tạo một dashboard quản lý 'Bệnh viện Job' (Failed Job Management). \nGiao diện gồm 2 phần:\n1. Phía trên: Danh sách các Job đang chạy. Có nút 'Gây lỗi ngẫu nhiên' để giả lập Job thất bại.\n2. Phía dưới: Bảng 'Failed Jobs' chứa các Job đã chết. \n- Mỗi dòng trong bảng Failed Job có cột: ID, Tên Job, Lỗi (ví dụ: 500 Internal Server Error), Thời gian.\n- Có nút 'Retry' bên cạnh mỗi Job lỗi. Khi nhấn Retry, Job đó biến mất khỏi bảng Failed và xuất hiện lại ở danh sách Đang chạy phía trên.\n- Có nút 'Retry All' để hồi sinh tất cả.\n- Có bảng Log bên cạnh hiển thị tiến trình: 'Job #101 đang thử lại lần 2...', 'Job #101 đã hy sinh, chuyển vào nhà xác'.\nThiết kế hiện đại, bảng biểu rõ ràng, sử dụng các icon để phân biệt trạng thái."
    }
}

Tổng kết Series

Vậy là chúng ta đã đi qua trọn vẹn 5 phần của series "Chinh phục Queue":

  1. Giới thiệu: Hiểu về xử lý bất đồng bộ.
  2. Job Classes: Đóng gói công việc chuyên nghiệp.
  3. Dispatching: Đưa Job vào hàng đợi và nghệ thuật Delay.
  4. Worker & Supervisor: Vận hành hệ thống chạy ngầm "bất tử".
  5. Failed Jobs: Quản lý rủi ro và hồi sinh dữ liệu.

Hy vọng series này giúp anh em tự tin hơn khi thiết kế các hệ thống chịu tải lớn và xử lý các tác vụ nặng. Queue không khó, cái khó là làm sao để kiểm soát được nó khi có sự cố.

Học nữa, học mãi: Queue chỉ là bước đầu của kiến trúc Event-Driven Microservices. Nếu anh em muốn tiến xa hơn, hãy tìm hiểu về Message Brokers hạng nặng như RabbitMQ hoặc Apache Kafka để xử lý hàng triệu message mỗi giây.

Cảm ơn anh em đã theo dõi đến tận cuối series. Đừng quên Upvote và Follow mình trên Viblo để không bỏ lỡ những bài viết thực chiến tiếp theo về System Design nhé!

Chúc anh em code mượt, Job luôn "Completed" và ít khi phải vào "nhà xác"! 🚀


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí