Chào bạn,
Trong bài học trước, chúng ta đã cùng nhau xây dựng thành công một External Task Worker bằng Java. Worker này có khả năng kết nối tới Camunda Engine, nhận một công việc từ một topic cụ thể, thực thi logic, và báo cáo hoàn thành. Bạn đã thấy quy trình tự động tiếp tục chạy một cách liền mạch khi worker hoàn thành nhiệm vụ của nó.
Tuy nhiên, trong các hệ thống thực tế, không phải lúc nào mọi việc cũng suôn sẻ. Điều gì sẽ xảy ra nếu một tác vụ cần nhiều thời gian để xử lý, chẳng hạn như chuyển mã một video lớn hay tạo một báo cáo tài chính phức tạp? Hoặc tệ hơn, điều gì sẽ xảy ra nếu worker của bạn gặp sự cố và "chết" giữa chừng sau khi đã nhận việc?
Bài học hôm nay sẽ giải quyết chính xác những vấn đề này. Mục tiêu của chúng ta là cấu hình lock duration và timeout cho các tác vụ chạy dài (long-running tasks). Việc nắm vững các cơ chế này là cực kỳ quan trọng để xây dựng các worker mạnh mẽ, có khả năng chịu lỗi và đảm bảo tính nhất quán của quy trình.
1. Hiểu về Lock Duration và Timeout
Khi một worker gửi yêu cầu fetchAndLock tới Camunda Engine, engine sẽ tìm các tác vụ có sẵn, trả về cho worker và thực hiện một hành động quan trọng: khóa (lock) các tác vụ đó.
- Lock là gì? Một khi tác vụ đã được khóa cho một worker cụ thể, nó sẽ không hiển thị cho bất kỳ worker nào khác. Điều này đảm bảo rằng công việc không bị xử lý trùng lặp.
- Lock Duration là gì? Mỗi lần khóa, engine sẽ đặt một "đồng hồ đếm ngược", được gọi là
lockDuration. Đây chính là khoảng thời gian tối đa mà worker được phép giữ tác vụ đó.
Cơ chế này tạo ra một cơ chế timeout tự nhiên:
- Trường hợp Worker hoàn thành đúng hạn: Worker xử lý xong và gọi
complete()trước khilockDurationkết thúc. Engine mở khóa và quy trình tiếp tục. Đây là kịch bản lý tưởng. - Trường hợp Worker gặp sự cố (Timeout): Worker nhận tác vụ nhưng bị treo, crash, hoặc mất kết nối mạng. Sau khi
lockDurationkết thúc, engine sẽ tự động mở khóa tác vụ. Tác vụ đó sẽ trở lại trạng thái sẵn sàng và có thể được một worker khác (hoặc chính worker đó sau khi khởi động lại) nhận xử lý. - Trường hợp Worker xử lý quá lâu: Worker vẫn đang chạy nhưng không hoàn thành kịp trong
lockDuration. Tác vụ cũng sẽ bị mở khóa và một worker khác có thể nhận lấy, dẫn đến nguy cơ xử lý trùng lặp.
Do đó, việc thiết lập lockDuration là một sự cân bằng.
2. Cấu hình Lock Duration ban đầu
Đối với các tác vụ có thời gian thực thi dự đoán được, cách đơn giản nhất là đặt lockDuration đủ dài.
Setting the timing parameters for external task clients
Một người dùng trên diễn đàn Camunda cũng đã có thắc mắc tương tự về việc cấu hình các tham số thời gian. Hãy xem câu trả lời và khuyến nghị từ một chuyên gia của Camunda.
Đọc câu trả lời của Ingo_Richtsmeier. Hãy chú ý đến khuyến nghị cốt lõi: "The lockDuration should be the longest expected completion time."
Như bạn đã đọc, nguyên tắc chung là: lockDuration nên được đặt bằng thời gian hoàn thành dài nhất mà bạn dự kiến cho tác vụ.
Bạn có thể cấu hình lockDuration (tính bằng mili giây) theo hai cách chính khi sử dụng Spring Boot Starter:
Cách 1: Cấu hình trong annotation @ExternalTaskSubscription
Đây là cách phổ biến và linh hoạt nhất vì bạn có thể đặt thời gian khóa khác nhau cho mỗi loại tác vụ.
@Component
// Đặt lock duration là 10 phút (600,000 ms)
@ExternalTaskSubscription(topicName = "generate-report", lockDuration = 600000)
public class ReportGenerationHandler implements ExternalTaskHandler {
@Override
public void execute(ExternalTask externalTask, ExternalTaskService externalTaskService) {
// ... logic tạo báo cáo ...
externalTaskService.complete(externalTask);
}
}
Cách 2: Cấu hình chung cho toàn bộ Client
Bạn cũng có thể đặt một lockDuration mặc định cho tất cả các topic mà client này xử lý.
// Trong file cấu hình Spring Boot của bạn
@Configuration
public class ExternalTaskClientConfig {
@Bean
public ExternalTaskClient externalTaskClient(CamundaBpmProperties properties) {
return ExternalTaskClient.create()
.baseUrl(properties.getClient().getBaseUrl())
.workerId("my-report-worker")
.lockDuration(600000) // Lock mặc định 10 phút
.build();
}
}
Tuy nhiên, cách tiếp cận này đặt ra một vấn đề với các tác vụ chạy rất dài và không thể dự đoán trước thời gian.
3. Thách thức với các tác vụ chạy dài (Long-Running Tasks)
Hãy tưởng tượng một tác vụ chuyển mã video. Nó có thể mất 5 phút cho một video ngắn, nhưng có thể mất 5 giờ cho một bộ phim.
- Nếu bạn đặt
lockDuration= 10 phút: Khi xử lý bộ phim, lock sẽ hết hạn và một worker khác sẽ bắt đầu chuyển mã lại từ đầu, gây lãng phí tài nguyên và có thể gây ra xung đột dữ liệu. - Nếu bạn đặt
lockDuration= 6 giờ: Nếu worker bị crash sau 1 phút làm việc, tác vụ đó sẽ bị "treo" trong gần 6 giờ cho đến khi lock hết hạn và một worker khác có thể thử lại. Đây là một sự chậm trễ không thể chấp nhận được.
Giải pháp cho vấn đề này là một cơ chế "heartbeat" (nhịp tim).
4. Gia hạn Lock một cách linh hoạt với extendLock
Camunda cung cấp một giải pháp rất hiệu quả: cho phép worker gia hạn lock trong khi nó vẫn đang xử lý tác vụ.
Extending Locks of External Tasks
Bài viết sau đây từ blog của Camunda giải thích rất rõ vấn đề của việc khóa cố định và giới thiệu giải pháp gia hạn lock.
Hãy đọc bài viết này. Tập trung vào các phần: Vấn đề của việc đặt lock duration quá ngắn hoặc quá dài. Kịch bản so sánh trước và sau khi có Extend Lock, nó minh họa rõ lợi ích của việc báo cáo tiến độ. Phần "How to use it" để xem cú pháp API trong Java.
Như bạn vừa tìm hiểu, ý tưởng rất đơn giản:
- Worker nhận tác vụ với một
lockDurationban đầu hợp lý (ví dụ: 10 phút). - Worker bắt đầu xử lý công việc.
- Định kỳ (ví dụ, sau mỗi 5 phút), trong khi vẫn đang xử lý, worker sẽ gọi
externalTaskService.extendLock(...)để báo cho engine: "Tôi vẫn đang hoạt động, hãy cho tôi thêm 10 phút nữa." - Nếu worker bị crash, lock sẽ hết hạn chỉ sau một khoảng thời gian ngắn (tối đa 10 phút kể từ lần gia hạn cuối cùng), cho phép worker khác nhanh chóng tiếp nhận.
Ví dụ thực hành
Hãy sửa đổi worker của chúng ta để mô phỏng một tác vụ chuyển mã video dài.
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;
@Component
// 1. Đặt lock ban đầu là 2 phút (120,000 ms)
@ExternalTaskSubscription(topicName = "video-transcoding", lockDuration = 120000)
public class VideoTranscodingHandler implements ExternalTaskHandler {
private final static Logger LOGGER = LoggerFactory.getLogger(VideoTranscodingHandler.class);
@Override
public void execute(ExternalTask externalTask, ExternalTaskService externalTaskService) {
String videoFile = externalTask.getVariable("videoFile");
LOGGER.info("Bắt đầu chuyển mã video '{}' cho task ID: {}", videoFile, externalTask.getId());
try {
// Giả lập một công việc kéo dài 5 phút, chia thành 5 chu kỳ, mỗi chu kỳ 1 phút.
for (int i = 0; i < 5; i++) {
LOGGER.info("...Đang xử lý phần {}/5...", i + 1);
Thread.sleep(60000); // 2. Làm việc trong 1 phút
// 3. Gia hạn lock thêm 2 phút nữa
// Cung cấp đối tượng externalTask và thời gian gia hạn mới
externalTaskService.extendLock(externalTask, 120000);
LOGGER.info("Đã gia hạn lock thành công cho task ID: {}", externalTask.getId());
}
LOGGER.info("Hoàn tất chuyển mã video '{}'!", videoFile);
// 4. Hoàn thành tác vụ
externalTaskService.complete(externalTask);
} catch (InterruptedException e) {
LOGGER.error("Tác vụ bị gián đoạn", e);
Thread.currentThread().interrupt();
// Trong bài học sau, chúng ta sẽ học cách xử lý lỗi này một cách bài bản
// bằng externalTaskService.handleFailure(...)
}
}
}
Trong ví dụ này, tác vụ mất 5 phút để chạy, nhưng lockDuration ban đầu chỉ là 2 phút. Nếu không có extendLock, tác vụ sẽ bị timeout và xử lý lại. Nhờ việc gia hạn lock sau mỗi phút làm việc, worker đảm bảo rằng nó luôn giữ quyền kiểm soát tác vụ cho đến khi hoàn thành.
Tổng kết
Qua bài học này, bạn đã trang bị thêm hai công cụ mạnh mẽ để đảm bảo độ tin cậy cho các External Task Worker của mình.
Những điểm chính cần ghi nhớ:
lockDurationhoạt động như một cơ chế timeout. Nếu worker không hoàn thành hoặc gia hạn trong khoảng thời gian này, tác vụ sẽ được mở khóa để worker khác xử lý.- Đối với các tác vụ có thời gian thực thi dự đoán được, hãy đặt
lockDurationbằng thời gian hoàn thành dự kiến dài nhất. - Đối với các tác vụ chạy dài hoặc không thể dự đoán thời gian, hãy sử dụng chiến lược gia hạn lock. Bắt đầu với một lock ngắn và gọi
externalTaskService.extendLock()định kỳ để tạo ra một "heartbeat", báo hiệu rằng worker vẫn đang hoạt động.
Xem trước bài học tiếp theo
Chúng ta đã xử lý được trường hợp worker bị "chết" một cách bất ngờ (technical failure) bằng cơ chế timeout của lock. Tuy nhiên, chúng ta chưa chủ động xử lý các lỗi này trong code. Trong bài học tiếp theo, "Xử lý lỗi kỹ thuật trong External Worker để kích hoạt retry và tạo incident", chúng ta sẽ tìm hiểu cách sử dụng các phương thức như handleFailure() để báo cáo lỗi một cách tường minh cho engine, cấu hình số lần thử lại (retries), và cách engine tạo ra các "incident" trong Cockpit để người vận hành có thể điều tra và xử lý.