Skip to main content

Welcome to a Grasp lesson

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

Lesson 41

Bắt và xử lý lỗi Service Task với Error Boundary Event

Recommended resources

Chào bạn,

Trong bài học trước, chúng ta đã học cách sử dụng Signal Events để thực hiện giao tiếp một-nhiều (broadcast), một cơ chế rất mạnh mẽ để đồng bộ hóa nhiều quy trình. Bạn đã thấy cách một tín hiệu duy nhất có thể kích hoạt hoặc thay đổi luồng của hàng loạt process instance đang chạy.

Tuy nhiên, trong quá trình thực thi một Service Task, đặc biệt là khi nó giao tiếp với các hệ thống bên ngoài như REST API hoặc cơ sở dữ liệu, mọi thứ không phải lúc nào cũng diễn ra suôn sẻ. Các lỗi có thể xảy ra: API bị sập, dữ liệu đầu vào không hợp lệ, tài khoản không đủ tiền, v.v. Việc để những lỗi này làm sập toàn bộ quy trình là điều không thể chấp nhận được trong một hệ thống sản xuất.

Vì vậy, mục tiêu của bài học hôm nay là: Sử dụng Error Boundary Event để bắt và xử lý các exception từ một Service Task. Bạn sẽ học cách xây dựng các quy trình có khả năng phục hồi, có thể dự đoán và xử lý các "lỗi nghiệp vụ" (business errors) một cách có kiểm soát, thay vì để chúng trở thành các sự cố kỹ thuật (technical incidents).


1. Phân biệt Lỗi nghiệp vụ và Lỗi kỹ thuật

Trước khi đi vào thực hành, điều quan trọng nhất là phải phân biệt được hai loại lỗi chính mà bạn sẽ gặp phải. Là một developer, việc hiểu rõ sự khác biệt này sẽ giúp bạn quyết định cách xử lý phù hợp.

  • Lỗi kỹ thuật (Technical Errors): Đây là những lỗi không lường trước được, thường liên quan đến cơ sở hạ tầng. Ví dụ: mất kết nối mạng, API của bên thứ ba tạm thời không khả dụng, lỗi hết bộ nhớ.

    • Cách xử lý điển hình: Tự động thử lại (retry) một vài lần. Nếu sau nhiều lần thử lại vẫn thất bại, hệ thống sẽ tạo ra một "incident" trong Camunda Cockpit để người vận hành can thiệp thủ công. Những logic này thường không được vẽ trực tiếp trên sơ đồ BPMN để tránh làm phức tạp hóa mô hình.
  • Lỗi nghiệp vụ (Business Errors): Đây là những lỗi có thể dự đoán được, là một phần của luồng logic nghiệp vụ. Ví dụ: "khách hàng không đủ tín dụng", "sản phẩm đã hết hàng", "mã giảm giá không hợp lệ".

    • Cách xử lý điển hình: Quy trình cần có một luồng xử lý thay thế rõ ràng. Ví dụ, nếu khách hàng không đủ tín dụng, quy trình sẽ chuyển sang bước "Thông báo từ chối" thay vì dừng lại. Những luồng thay thế này phải được mô hình hóa rõ ràng trên sơ đồ BPMN bằng cách sử dụng các công cụ như Error Boundary Event.

Video sau đây sẽ giúp bạn hình dung rõ hơn về hai loại lỗi này.

8 Tutorial Error Handling

Video này của Camunda giải thích sự khác biệt cơ bản giữa lỗi nghiệp vụ (business errors) được xử lý bằng các ký hiệu BPMN và lỗi kỹ thuật (technical errors) dẫn đến các incident.

Hãy xem từ 01:28 đến 02:11. Chú ý cách video minh họa Error Boundary Event như một công cụ để xử lý business error và cách các lỗi kỹ thuật được xử lý ngầm bằng cơ chế retry.

Bài học này sẽ tập trung hoàn toàn vào việc xử lý lỗi nghiệp vụ bằng Error Boundary Event.



2. Triển khai Error Boundary Event: Ném và Bắt

Việc xử lý lỗi bằng Error Boundary Event bao gồm hai bước chính, giống như cặp try-catch trong lập trình:

  1. Ném (Throwing): Mã Java bên trong Service Task của bạn phải chủ động "ném" ra một lỗi nghiệp vụ khi phát hiện một điều kiện cụ thể.
  2. Bắt (Catching): Một Error Boundary Event được gắn vào Service Task đó trong mô hình BPMN sẽ "bắt" lỗi này và chuyển hướng quy trình sang một luồng xử lý khác.

Bước 1: Ném lỗi từ Java Delegate

Trong Camunda, bạn không ném một Exception thông thường. Thay vào đó, bạn ném một đối tượng đặc biệt là org.camunda.bpm.engine.delegate.BpmnError. Đối tượng này mang một mã lỗi (error code), đóng vai trò như một chiếc chìa khóa để liên kết với Error Boundary Event trong mô hình.

// Giả sử trong một Service Task xử lý thanh toán
if (customerBalance < orderAmount) {
    // Ném một lỗi nghiệp vụ với một mã lỗi cụ thể
    throw new BpmnError("INSUFFICIENT_FUNDS", "Khách hàng không đủ tiền để thực hiện giao dịch.");
}
  • "INSUFFICIENT_FUNDS" là mã lỗi. Đây là thông tin quan trọng nhất.
  • "Khách hàng..." là thông điệp lỗi, có thể được lưu lại để phân tích sau.

Bước 2: Bắt lỗi trong Camunda Modeler

Trong mô hình BPMN, bạn sẽ gắn một Error Boundary Event vào Service Task có khả năng ném lỗi.

Camunda Error Boundary Event Configuration
Hình ảnh cho thấy một Error Boundary Event được gắn vào Service Task. Trong Properties Panel, chúng ta cấu hình 'Error Code' để khớp với mã lỗi được ném từ Java. Bạn cũng có thể chỉ định một 'Error Message Variable' để lưu lại thông điệp lỗi.

Cấu hình chi tiết như sau:

  1. Kéo một Boundary Event và thả nó lên Service Task.
  2. Click chuột phải vào nó, chọn Change Type -> Error Boundary Event. Biểu tượng sẽ chuyển thành hình tia sét.
  3. Chọn Error Boundary Event và vào Properties Panel, tab Details:
    • Error Code Variable: Nhập vào một chuỗi ký tự chính xác khớp với mã lỗi bạn đã định nghĩa trong mã Java (ví dụ: INSUFFICIENT_FUNDS).
    • Error Message Variable (Tùy chọn): Đặt tên cho một biến quy trình (ví dụ: paymentErrorMessage) để lưu lại thông điệp lỗi.
  4. Vẽ một luồng đi từ Error Boundary Event đến các bước xử lý lỗi (ví dụ: một User Task "Thông báo cho khách hàng").

Để xem một ví dụ hoàn chỉnh về cách ném và bắt lỗi, hãy xem video hướng dẫn sau đây.

Camunda Platform 7 Tutorial: Running Timer Events, Catching BPMN Error Events in Spring Boot Project

Video này của Camunda sẽ hướng dẫn bạn từng bước cách implement cơ chế xử lý lỗi: từ việc viết code Java để ném BpmnError, đến việc mô hình hóa Error Boundary Event để bắt nó.

Hãy xem từ 06:03 đến 08:56. Chú ý các bước sau: (06:03 - 07:21) Cách thêm logic vào Java Delegate để kiểm tra một điều kiện và throw new BpmnError("fall_overboard"). (07:21 - 07:55) Cách thêm một Error Boundary Event vào Service Task trong Modeler. (07:55 - 08:56) Cách cấu hình Error Boundary Event: đặt tên, và quan trọng nhất là thiết lập 'Error Code' để khớp với mã lỗi được ném từ code. Đồng thời, cấu hình một biến để lưu thông điệp lỗi.



3. Thực hành: Xử lý lỗi thanh toán không thành công

Bây giờ, hãy áp dụng kiến thức vào một kịch bản thực tế. Chúng ta sẽ xây dựng một quy trình xử lý đơn hàng, trong đó bước thanh toán có thể thất bại do không đủ tiền.

Kịch bản:

  1. Người dùng đặt hàng và nhập thông tin thanh toán.
  2. Hệ thống gọi đến Service Task "Xử lý thanh toán".
  3. Service Task này sẽ kiểm tra một biến customerBalance.
    • Nếu customerBalance đủ, quy trình tiếp tục đến bước "Giao hàng".
    • Nếu customerBalance không đủ, nó sẽ ném lỗi INSUFFICIENT_FUNDS.
  4. Error Boundary Event sẽ bắt lỗi này và chuyển quy trình sang bước "Thông báo thanh toán thất bại" cho người dùng.

Bước 1: Mô hình hóa quy trình

  1. Trong Camunda Modeler, tạo một quy trình như sau:

    • Start Event
    • User Task: Xác nhận đơn hàng
    • Service Task: Xử lý thanh toán
    • End Event: Hoàn tất
    • User Task: Thông báo thanh toán thất bại
    • End Event: Đóng đơn hàng
  2. Gắn một Error Boundary Event vào Service Task "Xử lý thanh toán".

  3. Nối Error Boundary Event đến User Task "Thông báo thanh toán thất bại". Nối task này đến End Event "Đóng đơn hàng".

  4. Nối luồng thành công từ "Xử lý thanh toán" đến End Event "Hoàn tất".

Quy trình của bạn sẽ trông giống như thế này:

Camunda BPMN Diagram with Error Boundary Event Configuration
Ví dụ về một quy trình xử lý lỗi. Khi 'Execute Error Function' (tương đương 'Xử lý thanh toán' của chúng ta) ném lỗi, luồng sẽ được chuyển hướng thay vì kết thúc đột ngột.

Bước 2: Cấu hình các phần tử

  1. Chọn Service Task Xử lý thanh toán:
    • Implementation: Java Class
    • Java Class: com.example.camunda.delegates.PaymentDelegate
  2. Chọn Error Boundary Event:
    • Trong Properties Panel, tab Details:
    • Name: Lỗi không đủ tiền
    • Error Code: INSUFFICIENT_FUNDS
    • Error Message Variable: paymentError

Bước 3: Viết mã Java Delegate

Tạo lớp PaymentDelegate.java trong dự án Spring Boot của bạn.

package com.example.camunda.delegates;

import org.camunda.bpm.engine.delegate.BpmnError;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component("PaymentDelegate")
public class PaymentDelegate implements JavaDelegate {

    private final Logger LOGGER = LoggerFactory.getLogger(PaymentDelegate.class);

    @Override
    public void execute(DelegateExecution execution) throws Exception {
        // Lấy các biến từ quy trình
        Double orderTotal = (Double) execution.getVariable("orderTotal");
        Double customerBalance = (Double) execution.getVariable("customerBalance");

        LOGGER.info(">>> Processing payment for order total: {}. Customer balance: {}", orderTotal, customerBalance);

        if (customerBalance < orderTotal) {
            LOGGER.error("!!! Insufficient funds. Throwing BPMN Error.");
            // Ném lỗi nghiệp vụ với mã lỗi và thông điệp
            throw new BpmnError("INSUFFICIENT_FUNDS", "Số dư tài khoản không đủ để thanh toán đơn hàng.");
        }

        // Nếu không có lỗi, thực hiện logic thanh toán thành công
        LOGGER.info(">>> Payment successful!");
        double newBalance = customerBalance - orderTotal;
        execution.setVariable("customerBalance", newBalance);
    }
}

Bước 4: Deploy và kiểm thử

  1. Deploy quy trình và khởi chạy ứng dụng.
  2. Kiểm thử luồng thành công:
    • Vào Tasklist, khởi tạo một process instance mới.
    • Trong form khởi tạo, thêm 2 biến: orderTotal (Type: Double, Value: 100) và customerBalance (Type: Double, Value: 500).
    • Start process. Hoàn thành task Xác nhận đơn hàng.
    • Vào Cockpit, bạn sẽ thấy quy trình đã đi qua Xử lý thanh toán và kết thúc tại Hoàn tất. Biến customerBalance đã được cập nhật thành 400.
  3. Kiểm thử luồng lỗi:
    • Khởi tạo một process instance khác.
    • Lần này, đặt biến: orderTotal (Type: Double, Value: 100) và customerBalance (Type: Double, Value: 50).
    • Start process. Hoàn thành task Xác nhận đơn hàng.
    • Lần này, vào Cockpit, bạn sẽ thấy token đã đi theo con đường của Error Boundary Event và đang dừng lại ở task Thông báo thanh toán thất bại.
    • Kiểm tra tab Variables của instance này, bạn sẽ thấy biến paymentError đã được tạo với giá trị là "Số dư tài khoản không đủ để thanh toán đơn hàng.".


4. Khi nào dùng Error Event, khi nào dùng Gateway?

Một câu hỏi quan trọng mà các developer thường đặt ra: "Tại sao không dùng một Exclusive Gateway sau Service Task để kiểm tra kết quả và rẽ nhánh?".

Đây là một câu hỏi rất hay, và câu trả lời nằm ở ngữ nghĩa của nghiệp vụ.

Dealing with problems and exceptions - Camunda 8 Docs

Tài liệu chính thức của Camunda cung cấp một hướng dẫn rất rõ ràng về việc khi nào nên dùng Gateway và khi nào nên dùng Error Event. Đây là kiến thức quan trọng để thiết kế các quy trình rõ ràng và đúng đắn.

Hãy đọc kỹ phần "Distinguishing between exceptions and results". Chú ý quy tắc cốt lõi: Sử dụng Gateway khi bạn xử lý một kết quả mong đợi của một hoạt động. Ví dụ: task "Kiểm tra tín dụng khách hàng" có kết quả là 'đủ' hoặc 'không đủ'. Cả hai đều là kết quả hợp lệ. Sử dụng Error Event khi một lỗi ngăn cản hoạt động đạt được kết quả mong đợi. Ví dụ: task "Kiểm tra tín dụng" không thể hoàn thành vì ID khách hàng không tồn tại. Hoạt động đã thất bại.

Trong ví dụ thanh toán của chúng ta, việc "không đủ tiền" có thể được xem như một kết quả (dùng Gateway) hoặc một lỗi (dùng Error Event). Việc sử dụng Error Boundary Event nhấn mạnh rằng đây là một tình huống ngoại lệ, một sự thất bại trong việc hoàn thành hành động "Xử lý thanh toán". Cách tiếp cận này thường làm cho sơ đồ BPMN rõ ràng hơn về "happy path".



Tổng kết

Qua bài học này, bạn đã trang bị thêm một công cụ cực kỳ quan trọng để xây dựng các quy trình nghiệp vụ mạnh mẽ và linh hoạt.

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

  • Phân biệt rõ ràng giữa lỗi kỹ thuật (xử lý bằng retry, incident) và lỗi nghiệp vụ (xử lý bằng luồng thay thế trong BPMN).
  • Error Boundary Event là công cụ chính để mô hình hóa việc xử lý lỗi nghiệp vụ.
  • Quy trình xử lý lỗi gồm 2 bước: Ném một BpmnError với một errorCode từ trong JavaDelegate, và Bắt nó bằng một Error Boundary Event được cấu hình với cùng errorCode đó.
  • Việc sử dụng Error Event hay Gateway phụ thuộc vào việc bạn xem một tình huống là "kết quả mong đợi" hay là một "lỗi ngoại lệ".

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

Trong các Service Task và Gateway của chúng ta, các quy tắc nghiệp vụ (business rules) như "khách hàng nào được coi là VIP?", "mức giảm giá cho đơn hàng trên 1000$ là bao nhiêu?" thường được hard-code trong Java hoặc viết dưới dạng biểu thức (Expression Language). Khi các quy tắc này trở nên phức tạp hoặc thay đổi thường xuyên, việc duy trì chúng trong code trở nên khó khăn.

Trong bài học tiếp theo, chúng ta sẽ tìm hiểu về DMN (Decision Model and Notation), một tiêu chuẩn cho phép bạn tách biệt các quy tắc nghiệp vụ ra khỏi quy trình, quản lý chúng dưới dạng các bảng quyết định trực quan và dễ thay đổi mà không cần deploy lại code.