Skip to content

ADR-0003. Phát hành bất đồng bộ trên BullMQ 3 phân vùng với hashing order tất định

TrườngGiá trị
StatusAccepted
Date2026-04-15
Decidersinvoice-team
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).
  • Throughput phải scale ngang trong khi giữ serialization 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 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) — Java-hashCode mod 3 tất định — nên cùng order luôn rơi vào cùng phân vùng. Job phát hành dùng jobId = orderId để idempotency.

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) short-circuit thành FAILED; job cạn lượt/DLQ lật hoá đơn sang FAILED và ghi một hàng audit. Concurrency worker phát hành là APP_ENV_INVOICE_ISSUANCE_WORKER_CONCURRENCY (mặc định 10); claim-expiry cố định 3.

Hệ quả

ƯuNhược
Serialization theo từng order không cần lock toàn cụcSố phân vùng (3) là hằng cố định
Scale ngang qua núm concurrencyTái cân bằng phân vùng sau này đổi ánh xạ order→partition
Backoff có giới hạn; lỗi vĩnh viễn fail nhanhTrạng thái retry nằm trên hàng hoá đơn (retryCount, metadata)
Claim-expiry là job delay (không polling)Xử lý DLQ là riêng biệt theo từng loại worker

Phương án đã cân nhắc

Phương ánƯuNhượcVì sao loại
Phát hành đồng bộ trong handler KafkaĐơn giản nhấtChặn consumer; không cô lập retryLỗ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ảnKhông có affinity theo order; concurrency rủi ro phát hành képMất bảo đảm serialization
Distributed lock mỗi orderLoại trừ tương hỗ rõ ràngTranh chấp lock + rủi ro rò khi lỗiHashing phân vùng đạt được điều này miễn phí
Cron-poll chỉ cho hoá đơn pendingKhông cần hạ tầng queueLatency cao cho mode REAL_TIMEChỉ giữ cho mode SCHEDULED

Tham chiếu

  • src/common/queues.ts (InvoiceQueuePartitions, getPartitionByKey, definitions)
  • 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

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.