ADR-0001. Tạo sổ bất đồng bộ do Kafka điều khiển qua một topic self-loop
| Trường | Giá trị |
|---|---|
| Status | Accepted |
| Date | 2026-03-30 |
| Deciders | ledger-team |
| Supersedes | — |
Bối cảnh
- Tạo một sổ là chậm và bùng nổ: lấy dữ liệu + render PDF Typst + render XLSX ExcelJS + mã hoá AES + upload S3, dễ tới vài giây mỗi tài liệu, nhân lên trên một batch cả năm.
- Một request HTTP đồng bộ không thể giữ mở lâu vậy, và một request bị crash sẽ để lại file S3 dang dở và trạng thái mơ hồ.
- Việc tạo phải retry được và sống sót qua crash của worker giữa pipeline.
Quyết định
Chúng ta sẽ tách enqueue khỏi execution bằng một topic Kafka ledger.generate mà service vừa produce tới (role api) vừa consume từ (role worker). Request HTTP trả về ngay với một LedgerJob ở PENDING; worker thực thi handleGeneration(ledgerId) và báo tiến độ qua WebSocket.
Consumer chạy với autocommit: false và commit chỉ sau khi upload + finalize thành công. Trạng thái job là một máy LedgerJob riêng (PENDING → PROCESSING → COMPLETED|REJECTED) được claim qua UPDATE có điều kiện nguyên tử.
Hệ quả
| Ưu | Nhược |
|---|---|
| Response HTTP nhanh; render lâu nằm ngoài đường request | Nhất quán cuối cùng — client phải poll/subscribe trạng thái |
Enqueue idempotent trên (merchantId, type, period) | Một message đã commit không bao giờ auto-replay; recovery cần retry rõ ràng hoặc quét kẹt |
| Scale ngang qua số consumer / replica worker | Người vận hành phải hiểu self-loop (không có producer/consumer ngoài) |
Phục hồi crash qua RecoveryComponent re-enqueue job kẹt | Hơi nhiều bộ phận chuyển động hơn một queue BullMQ |
Phương án đã cân nhắc
| Phương án | Ưu | Nhược | Vì sao loại |
|---|---|---|---|
| Tạo HTTP đồng bộ | Đơn giản nhất | Kết nối giữ lâu, không phục hồi crash, file dang dở | Không khả thi cho sổ batch/lớn |
| Queue BullMQ | Retry/backoff có sẵn | Thêm một bề mặt hạ tầng; Kafka đã có trong stack | Tái dùng Kafka sẵn thay vì thêm ngữ nghĩa Redis-queue |
| Auto-replay khi consume thất bại | Tự chữa | Rủi ro bão poison-message khi parse thất bại tất định | Retry thủ công + quét kẹt có giới hạn 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(quét kẹt)- Generation Pipeline