ADR-0001. Sinh sổ bất đồng bộ do Kafka điều khiển qua một topic tự lặp
| Trường | Giá trị |
|---|---|
| Status | Accepted |
| Date | 2026-03-30 |
| Deciders | Phat Nguyen |
| Supersedes | - |
Bối cảnh
- Sinh một sổ vừa chậm vừa dồn dập: lấy dữ liệu + render PDF bằng Typst + render XLSX bằng ExcelJS + mã hoá AES + tải lên S3, dễ mất vài giây mỗi tài liệu và nhân lên cho cả một batch trọn năm.
- Một request HTTP đồng bộ không thể giữ kết nối mở lâu như vậy, và nếu request gặp sự cố giữa chừng sẽ để lại file S3 dang dở cùng trạng thái không rõ ràng.
- Việc sinh sổ phải có thể thử lại được và sống sót qua sự cố của worker giữa chừng pipeline.
Quyết định
Chúng ta sẽ tách việc đưa vào hàng đợi khỏi việc thực thi bằng một topic Kafka ledger.generate mà service vừa ghi vào (role api) vừa đọc ra (role worker). Request HTTP trả về ngay với một LedgerJob ở trạng thái PENDING; worker thực thi handleGeneration(ledgerId) và báo tiến độ qua WebSocket.
Consumer chạy với autocommit: false và chỉ commit sau khi tải lên + hoàn tất thành công. Trạng thái job là một máy trạng thái LedgerJob riêng (PENDING → PROCESSING → COMPLETED|REJECTED), được giành quyền xử lý qua một lệnh UPDATE có điều kiện diễn ra như một thao tác duy nhất.
Hệ quả
| Ưu | Nhược |
|---|---|
| Response HTTP nhanh; phần render lâu nằm ngoài luồng xử lý request | Nhất quán sau cùng - client phải hỏi thăm hoặc đăng ký nhận trạng thái |
Đưa vào hàng đợi mang tính idempotent trên (merchantId, type, period) | Một message đã commit không bao giờ tự phát lại; việc khôi phục cần thử lại tường minh hoặc nhờ vào lượt quét job kẹt |
| Mở rộng theo chiều ngang bằng cách tăng số consumer / số bản worker | Người vận hành phải hiểu cơ chế tự lặp (không có producer/consumer bên ngoài) |
Khôi phục sau sự cố nhờ RecoveryComponent đưa lại các job kẹt vào hàng đợi | Nhiều thành phần vận động hơn so với một queue BullMQ một chút |
Phương án đã cân nhắc
| Phương án | Ưu | Nhược | Vì sao loại |
|---|---|---|---|
| Sinh sổ đồng bộ qua HTTP | Đơn giản nhất | Kết nối giữ lâu, không khôi phục được sau sự cố, để lại file dang dở | Không khả thi cho sổ lớn hoặc xử lý theo batch |
| Dùng queue BullMQ | Có sẵn cơ chế thử lại/backoff | Thêm một lớp hạ tầng nữa; Kafka thì đã có sẵn trong stack | Tận dụng lại Kafka thay vì thêm cơ chế queue trên Redis |
| Tự phát lại khi đọc thất bại | Tự khắc phục | Rủi ro bão message lỗi khi việc parse thất bại một cách tất định | Thử lại thủ công + lượt quét job kẹt có giới hạn thì an toàn hơn |
Tham chiếu
ledger/src/services/ledger-queue.service.ts(handleEnqueueGeneration)ledger/src/services/ledger-worker.service.ts(handleGeneration)ledger/src/components/kafka.component.ts(consumerautocommit: false)ledger/src/components/recovery.component.ts(lượt quét job kẹt)- Generation Pipeline