ADR 0012: Bus Redis Streams, outbox dùng chung trong Redis và lease bộ gửi
1. Tiêu đề quyết định (Title)
Dùng Redis Streams làm bus giữa ingest và worker, đặt outbox sự kiện trong Redis và điều phối bộ gửi bằng lease Redis, thay cho bus in-process và kế hoạch NATS JetStream ở giai đoạn này.
2. Trạng thái (Status)
Chấp nhận (Accepted) ngày 01/10/2026 bởi Product Owner. Ghi nhận một quyết định đã thực hiện trong mã (COL-R). Chấp nhận với điều kiện đã biết: Redis ở dev chỉ để phát triển, chưa có Sentinel hoặc Cluster, nên độ bền của bus, outbox và lease bằng độ bền của một Redis (xem ADR 0013, docs/08).
Một phần thay thế ADR 0009 (ADR 0009 vẫn giữ nguyên interface Bus và 64 shard; phần "GĐ 3 dùng NATS JetStream" và nhận định "Redis Streams không chọn" không còn khớp mã hiện tại).
Một phần thay thế ADR 0009 (ADR 0009 vẫn giữ nguyên interface Bus và 64 shard; phần "GĐ 3 dùng NATS JetStream" và nhận định "Redis Streams không chọn" không còn khớp mã hiện tại).
Ngày quyết định: 30/09/2026 (ngày ghi nhận đề xuất; ngày cài đặt thực tế xem lịch sử git, commit COL-R).
3. Ngữ cảnh (Context)
- ADR 0009 định nghĩa interface
Busvớiinprocở giai đoạn 1 và JetStream ở giai đoạn 3, và ghi Redis Streams đã cân nhắc, không chọn vì khó bảo đảm bền vững khi mất Redis. - Ngày 30/09/2026 tác vụ COL-R tách tiến trình
ingest,worker,adminđể chạy riêng. Yêu cầu của người dùng: môi trường dev phải mô phỏng đầy đủ như sản xuất, không làm tạm (xem ADR 0013). - Redis đã là phụ thuộc bắt buộc của hệ thống (registry, presence, giới hạn tốc độ), nên dùng thêm cho bus không thêm thành phần vận hành mới. JetStream chưa được cài đặt.
- Ràng buộc: NFR-07 (đệm khi mất TSDB), NFR-08 (outbox 24 giờ), R9 (ít thành phần).
4. Quyết định (Decision)
Cho giai đoạn 1 đến 2, thay vì inproc rồi JetStream:
- Bus: Redis Streams, một stream mỗi shard logic (
bus:s:<shard>, 64 shard,hash(server_id) mod 64), một consumer groupworkers. Bản ghi bị xóa sau khi handler xong, nên độ dài stream là backlog. Tiến trình chết giữa chừng để lại bản ghi chưa xác nhận, tiến trình khác nhận lại bằngXAUTOCLAIMsauclaim_idle(mặc định 30 giây). Ngữ nghĩa giao hàng: ít nhất một lần, trùng lặp vô hại vì mẫu idempotent theo timestamp. Khi backlog vượtqueue_per_shard, ingest trả 503 (ErrFull) và agent giữ đệm. - Outbox sự kiện: dùng chung trong Redis (
ob:qzset theo thời gian,ob:ehash sự kiện,ob:deadhash sự kiện chết) để mọi tiến trình đều đưa vào, một tiến trình gửi. - Lease bộ gửi: khóa
outbox:sender(SET NX PX, TTL 15 giây, gia hạn, trả lại khi tắt êm) bảo đảm một bộ gửi tại một thời điểm. Lease không có fencing, chỉ bảo vệ công việc an toàn khi chạy hai lần (gửi idempotent theoseq). - Phạm vi: Collector, các vai trò
ingest,worker. InterfaceBusgiữ nguyên để có thể thay bằng JetStream sau này.
5. Cơ sở lý luận (Rationale)
Ưu điểm:
- Không thêm thành phần: Redis đã bắt buộc, giảm tăng độ phức tạp (R9).
- Cho phép tách ingest và worker chạy ở nhiều tiến trình ngay bây giờ, đúng yêu cầu mô phỏng sản xuất.
- Có kiểm thử với Redis thật (
internal/bus/redis_test.go,internal/outbox/redis_test.go).
Nhược điểm chấp nhận được (cần ghi rõ):
- Độ bền bus và outbox bằng độ bền của Redis. Cần AOF
appendfsync everysectối thiểu và replica (docs/07); mất tối đa khoảng 1 giây ghi khi Redis sập, và Redis mất hẳn thì mất backlog. Đây chính là lý do ADR 0009 không chọn Redis Streams. - Khoảng 500 ms mẫu đang gộp ở worker có thể mất khi worker sập.
- Redis dùng chính sách
noeviction, Redis đầy làm ingest lỗi thay vì âm thầm bỏ dữ liệu. - Lease không fencing: trong cửa sổ TTL có thể có hai bộ gửi (hai lần gửi, không mất dữ liệu nhờ idempotent).
- Mã hiện thực không dùng lease Redis để gán shard cho worker như ADR 0009 mô tả; chia việc bằng consumer group,
XAUTOCLAIMxử lý worker chết. Việc trạng thái đánh giá luật vẫn nằm trong bộ nhớ một worker cần được xác nhận ở L2 Collector (chưa kiểm chứng trong phạm vi báo cáo này).
Phương án đã xem xét: xem mục 7.
6. Ảnh hưởng (Consequences)
- Tích cực: chạy được nhiều tiến trình sau cân tải, mô phỏng sản xuất ở dev, backlog quan sát được qua
ahc_*. - Rủi ro: Redis trở thành điểm lỗi đơn cho bus và outbox nếu không có replica và AOF. Sentinel hoặc Cluster chưa có ở dev (
docs/09). - Việc tiếp theo:
- Cập nhật ADR 0009 sang trạng thái "Một phần bị thay thế bởi ADR 0012" khi ADR này được chấp nhận (không sửa trong đợt này vì thuộc sở hữu tài liệu khác).
docs/04mục ghi "Bus in-process ... tách vai trò cần adapter JetStream (chưa làm)" đã lạc hậu so với mã, cần cập nhật.- Quyết định giữ Redis Streams hay chuyển JetStream ở giai đoạn 3 dựa trên kiểm thử hỗn loạn (X-9: mất Redis, TSDB, node).
- Kiểm thử mất Redis giữa chừng với AOF bật.
7. Các phương án được đánh giá (Considered Options)
| Phương án | Ưu điểm | Nhược điểm | Kết luận |
|---|---|---|---|
Bus inproc (ADR 0009 giai đoạn 1) | Đơn giản, nhanh | Không tách tiến trình, không bền, không mô phỏng sản xuất | Không chọn cho dev đầy đủ, vẫn còn ở dạng cấu hình một tiến trình |
| NATS JetStream (ADR 0009 giai đoạn 3) | Lưu tệp, nhân bản, consumer bền | Thêm một thành phần vận hành, chưa cài đặt | Hoãn, xem lại ở giai đoạn 3 |
| Redis Streams | Không thêm thành phần, tách tiến trình được | Độ bền phụ thuộc AOF và replica của Redis | Chọn (đề xuất) |
| Kafka | Mạnh, bền | Nặng vận hành (ADR 0009) | Không chọn |
8. Tài liệu liên quan (Related Artifacts)
- L2: L2 Collector (đang được viết), L1 HLD
- ADR: 0009, 0008, 0013
- Tài liệu:
docs/04-data-model.md,docs/08-scaling-ha.md,docs/07-security.md,docs/11-roadmap.md(COL-R) - Mã:
internal/bus/redis.go,internal/outbox/redis.go,internal/outbox/sender.go,internal/redisx/lease.go - Rủi ro: R9, R15
9. Phê duyệt (Approvals)
| Tên | Vai trò | Ghi chú |
|---|---|---|
| chưa chỉ định | Enterprise Architect | Chưa duyệt |
| chưa chỉ định | Solution Architect | Chưa duyệt |
| Thiện Phan Ngọc | Product Owner | Chấp nhận 01/10/2026. Redis dev chưa có replica HA, chấp nhận |