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.