Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 34

Xử lý Lỗi Kỹ Thuật trong External Worker: Retry và Incident

Recommended resources

Chào bạn,

Ở bài học trước, chúng ta đã tìm hiểu cách quản lý các tác vụ chạy dài bằng lockDuration và cơ chế "heartbeat" với extendLock. Điều này đảm bảo rằng nếu worker của bạn bị "chết" đột ngột, công việc sẽ không bị treo vô thời hạn. Tuy nhiên, đó là một cách xử lý lỗi thụ động.

Trong thực tế, worker của chúng ta có thể gặp phải các lỗi có thể lường trước, ví dụ như một dịch vụ bên ngoài tạm thời không khả dụng, mất kết nối cơ sở dữ liệu, hoặc một lỗi logic tạm thời. Trong những trường hợp này, việc chỉ chờ lock hết hạn và thử lại một cách "mù quáng" là không hiệu quả. Chúng ta cần một cách chủ động hơn để báo cáo lỗi và kiểm soát quá trình thử lại.

Bài học hôm nay sẽ tập trung vào mục tiêu đó: Xử lý lỗi kỹ thuật trong External Worker để kích hoạt retry và tạo incident. Bạn sẽ học cách làm cho worker của mình "thông minh" hơn bằng cách báo cáo lỗi một cách tường minh cho Camunda Engine, định nghĩa một chiến lược thử lại, và khi mọi nỗ lực đều thất bại, tạo ra một "sự cố" (incident) để con người có thể can thiệp.


1. Lỗi Kỹ thuật và Lỗi Nghiệp vụ: Sự khác biệt quan trọng

Trước khi đi vào chi tiết, chúng ta cần phân biệt rõ hai loại lỗi chính trong một quy trình:

  • Lỗi Kỹ thuật (Technical Error): Đây là những sự cố không mong muốn trong quá trình thực thi, thường do môi trường hoặc code. Ví dụ: NullPointerException, mất kết nối mạng, dịch vụ bên ngoài trả về lỗi 503 Service Unavailable. Đặc điểm của lỗi kỹ thuật là việc thử lại (retry) có thể sẽ giải quyết được vấn đề.
  • Lỗi Nghiệp vụ (Business Error): Đây là những tình huống được dự đoán trước trong logic nghiệp vụ. Ví dụ: khách hàng không đủ tín dụng, sản phẩm hết hàng. Việc thử lại sẽ không thay đổi kết quả. Thay vào đó, quy trình cần được thiết kế để rẽ sang một nhánh xử lý khác (ví dụ: thông báo cho khách hàng, hủy đơn hàng).

8 Tutorial Error Handling

Video sau đây từ Camunda giải thích ngắn gọn sự khác biệt này. Hãy xem để có cái nhìn trực quan.

Xem đoạn từ 01:28 đến 02:11. Chú ý cách video phân biệt giữa "business error" (được xử lý bằng các ký hiệu BPMN như Error Boundary Event) và lỗi do "technical reasons" (được xử lý bằng retry cycle).

Trong bài học này, chúng ta sẽ tập trung hoàn toàn vào lỗi kỹ thuật. Việc xử lý lỗi nghiệp vụ sẽ được đề cập trong một bài học sau.



2. Báo cáo Lỗi Kỹ thuật với handleFailure

Khi External Worker của bạn gặp lỗi kỹ thuật, thay vì để nó crash và chờ lock hết hạn, bạn nên bắt (catch) exception đó và chủ động báo cáo lỗi về cho Camunda Engine bằng phương thức ExternalTaskService.handleFailure().

External Tasks - Reporting Task Failure

Tài liệu chính thức của Camunda giải thích rất chi tiết về phương thức này. Hãy đọc để hiểu các tham số của nó.

Tìm đến mục "Reporting Task Failure". Đọc kỹ phần mô tả về phương thức handleFailure và ý nghĩa của các tham số: errorMessage, errorDetails, retries, và retryTimeout. Đặc biệt chú ý đến câu: "When setting retries to 0, a task can no longer be fetched and an incident is created for this task."

Như bạn đã đọc, handleFailure cho phép bạn cung cấp thông tin chi tiết về lỗi và định nghĩa chiến lược thử lại:

  • handleFailure(String taskId, String workerId, String errorMessage, String errorDetails, int retries, long retryTimeout)
    • errorMessage: Một thông báo lỗi ngắn gọn, sẽ hiển thị trực tiếp trong danh sách incident trên Cockpit. (Tối đa 666 ký tự).
    • errorDetails: Thông tin lỗi chi tiết, thường là toàn bộ stack trace của exception. Giúp developer debug dễ dàng hơn.
    • retries: Số lần thử lại còn lại. Đây là tham số quan trọng nhất. Nếu retries > 0, tác vụ sẽ được mở khóa và có thể được fetch lại sau retryTimeout. Nếu retries = 0, Engine sẽ tạo một incident.
    • retryTimeout: Khoảng thời gian (tính bằng mili giây) mà Engine phải chờ trước khi cho phép worker fetch lại tác vụ này.


3. Implement Cơ chế Thử lại trong Worker

Một điểm cực kỳ quan trọng mà tài liệu đã đề cập là: Camunda Engine không tự động giảm số lần retries. Chính worker của bạn phải chịu trách nhiệm cho logic này.

Điều này nghe có vẻ phức tạp, nhưng lại rất linh hoạt. Hãy xem cách triển khai nó trong thực tế.

External Task Retry Java - Discussion & Questions

Một lập trình viên trên diễn đàn Camunda đã đặt một câu hỏi rất hay: làm thế nào để khởi tạo và giảm số lần retries bằng Java? Câu trả lời từ chuyên gia của Camunda cung cấp một mẫu code rất rõ ràng và thực tế.

Đọc câu hỏi ban đầu của người dùng và câu trả lời của Ingo Richtsmeier. Tập trung vào đoạn code mẫu trong câu trả lời. Hãy để ý cách anh ấy kiểm tra externalTask.getRetries() có null hay không để khởi tạo số lần thử lại ban đầu.

Dựa trên mẫu code đó, chúng ta có thể xây dựng một cấu trúc try-catch mạnh mẽ cho handler của mình.

Ví dụ thực hành

Hãy sửa đổi VideoTranscodingHandler của chúng ta. Lần này, chúng ta sẽ giả lập một lỗi kỹ thuật xảy ra ngẫu nhiên và implement cơ chế retry.

import org.camunda.bpm.client.task.ExternalTask;
import org.camunda.bpm.client.task.ExternalTaskHandler;
import org.camunda.bpm.client.task.ExternalTaskService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.camunda.bpm.client.spring.annotation.ExternalTaskSubscription;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Random;

@Component
@ExternalTaskSubscription(topicName = "video-transcoding", lockDuration = 30000) // Lock 30 giây
public class VideoTranscodingHandler implements ExternalTaskHandler {

    private final static Logger LOGGER = LoggerFactory.getLogger(VideoTranscodingHandler.class);
    private final Random random = new Random();

    @Override
    public void execute(ExternalTask externalTask, ExternalTaskService externalTaskService) {
        String videoFile = externalTask.getVariable("videoFile");
        LOGGER.info("Bắt đầu xử lý video '{}'...", videoFile);

        try {
            // Giả lập lỗi: 50% khả năng dịch vụ chuyển mã bị lỗi
            if (random.nextBoolean()) {
                LOGGER.error("Dịch vụ chuyển mã không khả dụng!");
                throw new IllegalStateException("Transcoding service is currently unavailable");
            }

            // Giả lập công việc thành công
            LOGGER.info("Chuyển mã video '{}' thành công!", videoFile);
            externalTaskService.complete(externalTask);

        } catch (Exception e) {
            LOGGER.warn("Xảy ra lỗi kỹ thuật. Báo cáo failure cho Camunda...");

            // 1. Lấy số lần retry còn lại. Lần đầu tiên chạy, nó sẽ là null.
            Integer retries = externalTask.getRetries();
            if (retries == null) {
                retries = 3; // 2. Khởi tạo với 3 lần thử lại
            }

            // 3. Chuyển stack trace thành String để gửi lên errorDetails
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            String stackTrace = sw.toString();

            // 4. Báo cáo lỗi, giảm số lần retry đi 1
            externalTaskService.handleFailure(
                    externalTask,
                    e.getMessage(), // errorMessage
                    stackTrace,     // errorDetails
                    retries - 1,    // giảm số lần retry
                    10000L          // chờ 10 giây trước khi thử lại
            );
        }
    }
}

Hãy chạy thử quy trình với worker này. Bạn sẽ thấy trong log:

  • Worker sẽ báo cáo lỗi.
  • Sau 10 giây, nó sẽ nhận lại tác vụ và thử lại. Số lần retry còn lại sẽ là 2.
  • Quá trình này lặp lại cho đến khi:
    • Việc xử lý thành công (xác suất 50%).
    • Số lần retry về 0.


4. Incident là gì?

Khi bạn gọi handleFailure với retries = 0, worker đã nói với Engine rằng: "Tôi đã thử hết sức nhưng không thể hoàn thành. Cần có sự can thiệp của con người."

Lúc này, Camunda Engine sẽ:

  1. Ngừng việc cho phép worker fetch tác vụ này.
  2. Tạo ra một Incident (sự cố).
  3. Gắn cờ đỏ cho thực thể quy trình (process instance) đó trong Cockpit.

Một incident là một tín hiệu báo động cho người vận hành hệ thống.

8 Tutorial Error Handling

Hãy xem incident trông như thế nào trong Cockpit và cách truy cập thông tin lỗi mà worker đã gửi lên.

Hãy xem video từ 13:20 đến 15:00. Chú ý các điểm sau: Process instance có một huy hiệu màu đỏ với số "1", báo hiệu có một incident. Khi vào trong, bạn sẽ thấy activity đang bị lỗi cũng có viền đỏ. Có một tab "Incidents" ở phía dưới. Nhấp vào đó sẽ thấy errorMessage mà chúng ta đã cung cấp. Bạn có thể xem chi tiết stack trace (errorDetails) để chẩn đoán vấn đề.

Việc cung cấp errorMessage và errorDetails rõ ràng trong handleFailure là cực kỳ quan trọng, vì nó giúp người vận hành và developer nhanh chóng xác định nguyên nhân gốc rễ của vấn đề.



5. Xử lý và Giải quyết Incident

Khi một incident đã xảy ra, quy trình sẽ bị dừng lại ở tác vụ đó. Để tiếp tục, bạn cần phải giải quyết nó. Quá trình này thường bao gồm hai bước:

  1. Sửa lỗi gốc:
    • Nếu là lỗi code trong worker, bạn cần sửa code và deploy phiên bản mới.
    • Nếu là do một hệ thống bên ngoài bị sập, bạn cần khởi động lại hệ thống đó.
  2. Thử lại tác vụ:
    • Sau khi lỗi gốc đã được khắc phục, bạn cần yêu cầu Camunda thử lại tác vụ.

Cách đơn giản nhất để thử lại một tác vụ External Task có incident là thông qua Cockpit:

  1. Truy cập vào process instance đang có incident.
  2. Đi đến tab "Incidents".
  3. Bạn sẽ thấy một nút Retry (hoặc một biểu tượng hình mũi tên quay vòng) bên cạnh thông báo lỗi.
  4. Nhấp vào nút đó. Camunda sẽ tăng số lần retries của tác vụ lên 1 (từ 0 lên 1).
  5. Ngay lập tức, tác vụ sẽ trở nên khả dụng để worker fetch và xử lý lại.

Hành động này tương đương với việc gọi API ExternalTaskService#setRetries, như được đề cập trong tài liệu (LINK, section 3).



Tổng kết

Qua bài học này, bạn đã học được một trong những kỹ thuật quan trọng nhất để xây dựng các hệ thống BPM tự động có khả năng chịu lỗi.

Những điểm chính cần ghi nhớ:

  • Phân biệt rõ Lỗi Kỹ thuật (cần retry) và Lỗi Nghiệp vụ (cần rẽ nhánh).
  • Sử dụng try-catch trong worker để bắt các exception kỹ thuật.
  • Gọi ExternalTaskService.handleFailure() để báo cáo lỗi một cách tường minh cho Engine.
  • Worker phải tự quản lý logic khởi tạo và giảm số lần retries bằng cách sử dụng externalTask.getRetries().
  • Khi retries về 0, một incident sẽ được tạo trong Cockpit, báo hiệu cần có sự can thiệp của con người.
  • Incident chứa errorMessage và errorDetails giúp chẩn đoán lỗi.
  • Bạn có thể giải quyết incident và thử lại tác vụ trực tiếp từ giao diện Cockpit.

Xem trước bài học tiếp theo

Chúng ta đã thành thạo việc xử lý các lỗi kỹ thuật không lường trước. Nhưng còn về các lỗi nghiệp vụ thì sao? Trong bài học tiếp theo, "Báo cáo lỗi nghiệp vụ (BPMN Error) từ External Worker về process engine", chúng ta sẽ tìm hiểu cách sử dụng một phương thức khác, handleBpmnError, để báo cáo một lỗi nghiệp vụ cụ thể và kích hoạt một luồng xử lý thay thế đã được mô hình hóa ngay trong sơ đồ BPMN.