Hệ thống ngoài sẽ lỗi — timeout, 503, mất mạng chớp nhoáng. Phần lớn lỗi là tạm thời (transient) và sẽ hết nếu thử lại sau vài giây. Retry đúng cách là kỹ năng cốt lõi của orchestration. Nhưng retry sai cách còn tệ hơn không retry: bạn có thể trừ tiền khách hai lần, hoặc "đánh sập" chính hệ thống đang gặp sự cố.
Phân loại lỗi trước khi retry
Không phải lỗi nào cũng nên thử lại:
- Transient (nên retry): timeout, 502/503/504, connection reset, rate-limited 429.
- Permanent (KHÔNG retry): 400 (payload sai), 401/403 (sai key/quyền), 422 (nghiệp vụ từ chối, ví dụ "hết hàng").
- Không rõ (retry thận trọng): 500 chung chung — retry vài lần rồi bỏ.
Exponential backoff + jitter
Đừng retry ngay tức khắc và đều đặn. Dùng backoff lũy thừa cộng jitter (nhiễu ngẫu nhiên) để tránh "thundering herd" — hàng loạt client cùng retry một lúc.
import random
def backoff_delay(attempt, base=1.0, cap=60.0):
# attempt: 1,2,3...
exp = min(cap, base * (2 ** (attempt - 1)))
return exp / 2 + random.uniform(0, exp / 2) # jitter
# attempt 1 -> ~0.5-1s, 2 -> ~1-2s, 3 -> ~2-4s ...
Giới hạn số lần và đường chết
Luôn có trần. Khi vượt trần, đơn không biến mất — nó đi vào Dead Letter Queue (DLQ) hoặc trạng thái FAILED để người thật xử lý.
def run_step(row, adapter):
try:
result = adapter.do(row.context())
row.apply_success(result)
except PermanentError:
row.state = "FAILED" # không retry
except TransientError:
row.attempt_count += 1
if row.attempt_count >= MAX_ATTEMPTS:
row.state = "FAILED" # -> DLQ, cần bù trừ
alert_ops(row.order_id)
else:
queue.push(row.order_id,
delay=backoff_delay(row.attempt_count))
db.save(row)
Ghi attempt_count vào state bền, không đếm trong RAM — nếu không, restart sẽ reset bộ đếm và retry vô hạn.
Circuit breaker
Nếu một hệ thống ngoài đang chết hàng loạt, đừng cứ nện request vào nó. Circuit breaker mở mạch sau N lỗi liên tiếp, tạm dừng gọi trong T giây, rồi thử "nửa mở" một request thăm dò. Việc này bảo vệ cả bạn lẫn đối tác.
Checklist retry an toàn
- [ ] Đã phân loại transient vs permanent bằng mã lỗi cụ thể.
- [ ] Backoff lũy thừa có jitter, có cap trần.
- [ ]
attempt_countlưu bền, không nằm trong bộ nhớ. - [ ] Có trần retry và có DLQ/
FAILEDkhi vượt. - [ ] Mỗi retry dùng cùng idempotency key (xem bài 5).
- [ ] Cảnh báo ops khi một đơn vào FAILED.
Các bước triển khai
- Lập bảng ánh xạ mã lỗi → transient/permanent cho từng adapter.
- Thêm hàm
backoff_delayvà dùng nó khi đẩy job lại. - Lưu và tăng
attempt_counttrong cùng transaction với state. - Đặt
MAX_ATTEMPTSvà tuyến FAILED/alert. - Test bằng cách mock adapter trả 503 vài lần rồi 200 — xác nhận đơn cuối cùng thành công.
Tôn trọng tín hiệu Retry-After
Khi hệ thống ngoài trả 429 (rate limited) hoặc 503, chúng thường kèm header Retry-After cho biết nên chờ bao lâu. Hãy ưu tiên giá trị này thay vì backoff tự tính — đối tác biết rõ khi nào họ sẵn sàng hơn bạn. Bỏ qua Retry-After và nện request sớm chỉ khiến bạn bị chặn lâu hơn.
def next_delay(resp, attempt):
ra = resp.headers.get("Retry-After")
if ra:
return float(ra) # tôn trọng đối tác
return backoff_delay(attempt) # fallback tự tính
Timeout: đừng chờ vô hạn
Retry chỉ có ý nghĩa khi mỗi lần gọi có timeout rõ ràng. Nếu một lời gọi treo mãi (không timeout), worker của bạn kẹt và bước không bao giờ được đánh dấu để retry. Đặt hai loại timeout: connect timeout (thời gian bắt tay kết nối) và read timeout (thời gian chờ phản hồi). Timeout nên ngắn hơn nhiều so với chu kỳ backoff để một request chậm không làm nghẽn cả pipeline.
Khung tư duy: retry ở đâu trong quy trình
Retry nên nằm ở tầng orchestrator (đẩy lại job vào queue với delay), không phải một vòng for chặn ngay trong lời gọi HTTP. Retry trong vòng lặp đồng bộ giữ worker bận suốt thời gian chờ backoff — lãng phí. Retry qua queue giải phóng worker làm việc khác, và giúp attempt_count được lưu bền giữa các lần thử.
Sai lầm thường gặp
- Retry lỗi permanent: thử lại 400 mãi mãi, đơn kẹt, tốn tài nguyên.
- Retry đều nhịp không jitter: mọi worker cùng đập vào service vừa hồi phục, đánh sập lần nữa.
- Đếm attempt trong RAM: restart reset về 0, retry vô hạn.
- Retry mà không kèm idempotency key: mỗi lần thử tạo một khoản charge mới — khách bị trừ nhiều lần.