ADR-0003. Phát hành bất đồng bộ trên BullMQ 3 phân vùng với hàm băm order xác định
| Trường | Giá trị |
|---|---|
| Status | Accepted |
| Date | 2026-04-15 |
| Deciders | Phat Nguyen |
| Supersedes | - |
Bối cảnh
- Phát hành qua provider là I/O-bound (HTTP tới VNIS/VNPAY/T-VAN) và có thể lỗi tạm thời - không được chặn Kafka consumer hay request REST.
- Cùng một order không bao giờ được phát hành đồng thời trên nhiều worker (rủi ro phát hành kép).
- Thông lượng phải mở rộng theo chiều ngang trong khi vẫn giữ tuần tự hoá theo từng order.
- Retry cần backoff có giới hạn; lỗi vĩnh viễn (4xx) không được retry.
Quyết định
Chúng ta sẽ phát hành hoá đơn bất đồng bộ qua BullMQ với 3 phân vùng cho mỗi loại queue (issuance, claim-expiry). Một order được định tuyến tới phân vùng bằng getPartitionByKey(orderId) - hàm Java-hashCode mod 3 mang tính xác định - nên cùng một order luôn rơi vào cùng phân vùng. Job phát hành dùng jobId = orderId để đảm bảo idempotent.
Chính sách retry đến từ InvoiceProviderConfig.retryMetadata (mặc định maxRetryCount = 3, retryDelayMinutes = [5, 15, 60]). Lỗi 4xx vĩnh viễn (≠429) lập tức chuyển sang FAILED; job cạn lượt retry hoặc rơi vào DLQ sẽ lật hoá đơn sang FAILED và ghi một hàng audit. Mức concurrency của worker phát hành lấy từ APP_ENV_INVOICE_ISSUANCE_WORKER_CONCURRENCY (mặc định 10); claim-expiry cố định ở mức 3.
Hệ quả
| Ưu | Nhược |
|---|---|
| Tuần tự hoá theo từng order mà không cần khoá toàn cục | Số phân vùng (3) là hằng số cố định |
| Mở rộng theo chiều ngang qua tham số concurrency | Tái cân bằng phân vùng sau này sẽ đổi ánh xạ order→partition |
| Backoff có giới hạn; lỗi vĩnh viễn thất bại nhanh | Trạng thái retry nằm trên hàng hoá đơn (retryCount, metadata) |
| Claim-expiry là job có độ trễ (không cần polling) | Xử lý DLQ làm riêng theo từng loại worker |
Phương án đã cân nhắc
| Phương án | Ưu | Nhược | Vì sao loại |
|---|---|---|---|
| Phát hành đồng bộ trong handler Kafka | Đơn giản nhất | Chặn consumer; không cô lập retry | Lỗi provider tạm thời làm nghẽn pipeline |
| Một queue (không phân vùng) | Định tuyến đơn giản | Không có tính gắn kết theo order; concurrency gây rủi ro phát hành kép | Mất bảo đảm tuần tự hoá |
| Khoá phân tán cho mỗi order | Loại trừ lẫn nhau một cách tường minh | Tranh chấp khoá + rủi ro rò rỉ khi gặp lỗi | Băm theo phân vùng đạt được điều này mà không tốn thêm chi phí |
| Cron-poll chỉ cho hoá đơn đang chờ | Không cần hạ tầng queue | Độ trễ cao với mode REAL_TIME | Chỉ giữ cho mode SCHEDULED |
Tham chiếu
src/common/queues.ts(InvoiceQueuePartitions,getPartitionByKey, các định nghĩa)src/components/invoice-queue/component.ts(queue/worker phân vùng, DLQ)src/services/invoice-issuance-queue.service.ts(enqueueIssuance,_handleIssuanceFailure)- Xem thêm: API Events §3