Chương 18. Giao Việc Rồi Xử Lý Sau: Queue

Ở chương trước, ta nói về HTTP/API:

> Một bên gọi trực tiếp một bên khác và chờ kết quả.

HTTP rất tốt khi người gọi cần câu trả lời ngay.

Nhưng trong hệ thống thật, có rất nhiều việc không cần, không nên, hoặc không thể làm xong ngay trong request.

Ví dụ:

  • Chấm bài bằng AI mất 1 phút 30 giây.
  • Gửi email xác nhận.
  • Tạo báo cáo lớn.
  • Resize ảnh.
  • Render video.
  • Đồng bộ dữ liệu sang CRM.
  • Gọi API ngoài dễ chậm.
  • Retry payment webhook.
  • Crawl website.
  • Tính analytics.

Nếu nhét tất cả vào request HTTP, người dùng phải chờ, server bị chiếm tài nguyên, timeout dễ xảy ra, và một lỗi phụ có thể làm hỏng luồng chính.

Queue sinh ra để giải quyết kiểu vấn đề này.

Nói ngắn gọn:

> Queue là hàng đợi công việc. Hệ thống bỏ việc vào hàng đợi, worker lấy ra xử lý sau.

Ví dụ:

API nhận bài nộp
-> tạo grading job
-> bỏ job vào queue
-> trả job_id cho người dùng

Worker
-> lấy job từ queue
-> gọi Gemini API
-> lưu kết quả

Queue không phải phép màu.

Nó không làm một bài chấm 90 giây tự nhiên còn 1 giây.

Nó giúp:

  • Người dùng không phải chờ 90 giây trong request.
  • Hệ thống kiểm soát được có bao nhiêu job chạy cùng lúc.
  • Job lỗi có thể retry.
  • Khi tải tăng, việc được xếp hàng thay vì làm sập server.
  • Các phần xử lý lâu được tách khỏi phần nhận request.

---

18.1. Queue là gì?

Queue là một hàng đợi.

Giống như xếp hàng mua bánh.

Khách đến quầy, lấy số, chờ đến lượt.

Quán không cần phục vụ tất cả cùng một lúc.

Trong hệ thống:

Producer -> Queue -> Worker

Trong đó:

  • Producer là bên tạo công việc.
  • Queue là nơi giữ công việc chờ xử lý.
  • Worker là bên lấy công việc ra và xử lý.

Ví dụ:

API server -> grading_jobs queue -> Celery workers

API server nhận request nộp bài.

Nó không chấm bài ngay.

Nó tạo job và đưa vào queue.

Worker lấy job ra chấm.

---

18.2. Ví dụ quán bánh: phiếu chờ trong bếp

Khách đặt bánh ở quầy.

Nhân viên quầy không tự vào bếp làm bánh ngay.

Nhân viên tạo phiếu:

Phiếu làm bánh #123
- Bánh chocolate
- Giao lúc 17:00
- Ghi chú: ít đường

Phiếu được đưa vào hàng chờ của bếp.

Bếp có 3 thợ.

Mỗi thợ lấy một phiếu và làm.

Nếu hôm nay nhiều đơn, phiếu chờ dài hơn.

Khách không đứng nhìn nhân viên quầy làm bánh.

Nhân viên quầy chỉ cần nói:

Quán đã nhận đơn của bạn. Mã đơn là #123.

Đây chính là queue.

Trong phần mềm:

Frontend -> API: đặt bánh
API -> Database: tạo order
API -> Queue: tạo kitchen job
API -> Frontend: đã nhận đơn

Kitchen Worker -> Queue: lấy job
Kitchen Worker -> Database: cập nhật trạng thái làm bánh

Queue tách quầy nhận đơn khỏi bếp làm bánh.

---

18.3. Queue giải quyết vấn đề gì?

Queue giải quyết năm vấn đề lớn.

Vấn đề 1: Việc lâu

Nếu việc mất lâu, không nên giữ request chính.

Ví dụ:

  • AI grading 90 giây.
  • Export Excel 2 phút.
  • Render video 10 phút.

Queue cho phép trả response nhanh rồi xử lý sau.

Vấn đề 2: Tải tăng đột ngột

Nếu 1.000 request cùng đến, hệ thống có thể không xử lý kịp ngay.

Queue cho phép xếp hàng.

Worker xử lý theo năng lực thật.

Vấn đề 3: Retry

Nếu gửi email lỗi tạm thời, job có thể retry sau.

Không cần bắt user bấm lại.

Vấn đề 4: Cô lập phần dễ lỗi

Gửi email lỗi không nên làm tạo order thất bại.

Analytics lỗi không nên làm payment thất bại.

Queue giúp tách phần chính và phần phụ.

Vấn đề 5: Kiểm soát concurrency

Nếu Gemini API cho phép 100 RPM nhưng hệ thống chỉ có 4 worker, cùng lúc chỉ xử lý được khoảng 4 job nếu mỗi job chiếm một worker.

Queue giúp nhìn thấy backlog và điều chỉnh worker/concurrency phù hợp.

---

18.4. Queue không làm việc biến mất

Đây là hiểu nhầm phổ biến:

> Đưa vào queue là xong.

Không đúng.

Queue chỉ chuyển việc từ "làm ngay" sang "làm sau".

Nếu mỗi job mất 90 giây, 1.000 job vẫn cần tổng rất nhiều thời gian xử lý.

Queue giúp hệ thống không nghẽn request chính, nhưng việc vẫn phải được worker làm.

Ví dụ:

1 job = 90 giây
4 worker chạy song song

Thông lượng tối đa xấp xỉ:

4 job / 90 giây
= 2.67 job / phút

Nếu mỗi phút có 20 bài mới được nộp, backlog sẽ tăng.

Queue không sửa được bài toán thiếu năng lực xử lý.

Nó chỉ làm năng lực xử lý trở nên rõ ràng hơn.

---

18.5. Công thức rất thực tế

Khi dùng queue, luôn nghĩ đến ba con số:

arrival rate: tốc độ job đi vào
service rate: tốc độ worker xử lý
queue length: số job đang chờ

Ví dụ AI Judge:

Mỗi bài chấm mất 90 giây.
Có 10 worker chạy song song.

Thông lượng tối đa:

10 job / 90 giây
= 6.67 job / phút

Nếu hệ thống nhận:

5 bài / phút

queue ổn, vì worker xử lý nhanh hơn tốc độ job đến.

Nếu nhận:

20 bài / phút

queue sẽ dài dần.

Không có kiến trúc nào thoát khỏi phép tính này.

Muốn xử lý nhiều hơn, phải:

  • Tăng worker/concurrency.
  • Giảm thời gian mỗi job.
  • Chia job theo loại.
  • Tăng quota provider nếu bị giới hạn bên ngoài.
  • Giới hạn tốc độ nhận job.
  • Ưu tiên job quan trọng.

---

18.6. Queue và latency

Khi dùng queue, latency của job gồm:

Thời gian chờ trong queue + thời gian xử lý

Ví dụ:

Job chờ queue: 3 phút
Job xử lý: 1 phút 30 giây
Tổng latency: 4 phút 30 giây

Nhiều người chỉ nhìn thời gian xử lý.

Nhưng với người dùng, thời gian chờ mới là thứ họ cảm nhận.

Nếu học viên nộp bài và 10 phút sau mới có điểm, nguyên nhân có thể không phải Gemini chậm.

Có thể job chờ queue quá lâu.

Vì vậy cần đo:

  • Job được tạo lúc nào.
  • Job bắt đầu chạy lúc nào.
  • Job chạy xong lúc nào.

Từ đó biết:

queue_wait_time
processing_time
total_time

Không đo ba thứ này, rất khó tối ưu.

---

18.7. Queue phù hợp khi nào?

Queue phù hợp khi:

  • Việc xử lý lâu.
  • Không cần trả kết quả cuối ngay.
  • Cần retry.
  • Có thể xử lý sau.
  • Muốn kiểm soát concurrency.
  • Muốn chống tải tăng đột ngột.
  • Muốn tách phần chính khỏi phần phụ.
  • Muốn chạy job nền theo lịch.

Ví dụ:

SendEmailJob
RunGradingJob
GenerateReport
ResizeImage
SyncToCrm
ProcessPaymentWebhook
RebuildSearchIndex
CalculateRecommendation
ExportOrdersCsv

Queue đặc biệt tốt cho việc I/O-bound lâu:

  • Gọi AI API.
  • Gửi email.
  • Gọi payment provider.
  • Gọi external API.
  • Upload file.

Vì worker có thể chờ I/O trong nền, không giữ request người dùng.

---

18.8. Queue không phù hợp khi nào?

Queue không phù hợp nếu:

  • Người dùng cần kết quả ngay.
  • Việc rất nhỏ và đơn giản.
  • Thứ tự xử lý cực kỳ chặt nhưng queue không đảm bảo.
  • Hệ thống chưa có cách theo dõi job.
  • Không có retry/idempotency.
  • Dùng queue để che giấu logic rối.

Ví dụ:

User đăng nhập.

Không thể trả:

Đã nhận yêu cầu đăng nhập, vui lòng chờ worker xử lý.

Login cần kết quả ngay.

Ví dụ:

Frontend cần lấy tên sản phẩm để hiển thị.

Không dùng queue.

Dùng HTTP GET hoặc cache.

Queue dùng cho "làm việc", không dùng cho "hỏi dữ liệu để hiển thị ngay".

---

18.9. Queue khác HTTP như thế nào?

HTTP:

A gọi B
A chờ B trả lời

Queue:

A bỏ việc vào queue
Worker xử lý sau
A không chờ kết quả cuối

So sánh:

| Câu hỏi | HTTP/API | Queue | |---|---|---| | Có chờ kết quả ngay không? | Thường có | Thường không | | Phù hợp việc lâu không? | Không lý tưởng | Rất phù hợp | | Có backlog rõ không? | Không rõ | Có | | Retry tự nhiên không? | Cần tự thiết kế | Dễ hơn nhưng vẫn phải cẩn thận | | Người dùng nhận gì? | Kết quả hoặc lỗi | Job id/trạng thái đã nhận | | Lỗi xử lý thế nào? | Trả lỗi ngay | Retry, fail, DLQ | | Kiểm soát concurrency | Khó hơn | Rõ hơn |

Không phải chọn một trong hai.

Thường hệ thống dùng cả hai.

Ví dụ:

HTTP để nhận request
Queue để xử lý việc lâu
HTTP để xem trạng thái job

---

18.10. Queue khác Pub/Sub như thế nào?

Queue thường dùng để chia công việc cho worker.

Một job thường chỉ cần một worker xử lý.

Ví dụ:

RunGradingJob(job_123)

Chỉ một worker nên chấm job này.

Pub/Sub dùng để phát sự kiện cho nhiều bên nghe.

Ví dụ:

OrderPlaced

Nhiều bên có thể nghe:

  • Kitchen.
  • Notification.
  • Analytics.
  • Inventory.

Nói ngắn:

Queue: một việc, một worker xử lý.
Pub/Sub: một sự kiện, nhiều consumer có thể phản ứng.

Một số công cụ có thể làm cả hai, nhưng ý nghĩa thiết kế khác nhau.

Chương sau sẽ nói kỹ hơn về Pub/Sub.

---

18.11. Job là gì?

Job là một đơn vị công việc được đưa vào queue.

Ví dụ:

RunGradingJob
- grading_job_id: job_123

Hoặc:

SendEmail
- email_id: email_456

Job tốt nên:

  • Có id rõ.
  • Có loại job.
  • Có payload vừa đủ.
  • Có trạng thái trong database nếu cần theo dõi.
  • Có retry policy.
  • Có timeout.
  • Có idempotency.

Không nên nhét quá nhiều dữ liệu vào job payload nếu dữ liệu đó có thể thay đổi hoặc quá lớn.

Ví dụ AI Judge:

Không nên đưa toàn bộ nội dung bài, rubric, prompt, config vào message queue nếu quá lớn.

Có thể chỉ đưa:

grading_job_id

Worker load dữ liệu từ database.

Nhưng nếu cần snapshot tại thời điểm tạo job, có thể lưu snapshot trong database của job.

---

18.12. Job status

Nếu người dùng cần biết tiến độ, job nên có trạng thái.

Ví dụ:

PENDING
RUNNING
SUCCEEDED
FAILED
CANCELLED
RETRYING

AI Judge:

GradingJob
- id
- submission_id
- status
- attempt_count
- max_attempts
- started_at
- completed_at
- failed_reason
- score
- feedback

Luồng:

PENDING -> RUNNING -> SUCCEEDED
PENDING -> RUNNING -> FAILED -> RETRYING -> RUNNING
PENDING -> CANCELLED

Frontend có thể gọi:

GET /grading-jobs/{id}

để xem:

  • Đang chờ.
  • Đang chấm.
  • Chấm xong.
  • Lỗi.

Nếu không có job status, người dùng chỉ thấy "đang quay" mà không biết chuyện gì xảy ra.

---

18.13. Worker là gì?

Worker là process/thread/service lấy job từ queue và xử lý.

Ví dụ:

Celery worker
Sidekiq worker
BullMQ worker
RQ worker
Laravel queue worker
Hangfire worker
SQS consumer

Worker khác web server ở chỗ:

  • Web server xử lý request người dùng.
  • Worker xử lý việc nền.

Tách hai loại này giúp:

  • Web request nhanh hơn.
  • Job lâu không chiếm web worker.
  • Có thể scale worker riêng.
  • Có thể tạo worker pool riêng cho từng loại job.

Ví dụ:

web: xử lý API
worker-ai: chấm bài
worker-email: gửi email
worker-report: tạo báo cáo

Không nên để mọi loại job chen nhau trong một hàng nếu độ quan trọng và thời gian xử lý rất khác nhau.

---

18.14. Concurrency của worker

Concurrency là số việc có thể xử lý cùng lúc.

Nếu một worker process có concurrency 4, nó có thể xử lý 4 job cùng lúc, tùy loại worker.

Ví dụ:

Celery workers = 4
Mỗi job AI chờ Gemini 90 giây

Nếu mỗi worker chỉ xử lý một job tại một thời điểm, cùng lúc chỉ có 4 job chạy.

Thông lượng:

4 job / 90 giây
= 2.67 job / phút

Nếu Gemini API cho phép 100 request/phút, nhưng phía ta chỉ chạy 4 job song song, ta đang dùng rất ít năng lực bên ngoài.

Có thể tăng:

  • Số worker process.
  • Concurrency mỗi worker.
  • Dùng async/gevent/eventlet cho I/O-bound.
  • Tách queue riêng cho AI.

Nhưng tăng concurrency phải đo:

  • CPU.
  • RAM.
  • DB connection.
  • External API rate limit.
  • Timeout.
  • Error rate.
  • Cost.

Tăng worker không kiểm soát có thể làm sập database hoặc vượt quota.

---

18.15. Queue giúp kiểm soát concurrency như thế nào?

Không có queue, nếu 1.000 request cùng gọi Gemini trực tiếp, bạn có thể tạo 1.000 call gần như cùng lúc.

Điều này dễ gây:

  • Timeout.
  • Vượt quota.
  • Tốn cost đột biến.
  • Server quá tải.

Có queue, bạn có thể nói:

Tối đa 50 job AI chạy cùng lúc.
Phần còn lại chờ.

Đây là điểm rất quan trọng:

> Queue biến tải đột biến thành hàng chờ có kiểm soát.

Không có queue:

Tải đột biến -> tất cả cùng chạy -> dễ sập

Có queue:

Tải đột biến -> xếp hàng -> worker xử lý theo năng lực

Đổi lại, người dùng có thể phải chờ kết quả lâu hơn.

Đó là trade-off.

---

18.16. Backlog là gì?

Backlog là số job đang chờ xử lý.

Ví dụ:

Queue có 5.000 grading jobs đang chờ.

Backlog không phải lúc nào cũng xấu.

Nếu hệ thống đang xử lý ổn và backlog giảm dần, không sao.

Nhưng backlog nguy hiểm khi:

  • Tăng liên tục.
  • Không giảm sau khi hết peak.
  • Job chờ quá lâu.
  • Người dùng bắt đầu phàn nàn.
  • Retry làm backlog tăng thêm.
  • Worker đang lỗi nhưng queue vẫn nhận job.

Cần theo dõi:

  • Queue length.
  • Oldest job age.
  • Job wait time.
  • Processing rate.
  • Failure rate.
  • Retry count.

Trong nhiều hệ thống, oldest job age còn quan trọng hơn queue length.

Vì 10.000 job nhỏ có thể xử lý nhanh, nhưng 100 job chờ 2 giờ là vấn đề lớn.

---

18.17. Queue dài bao nhiêu là nguy hiểm?

Không có con số chung.

Phải nhìn theo SLA hoặc kỳ vọng người dùng.

Ví dụ AI Judge:

Nếu kỳ vọng:

95% bài được chấm trong 3 phút

thì queue nguy hiểm khi nhiều job chờ quá 1-2 phút trước khi bắt đầu xử lý, vì bản thân chấm đã mất 90 giây.

Ví dụ email marketing:

Email gửi chậm 10 phút có thể vẫn ổn.

Ví dụ reset password email:

Chậm 10 phút là rất tệ.

Cùng là email, nhưng mức khẩn cấp khác nhau.

Vì vậy nên chia queue theo loại việc:

critical
default
low_priority
ai_grading
email_transactional
email_bulk

Không để email marketing 1 triệu cái làm chậm email reset password.

---

18.18. Priority queue

Priority queue cho phép job quan trọng được xử lý trước.

Ví dụ:

High priority:
- reset password email
- payment confirmation
- grading job của bài thi đang diễn ra

Low priority:
- analytics
- bulk email
- recommendation rebuild

Nhưng priority cũng có rủi ro.

Nếu job high priority quá nhiều, job low priority có thể bị đói.

Cần theo dõi:

  • Low priority có bị chờ quá lâu không.
  • Có cần worker riêng không.
  • Có cần giới hạn high priority không.

Nhiều khi tách queue riêng dễ hiểu hơn priority phức tạp.

---

18.19. Retry trong queue

Job lỗi có thể retry.

Ví dụ:

Gửi email lỗi do provider timeout
-> retry sau 30 giây
-> retry sau 2 phút
-> retry sau 10 phút

Retry nên có:

  • Số lần tối đa.
  • Backoff.
  • Jitter.
  • Lưu lỗi cuối.
  • Không retry lỗi chắc chắn không sửa được.
  • Idempotency.

Ví dụ không nên retry:

  • Email address sai format.
  • User không có quyền.
  • Payload thiếu field bắt buộc.

Ví dụ có thể retry:

  • Network timeout.
  • External API 503.
  • Rate limit 429 theo Retry-After.
  • Database deadlock tạm thời.

Retry không phải "thử mãi đến khi được".

Retry là chiến lược xử lý lỗi tạm thời.

---

18.20. Retry storm

Retry storm xảy ra khi nhiều job cùng lỗi và cùng retry, làm hệ thống quá tải thêm.

Ví dụ:

Gemini API chậm.

1.000 grading job timeout.

Tất cả retry sau 10 giây.

Sau 10 giây, 1.000 job lại gọi Gemini cùng lúc.

Provider càng chậm hơn.

Hệ thống càng lỗi hơn.

Cách giảm rủi ro:

  • Exponential backoff.
  • Jitter để retry không cùng lúc.
  • Rate limit worker.
  • Circuit breaker với provider.
  • Tạm pause queue khi provider lỗi nặng.
  • DLQ sau số lần retry tối đa.

Retry sai có thể nguy hiểm hơn không retry.

---

18.21. Dead Letter Queue

Dead Letter Queue, gọi tắt là DLQ, là nơi chứa job đã retry quá số lần nhưng vẫn thất bại.

Ví dụ:

RunGradingJob retry 5 lần
vẫn lỗi do prompt quá dài
-> đưa vào DLQ

DLQ giúp:

  • Không để job lỗi retry vô hạn.
  • Không chặn queue chính.
  • Có nơi để debug.
  • Có thể sửa dữ liệu rồi replay.
  • Có thể cảnh báo vận hành.

Job vào DLQ không nên bị quên.

Cần có:

  • Dashboard.
  • Alert.
  • Công cụ xem lỗi.
  • Quy trình replay hoặc mark failed.

DLQ là thùng thư lỗi, không phải thùng rác.

---

18.22. Idempotency trong queue

Queue thường có cơ chế "at least once".

Nghĩa là:

> Một job có thể được xử lý ít nhất một lần, và đôi khi hơn một lần.

Vì sao?

Worker có thể:

  • Xử lý xong nhưng chết trước khi ack.
  • Timeout.
  • Mất connection với broker.
  • Retry sau lỗi không rõ.

Vì vậy handler phải idempotent.

Ví dụ RunGradingJob(job_123):

Nếu job đã SUCCEEDED, worker retry không được chấm lại và ghi kết quả khác.

Ví dụ SendEmail(email_123):

Nếu email đã gửi, retry có thể bỏ qua hoặc dùng provider idempotency nếu có.

Ví dụ RefundPayment(refund_123):

Retry không được hoàn tiền hai lần.

Idempotency là điều kiện bắt buộc của job quan trọng.

---

18.23. Ack là gì?

Ack là tín hiệu worker nói với queue:

> Tôi đã xử lý xong job này, có thể xóa khỏi queue.

Nếu worker ack quá sớm:

ack job
đang xử lý
worker chết

Job có thể bị mất.

Nếu worker ack sau khi xử lý:

xử lý xong
ack job

An toàn hơn, nhưng nếu worker xử lý xong rồi chết trước ack, job có thể chạy lại.

Đó là lý do cần idempotency.

Thông thường với job quan trọng:

> Ack sau khi xử lý thành công.

Nhưng phải chấp nhận có thể xử lý trùng trong một số tình huống.

---

18.24. Visibility timeout

Một số queue như SQS dùng khái niệm visibility timeout.

Nói dễ hiểu:

> Khi worker lấy job, job tạm thời bị ẩn khỏi worker khác. Nếu worker không ack trong thời gian quy định, job hiện lại để worker khác xử lý.

Ví dụ:

visibility timeout = 2 phút
job xử lý mất 5 phút

Nếu không gia hạn, queue nghĩ worker chết và đưa job cho worker khác.

Kết quả:

Hai worker có thể xử lý cùng một job.

Vì vậy timeout của queue phải phù hợp với thời gian xử lý job.

Với job dài:

  • Tăng visibility timeout.
  • Gia hạn trong lúc chạy.
  • Chia job thành bước nhỏ hơn.
  • Thiết kế idempotency.

---

18.25. Job timeout

Mỗi job nên có timeout.

Nếu không, worker có thể bị kẹt mãi.

Ví dụ:

RunGradingJob timeout = 180 giây

Nếu Gemini API treo, job không nên giữ worker vô hạn.

Timeout cần đặt theo loại job:

  • Email: vài giây đến vài chục giây.
  • AI grading: dài hơn.
  • Report lớn: có thể vài phút.
  • Video processing: có thể rất dài, nhưng nên có heartbeat/progress.

Nếu job thường xuyên chạm timeout, đừng chỉ tăng timeout.

Hãy hỏi:

  • Job có quá lớn không?
  • Có thể chia nhỏ không?
  • External API có chậm không?
  • Worker concurrency có phù hợp không?
  • Có bottleneck database không?

---

18.26. Long-running job

Job dài cần thiết kế kỹ hơn job ngắn.

Ví dụ render video 30 phút.

Nếu worker chết ở phút 29, làm lại từ đầu rất phí.

Cách xử lý:

  • Chia job thành nhiều bước.
  • Lưu progress.
  • Có checkpoint.
  • Có heartbeat.
  • Có thể resume.
  • Có timeout hợp lý.

AI grading 90 giây thường chưa quá dài, nhưng vẫn cần:

  • Trạng thái RUNNING.
  • Timeout.
  • Retry.
  • Lưu attempt.
  • Không chạy trùng.

Long-running job không chỉ là "set timeout dài".

Nó cần khả năng quan sát và phục hồi.

---

18.27. Job payload nên lớn hay nhỏ?

Có hai kiểu.

Payload nhỏ

{
  "grading_job_id": "job_123"
}

Worker lấy id rồi query database.

Ưu điểm:

  • Message nhỏ.
  • Dữ liệu mới nhất.
  • Queue nhẹ.

Nhược điểm:

  • Phụ thuộc database.
  • Nếu dữ liệu đã đổi, phải biết muốn dùng dữ liệu mới hay snapshot cũ.

Payload lớn

{
  "submission_content": "...",
  "rubric": "...",
  "model": "gemini..."
}

Ưu điểm:

  • Worker ít query hơn.
  • Có snapshot tại thời điểm tạo job.

Nhược điểm:

  • Message lớn.
  • Khó version.
  • Dữ liệu nhạy cảm đi qua broker.
  • Broker có giới hạn size.

Quy tắc thực dụng:

> Payload nên đủ để worker biết job nào cần xử lý, còn dữ liệu lớn hoặc nhạy cảm nên nằm ở storage/database có kiểm soát.

Nếu cần snapshot, lưu snapshot vào bảng job hoặc object storage, rồi message chỉ chứa id.

---

18.28. Queue và database

Một lỗi phổ biến là:

save database
enqueue job

Nếu database lưu thành công nhưng enqueue thất bại thì sao?

Ví dụ:

Tạo submission thành công
Nhưng enqueue grading job thất bại

Học viên đã nộp bài nhưng không bao giờ được chấm.

Cách tốt hơn là dùng outbox hoặc lưu job trong database cùng transaction:

begin transaction
  save submission
  save grading_job = PENDING
  save outbox/job_to_enqueue
commit

background dispatcher enqueue job

Hoặc worker đọc trực tiếp bảng job pending.

Điểm quan trọng:

> Thay đổi nghiệp vụ và việc tạo job phải nhất quán với nhau.

Nếu không, sẽ có dữ liệu "mồ côi": có trạng thái cần xử lý nhưng không có job chạy.

---

18.29. Transactional outbox với queue

Outbox không chỉ dùng cho event.

Nó cũng giúp enqueue job đáng tin hơn.

Luồng:

begin transaction
  create order
  insert outbox message: CreateKitchenTicket
commit

dispatcher
  read outbox
  publish message to queue
  mark published

Nếu server chết sau commit, outbox message vẫn còn.

Dispatcher chạy lại và publish.

Nếu publish thành công nhưng mark published thất bại, message có thể publish lại.

Vì vậy consumer vẫn phải idempotent.

Outbox giúp không mất message.

Nó không loại bỏ nhu cầu idempotency.

---

18.30. Queue và polling trạng thái

Khi job chạy nền, client cần biết kết quả.

Cách đơn giản nhất:

POST /submissions
-> trả grading_job_id

GET /grading-jobs/{id}
-> trả status/result

Frontend có thể polling mỗi vài giây.

Ví dụ:

PENDING
RUNNING
SUCCEEDED
FAILED

Polling đơn giản và dễ triển khai.

Nhược điểm:

  • Tạo request lặp lại.
  • Kết quả không realtime tuyệt đối.

Nếu cần trải nghiệm tốt hơn, có thể dùng:

  • SSE.
  • WebSocket.
  • Push notification.
  • Email.

Nhưng bắt đầu với polling thường đủ tốt.

Đừng làm WebSocket chỉ vì job chạy nền nếu polling 3-5 giây đã ổn.

---

18.31. Queue và progress

Một số job có thể báo progress.

Ví dụ export report:

10% đọc dữ liệu
50% tạo file
90% upload file
100% xong

AI grading thường khó báo progress thật nếu phần lớn thời gian nằm trong một API call.

Có thể chỉ báo:

PENDING
RUNNING
SUCCEEDED
FAILED

Đừng giả progress:

87%

nếu hệ thống không thật sự biết.

Progress tốt phải phản ánh trạng thái thật, không chỉ làm UI có vẻ sống động.

---

18.32. Queue và rate limit

Queue giúp kiểm soát tốc độ gọi external API.

Ví dụ Gemini API cho phép 100 requests/phút.

Bạn không nên để worker gọi vượt mức đó.

Cần có:

  • Worker concurrency phù hợp.
  • Rate limiter.
  • Backoff khi gặp 429.
  • Theo dõi request/minute.
  • Tách queue theo provider/model nếu cần.

Ví dụ:

worker-ai concurrency = 50
rate limit = 90 requests/minute

Đừng dùng hết 100/100 nếu muốn có vùng an toàn.

Và nhớ:

> Concurrency không giống RPM.

Nếu mỗi request mất 90 giây, để đạt 100 requests/phút, số request đang chạy song song có thể rất lớn.

Ước tính:

concurrency cần thiết ≈ throughput * latency

Nếu muốn 100 request/phút:

100 / 60 = 1.67 request/giây
latency = 90 giây
concurrency ≈ 1.67 * 90 = 150

Tức là muốn dùng đủ 100 RPM với mỗi job 90 giây, bạn có thể cần khoảng 150 request đang bay song song, nếu không có bottleneck khác.

Đây là lý do 4 worker là quá ít cho workload I/O chờ lâu.

---

18.33. Queue và backpressure

Backpressure là cách hệ thống nói:

> Tôi đang quá tải, đừng nhận thêm hoặc hãy nhận chậm lại.

Nếu queue dài quá nhanh, API vẫn nhận vô hạn job mới thì người dùng sẽ chờ rất lâu.

Có thể cần:

  • Giới hạn số job pending/user.
  • Giới hạn số job pending/toàn hệ thống.
  • Trả 429 hoặc 503 khi quá tải.
  • Hiển thị "hệ thống đang bận".
  • Ưu tiên user trả phí.
  • Tăng worker tạm thời.

Queue không nên là hố không đáy.

Nếu cứ nhận job mà không xử lý kịp, hệ thống chỉ đang trì hoãn vấn đề.

Backpressure giúp hệ thống thất bại có kiểm soát thay vì hứa bừa.

---

18.34. Queue overload

Queue overload xảy ra khi job đến nhanh hơn khả năng xử lý trong thời gian dài.

Dấu hiệu:

  • Queue length tăng liên tục.
  • Oldest job age tăng.
  • Worker CPU/RAM cao hoặc external API chậm.
  • Retry tăng.
  • Job timeout tăng.
  • Người dùng thấy kết quả chậm.

Cách xử lý:

  • Tăng worker nếu còn tài nguyên.
  • Tối ưu thời gian xử lý job.
  • Tách queue theo loại job.
  • Giảm retry hoặc backoff tốt hơn.
  • Rate limit producer.
  • Tạm dừng job low priority.
  • Scale external dependency nếu có thể.
  • Thông báo trạng thái cho người dùng.

Đừng chỉ restart worker theo thói quen.

Phải biết bottleneck là gì.

---

18.35. Queue và autoscaling

Queue rất hợp với autoscaling worker.

Ví dụ:

Nếu queue length > 1000
-> tăng số worker

Nếu queue length < 100
-> giảm số worker

Hoặc tốt hơn:

scale theo oldest job age hoặc queue wait time

Vì queue length không nói hết.

Autoscaling cần giới hạn:

  • Không scale quá quota external API.
  • Không mở quá nhiều DB connection.
  • Không vượt budget.
  • Không làm hệ thống khác quá tải.

Scale worker không phải lúc nào cũng tốt.

Nếu bottleneck là database, tăng worker có thể làm database chết nhanh hơn.

---

18.36. Tách queue theo loại job

Một hàng đợi duy nhất cho mọi thứ thường đơn giản lúc đầu.

Nhưng khi hệ thống lớn, nên tách.

Ví dụ:

queue: critical
queue: default
queue: ai_grading
queue: email_transactional
queue: email_bulk
queue: reports
queue: analytics

Lý do:

  • Job lâu không chặn job ngắn.
  • Job quan trọng không bị job phụ làm chậm.
  • Mỗi loại có concurrency riêng.
  • Mỗi loại có retry/timeout riêng.
  • Dễ theo dõi bottleneck.

Ví dụ:

Không nên để:

AI grading 90 giây
email reset password 1 giây

cùng tranh 4 worker.

Nếu queue chung, 100 job AI có thể làm email reset password chờ rất lâu.

---

18.37. Queue và fairness

Fairness là công bằng giữa user/tenant/nhóm job.

Ví dụ một lớp học nộp 10.000 bài cùng lúc.

Nếu queue xử lý theo FIFO tuyệt đối, các lớp khác có thể phải chờ rất lâu.

Cách xử lý:

  • Giới hạn số job pending mỗi user/lớp.
  • Chia queue theo tenant nếu cần.
  • Round-robin giữa nhóm.
  • Priority cho job quan trọng.
  • Rate limit producer.

Không phải hệ thống nào cũng cần fairness phức tạp.

Nhưng nếu bạn xây SaaS nhiều khách hàng, một khách hàng lớn không nên làm khách hàng khác nghẽn hoàn toàn.

---

18.38. Queue và scheduled job

Queue cũng dùng cho job chạy sau hoặc chạy định kỳ.

Ví dụ:

  • Gửi email nhắc học sau 1 giờ.
  • Hủy seat hold sau 10 phút.
  • Retry payment sau 5 phút.
  • Tạo báo cáo mỗi đêm.
  • Dọn file tạm hàng ngày.

Có hai kiểu:

Delayed job: chạy sau một khoảng thời gian
Scheduled job: chạy theo lịch

Ví dụ:

SeatReserved
-> enqueue ReleaseSeatHold sau 10 phút

Nếu khách không thanh toán, job release ghế.

Job theo lịch cũng cần idempotency.

Nếu cron chạy hai lần, không được tạo báo cáo trùng hoặc gửi email trùng nếu không muốn.

---

18.39. Queue và cron khác nhau thế nào?

Cron kích hoạt công việc theo thời gian.

Queue giữ công việc chờ worker xử lý.

Ví dụ:

Cron mỗi đêm 1h
-> enqueue GenerateDailyReport

Cron không nên tự làm toàn bộ job nặng nếu việc có thể dài hoặc cần retry.

Cách tốt:

Scheduler/Cron tạo job
Queue giữ job
Worker xử lý job

Như vậy vẫn có:

  • Retry.
  • Timeout.
  • Monitoring.
  • DLQ.
  • Concurrency control.

---

18.40. Queue và duplicate job

Duplicate job là job trùng.

Ví dụ user bấm nộp bài hai lần, hệ thống tạo hai grading job cho cùng một submission dù chính sách chỉ muốn một.

Cách chống:

  • Unique constraint.
  • Idempotency key.
  • Kiểm tra trạng thái trước khi enqueue.
  • Job key duy nhất.
  • Lock theo aggregate id.

Ví dụ:

unique(submission_id, grading_policy_version)

Nếu đã có grading job cho submission này, không tạo thêm.

Với queue, phải luôn giả định:

> Job có thể trùng, message có thể lặp, worker có thể retry.

Thiết kế theo giả định đó thì hệ thống bền hơn.

---

18.41. Queue và thứ tự xử lý

Nhiều người nghĩ queue luôn xử lý đúng thứ tự.

Không hẳn.

Nếu có nhiều worker, job có thể hoàn thành khác thứ tự lấy ra.

Ví dụ:

Job A lấy trước nhưng chạy 10 giây
Job B lấy sau nhưng chạy 1 giây
Job B xong trước

Nếu thứ tự quan trọng, cần thiết kế riêng:

  • Queue theo key.
  • Một worker cho một aggregate/key.
  • Sequence number.
  • Kiểm tra version.
  • Chờ event trước nếu cần.

Ví dụ:

Các thao tác trên cùng một wallet cần cẩn thận thứ tự.

Các job analytics thì thường không cần thứ tự chặt.

Đừng dựa vào "queue tự nhiên sẽ đúng thứ tự" nếu nghiệp vụ không cho phép sai.

---

18.42. Queue và lock theo key

Một số job không được chạy song song cho cùng một đối tượng.

Ví dụ:

  • Hai job chấm cùng một grading_job_id.
  • Hai job refund cùng một payment.
  • Hai job xử lý cùng một wallet.
  • Hai job rebuild cùng một report.

Cần lock theo key:

lock: grading_job:123
lock: payment:456
lock: wallet:789

Hoặc dùng database constraint/state machine để đảm bảo.

Ví dụ:

update grading_jobs
set status = 'RUNNING'
where id = 'job_123' and status = 'PENDING'

Nếu update được 1 row, worker thắng.

Nếu update được 0 row, job đã bị worker khác lấy hoặc không còn pending.

Đây là cách đơn giản và rất thực tế để tránh chạy trùng.

---

18.43. Queue và observability

Queue không có observability thì rất nguy hiểm.

Cần theo dõi:

  • Queue length.
  • Oldest job age.
  • Enqueue rate.
  • Processing rate.
  • Success rate.
  • Failure rate.
  • Retry count.
  • DLQ count.
  • Worker concurrency.
  • Worker CPU/RAM.
  • Job duration.
  • Queue wait time.

Cần log:

  • Job id.
  • Job type.
  • Attempt number.
  • Started at.
  • Finished at.
  • Error.
  • Correlation id.

Ví dụ debug AI Judge:

submission_id = sub_123
grading_job_id = job_456
enqueue_at = 10:00
started_at = 10:03
completed_at = 10:04:30
attempt = 1

Nhìn vào biết ngay:

  • Chờ queue 3 phút.
  • Xử lý 90 giây.
  • Tổng 4 phút 30 giây.

---

18.44. Queue và graceful shutdown

Worker cần tắt đúng cách.

Nếu deploy mới mà kill worker đột ngột:

  • Job đang chạy có thể bị mất.
  • Job có thể chạy lại.
  • External API call có thể bị ngắt.
  • Trạng thái job có thể kẹt RUNNING.

Graceful shutdown nghĩa là:

  • Ngừng nhận job mới.
  • Cho job đang chạy hoàn thành trong giới hạn.
  • Ack/nack rõ.
  • Nếu quá thời gian thì dừng an toàn.
  • Job dang dở có thể retry.

Với job dài, cần xử lý trạng thái RUNNING bị kẹt:

Nếu job RUNNING quá 30 phút không heartbeat
-> mark as stale
-> retry hoặc fail

Đây là chi tiết vận hành rất thực tế.

---

18.45. Queue và deployment

Khi deploy code mới, có thể còn job cũ trong queue.

Vấn đề:

  • Payload cũ không khớp code mới.
  • Job type cũ bị xóa.
  • Field mới chưa có trong message cũ.
  • Worker mới không đọc được job cũ.

Cần:

  • Version job payload nếu thay đổi lớn.
  • Backward compatibility.
  • Không xóa handler cũ quá sớm.
  • Drain queue nếu cần.
  • Deploy worker và producer theo thứ tự hợp lý.

Queue làm hệ thống bất đồng bộ, nghĩa là message cũ có thể sống lâu hơn bạn nghĩ.

Đổi schema job phải cẩn thận như đổi API.

---

18.46. Queue và dữ liệu nhạy cảm

Không nên nhét dữ liệu nhạy cảm vào message nếu không cần.

Ví dụ:

  • Token.
  • Mật khẩu.
  • Secret.
  • Thông tin thẻ.
  • Nội dung riêng tư lớn.

Message queue có thể được log, retry, inspect, lưu lại.

Nếu cần xử lý dữ liệu nhạy cảm:

  • Lưu trong database/storage có kiểm soát.
  • Message chỉ chứa id.
  • Mã hóa nếu cần.
  • Giới hạn quyền đọc queue.
  • Đặt retention hợp lý.

Đừng xem queue là nơi tạm vô hại.

Nhiều dữ liệu "tạm" lại nằm trong hệ thống rất lâu.

---

18.47. Công cụ queue phổ biến

Một số công cụ phổ biến:

RabbitMQ
Redis Queue / RQ / BullMQ
Celery với Redis/RabbitMQ
Sidekiq với Redis
AWS SQS
Google Cloud Tasks
Google Pub/Sub
Kafka
Azure Service Bus
Hangfire
Laravel Queue

Không cần học tất cả ngay.

Nên hiểu các tiêu chí chọn:

  • Có cần delay job không?
  • Có cần retry/DLQ không?
  • Có cần ordering không?
  • Có cần throughput rất cao không?
  • Có cần managed service không?
  • Team đang dùng ngôn ngữ/framework nào?
  • Vận hành tự quản hay dùng cloud?

Ví dụ:

  • Django/Python thường gặp Celery.
  • Ruby thường gặp Sidekiq.
  • Node.js thường gặp BullMQ.
  • Cloud system có thể dùng SQS/Pub/Sub/Cloud Tasks.
  • Streaming lớn có thể dùng Kafka, nhưng Kafka không phải queue đơn giản cho mọi thứ.

---

18.48. Celery trong bức tranh này

Celery là một task queue phổ biến trong Python.

Nó gồm:

Producer: code enqueue task
Broker: Redis/RabbitMQ
Worker: celery worker
Result backend: nơi lưu kết quả nếu dùng

Celery phù hợp cho:

  • Job nền.
  • Retry.
  • Scheduled task.
  • Tách worker theo queue.
  • Python/Django/FastAPI ecosystem.

Nhưng Celery vẫn cần thiết kế đúng:

  • Task phải idempotent.
  • Timeout rõ.
  • Retry policy rõ.
  • Queue riêng cho job lâu.
  • Monitoring bằng Flower/metrics/log.
  • Concurrency phù hợp workload.
  • Không dùng result backend bừa nếu không cần.

Celery không tự sửa kiến trúc sai.

Nó chỉ là công cụ chạy job.

---

18.49. Celery concurrency không phải lúc nào cũng là thread

Celery có nhiều pool:

  • prefork: nhiều process.
  • threads: nhiều thread.
  • gevent/eventlet: cooperative concurrency cho I/O.
  • solo: một worker đơn.

Mặc định thường là prefork.

Với prefork:

concurrency = 4

thường nghĩa là 4 process con xử lý 4 task cùng lúc.

Với thread pool, concurrency là số thread.

Với gevent/eventlet, concurrency là số greenlet, phù hợp hơn với I/O nếu thư viện tương thích.

Điểm chính:

> Concurrency của Celery là số task có thể chạy cùng lúc trong worker pool, nhưng nó là process, thread hay greenlet tùy pool.

Với job gọi AI API và chờ mạng, workload là I/O-bound.

Có thể cần tăng concurrency hoặc dùng kiểu worker phù hợp.

Nhưng phải test thật, vì thư viện HTTP, DNS, database driver có tương thích async/gevent hay không cũng quan trọng.

---

18.50. AI Judge: thiết kế queue thực dụng

Một thiết kế thực dụng:

POST /submissions
-> SubmitAssignmentUseCase
   - lưu Submission
   - tạo GradingJob = PENDING
   - ghi outbox/enqueue RunGradingJob(job_id)
-> trả 202 Accepted + job_id

Celery worker-ai
-> RunGradingJobUseCase(job_id)
   - chuyển job PENDING -> RUNNING
   - gọi Gemini API
   - lưu score/feedback
   - chuyển job -> SUCCEEDED
   - phát GradingCompleted

GET /grading-jobs/{id}
-> trả trạng thái/kết quả

Những điểm phải có:

  • GradingJob trong database.
  • Status rõ.
  • Attempt count.
  • Timeout.
  • Retry policy.
  • Idempotency.
  • Không chạy trùng cùng job.
  • Queue riêng cho AI.
  • Concurrency phù hợp với Gemini quota và tài nguyên.
  • Metrics queue wait time và processing time.

Nếu mỗi bài mất 90 giây, 4 worker là rất ít nếu mục tiêu xử lý nhiều bài.

Nhưng tăng lên bao nhiêu phải dựa trên:

  • Gemini RPM.
  • Latency thật.
  • CPU/RAM.
  • DB connection.
  • Cost.
  • Số bài đến mỗi phút.
  • Kỳ vọng thời gian trả kết quả.

---

18.51. Một phép tính AI Judge

Giả sử:

Gemini limit: 100 requests/phút
Mỗi bài: 90 giây

Nếu muốn tận dụng gần 100 requests/phút, concurrency cần khoảng:

100 request/phút = 1.67 request/giây
1.67 * 90 giây ≈ 150 request đồng thời

Tức là cần khoảng 150 job AI đang chờ response cùng lúc.

Nếu Celery chỉ có 4 slot chạy:

4 / 90 giây = 2.67 bài/phút

Ta không chạm giới hạn Gemini.

Ta nghẽn ở worker concurrency của mình.

Nhưng không có nghĩa phải tăng ngay lên 150.

Có thể tăng từng bước:

4 -> 10 -> 20 -> 50

và đo:

  • Error rate.
  • Timeout.
  • DB load.
  • RAM.
  • Cost.
  • Queue wait time.
  • Gemini 429.

Senior không đoán mò.

Senior tăng có kiểm soát.

---

18.52. Khi nào tách AI Judge thành service riêng?

Queue có thể nằm trong monolith.

Không nhất thiết phải tách AI Judge thành service riêng ngay.

Tách service riêng khi có lý do:

  • Workload AI khác hẳn phần còn lại.
  • Cần scale worker riêng.
  • Cần dùng runtime/framework khác.
  • Cần deploy độc lập.
  • Cần cô lập lỗi/cost.
  • Có nhiều hệ thống cùng dùng AI Judge.
  • Team riêng sở hữu.

Nhưng ngay cả khi tách service, queue vẫn quan trọng.

Ví dụ:

Main App -> AI Judge Service: tạo grading job
AI Judge Service -> Queue -> AI workers

FastAPI service không thay thế queue nếu việc chấm vẫn lâu.

FastAPI có thể là API layer của AI Judge.

Queue vẫn là cơ chế xử lý nền.

---

18.53. Queue không thay thế domain model

Một lỗi khác:

> Có queue rồi thì task muốn làm gì cũng được.

Không đúng.

Worker task vẫn nên gọi use case.

Ví dụ:

Celery task
-> RunGradingJobUseCase
-> GradingJob Aggregate
-> Repository

Không nên để task tự update field lung tung:

job.status = "success"
job.score = ...
send notification
update progress
...

Task là adapter.

Use case là nghiệp vụ.

Queue chỉ là cách gọi use case sau.

---

18.54. Những lỗi phổ biến khi dùng queue

Lỗi 1: Không có trạng thái job

Enqueue xong nhưng không biết job đang chờ, đang chạy, hay đã chết.

Lỗi 2: Task không idempotent

Retry gây gửi email hai lần, hoàn tiền hai lần, chấm hai kết quả.

Lỗi 3: Không đo queue wait time

Chỉ biết "job chậm", không biết chậm vì chờ hay vì xử lý.

Lỗi 4: Queue chung cho mọi thứ

Job dài chặn job ngắn, job phụ chặn job quan trọng.

Lỗi 5: Retry vô hạn

Job lỗi không sửa được vẫn retry mãi.

Lỗi 6: Không có DLQ

Job chết bị mất hoặc nằm kẹt không ai biết.

Lỗi 7: Payload quá lớn

Message nặng, khó version, lộ dữ liệu.

Lỗi 8: Không kiểm soát concurrency

Tăng worker làm sập DB hoặc vượt quota external API.

Lỗi 9: Enqueue không nhất quán với database

Tạo dữ liệu thành công nhưng job không được tạo, hoặc ngược lại.

Lỗi 10: Nghĩ queue làm hệ thống nhanh hơn

Queue làm request nhanh hơn, không nhất thiết làm job hoàn thành nhanh hơn.

---

18.55. Checklist thiết kế queue

Khi thiết kế một job queue, hãy hỏi:

  • Job này giải quyết use case gì?
  • Producer là ai?
  • Worker là ai?
  • Payload gồm gì?
  • Có cần lưu job status trong database không?
  • Người dùng có cần xem trạng thái không?
  • Job có timeout không?
  • Retry bao nhiêu lần?
  • Lỗi nào retry, lỗi nào không?
  • Có DLQ không?
  • Handler có idempotent không?
  • Có thể chạy trùng không?
  • Có cần lock theo key không?
  • Có cần thứ tự không?
  • Queue nào xử lý job này?
  • Concurrency bao nhiêu?
  • Rate limit external API bao nhiêu?
  • Queue wait time mục tiêu là gì?
  • Có metrics/log/alert không?
  • Khi deploy code mới, job cũ có còn chạy được không?
  • Nếu broker down thì producer làm gì?
  • Nếu worker down thì backlog xử lý ra sao?

Nếu không trả lời được, queue đó có thể sẽ trở thành nơi giấu lỗi thay vì giải quyết lỗi.

---

18.56. Bảng chọn nhanh

| Tình huống | Queue có phù hợp không? | |---|---| | Chấm AI mất 90 giây | Rất phù hợp | | Gửi email xác nhận | Phù hợp | | Đăng nhập | Không phù hợp | | Lấy danh sách sản phẩm | Không phù hợp | | Tạo báo cáo lớn | Phù hợp | | Resize ảnh | Phù hợp | | Thanh toán cần redirect URL ngay | Có thể cần HTTP trước, queue/event sau | | Webhook payment xử lý nhiều phản ứng | Nhận nhanh, xử lý sau bằng queue | | Analytics | Phù hợp | | Reset password email | Phù hợp nhưng phải queue ưu tiên cao |

---

18.57. Tóm tắt bằng một luồng

Luồng AI Judge với queue:

Frontend
-> POST /submissions
-> API lưu Submission + GradingJob(PENDING)
-> enqueue RunGradingJob(job_id)
-> trả 202 + job_id

Worker AI
-> lấy job
-> chuyển RUNNING
-> gọi Gemini API
-> lưu kết quả
-> chuyển SUCCEEDED
-> phát GradingCompleted

Frontend
-> GET /grading-jobs/{job_id}
-> xem kết quả

Cần quan sát:

queue length
oldest job age
queue wait time
processing time
success/failure rate
retry count
DLQ count
worker concurrency
external API 429/timeout

Nếu kết quả chậm, đừng đoán.

Hãy xem job chậm vì:

  • Chờ queue.
  • Worker ít.
  • Gemini chậm.
  • Retry nhiều.
  • Database chậm.
  • Rate limit.

---

18.58. Kết luận của chương

Queue là cách giao việc rồi xử lý sau.

Nó giúp hệ thống:

  • Không bắt người dùng chờ việc lâu.
  • Kiểm soát concurrency.
  • Xử lý tải đột biến bằng hàng chờ.
  • Retry việc lỗi tạm thời.
  • Cô lập phần phụ khỏi luồng chính.
  • Scale worker theo từng loại workload.

Nhưng queue cũng làm hệ thống bất đồng bộ hơn.

Nó đòi hỏi:

  • Job status.
  • Timeout.
  • Retry policy.
  • Idempotency.
  • DLQ.
  • Monitoring.
  • Backpressure.
  • Thiết kế payload và transaction cẩn thận.

Thông điệp quan trọng nhất:

> Queue không làm công việc biến mất. Queue giúp ta quyết định công việc chờ ở đâu, ai xử lý, xử lý bao nhiêu việc cùng lúc, lỗi thì retry thế nào, và khi quá tải thì hệ thống chậm có kiểm soát thay vì sập.

Ở chương tiếp theo, ta sẽ đi sang Pub/Sub: khi một sự kiện xảy ra và nhiều bên cùng cần biết, ta không chỉ giao một job cho một worker, mà phát sự kiện cho nhiều consumer phản ứng độc lập.