Queue: worker nhận (claim) job → đang xử lý → process bị kill, OOM, mất mạng trước khi ack. Job có “mất” không?
Hệ at-least-once hay dùng visibility timeout (SQS) / lease / lock TTL: trong lúc job bị ẩn khỏi consumer khác một khoảng T. Hết T mà chưa ack → job hiện lại để worker khác lấy. Đó là redelivery, không phải magic exactly-once.
Demo: Claim → crash → hết lease → redelivery.
Timeline
t=0 W1 claim job, visibility = 30s (job “invisible”)
t=5 W1 đang chạy
t=5 W1 chết (không ack, không extend)
t=30 timeout → job visible lại
t=31 W2 claim → xử lý (lần 2)
t=40 W2 ack → xong
Hệ quả: handler của bạn có thể chạy >1 lần → cần idempotent (và thường inbox nếu có side-effect ngoài).
Ack / complete / delete
- Ack thành công: xóa / đánh dấu done — không redelivery.
- Nack / fail: có thể về queue sớm hơn timeout (tùy hệ), kèm backoff.
- Hết attempt: DLQ.
Lease quá ngắn vs quá dài
- Ngắn quá: job còn chạy, lease hết → worker thứ hai nhận → hai process cùng làm một job (overlap).
- Dài quá: worker chết, job “treo” invisible lâu → latency phục hồi tệ.
Cách phổ biến: lease vừa phải +
extend / heartbeat định kỳ khi còn làm
(ChangeMessageVisibility, lock renew).
const lease = await queue.claim(jobId, { ttlMs: 15_000 });
const hb = setInterval(() => lease.extend(15_000), 5_000);
try {
await process(job);
await lease.ack();
} catch (e) {
await lease.nack(e);
} finally {
clearInterval(hb);
}
Overlap vẫn xảy ra
Network delay: extend chậm, đồng hồ lệch, GC pause dài hơn TTL. Đừng giả định “lease ⇒ mutual exclusion tuyệt đối”. Lớp chặn cuối: CAS / unique claim trong DB khi side-effect quan trọng (giống TOCTOU fixes).
// chỉ một worker được “bắt đầu side-effect”
UPDATE jobs SET status = 'running', worker = $1
WHERE id = $2 AND status = 'pending';
-- rowCount = 0 → ai đó đã cầm, exit
BullMQ / Redis mental model
Active job + stalled detection: worker không renew → job coi như
stalled, đưa lại waiting. Cùng họ lease; cấu hình
lockDuration / stalled interval phải khớp thời gian
xử lý thật (hoặc extend). Xem
visualization BullMQ.
Một câu để nhớ
Visibility timeout / lease = “tạm ẩn để làm, hết hạn thì ai khác làm lại”. Bảo vệ khỏi mất job khi worker chết — đổi lại bằng redelivery và khả năng overlap. Idempotency + (khi cần) claim atomic ở store là phần bắt buộc, không phải optional.