---
url: https://docs.nemo12.com/reference/queues.md
description: >-
  Queue Reference: nemo12-events, consumer, retry tối đa 5 lần và dead letter
  queue nemo12-events-dlq.
---

# Queue Reference

## 1. Queue đang có

| Queue | Vai trò | Cấu hình |
| --- | --- | --- |
| `nemo12-events` | Mọi domain event của hệ thống | `max_batch_size: 25` · `max_retries: 5` · DLQ `nemo12-events-dlq` |
| `nemo12-events-dlq` | Dead letter — message thất bại quá 5 lần | Chưa có consumer; xử lý thủ công |

Khai báo tại `workers/api/wrangler.jsonc`. Producer binding: `EVENTS`. Consumer: chính worker `nemo12-api` (`export default { queue }`).

Danh sách 19 loại event và payload từng loại: [Event Catalog](events.md) (sinh tự động từ code).

***

## 2. Envelope

```ts
{
  event_id: string,          // UUID, duy nhất cho mỗi lần publish
  idempotency_key: string,   // ổn định theo NGHIỆP VỤ — cùng nghiệp vụ = cùng key
  type: string,              // "EvidenceRecorded", "GoalSet", …
  occurred_at: string,       // ISO
  producer: "api",
  version: 1,
  payload: Record<string, unknown>
}
```

Phân biệt hai khóa: `event_id` đổi mỗi lần gửi; `idempotency_key` **không đổi** cho cùng một sự việc. Consumer phải khử trùng theo `idempotency_key`, không theo `event_id`.

Ví dụ: `warmup:${sessionId}`, `exam-attempt:${attemptId}`, `orca:${learnerId}:${competitionId}:${state}`.

***

## 3. Publish — không được làm hỏng nghiệp vụ chính

```ts
try {
  await queue.send(event);
} catch (err) {
  console.error("EVENT_PUBLISH_FAILED", { type, idempotencyKey, err });
}
```

Hai luật:

1. **Publish SAU khi commit.** Không bao giờ phát event cho việc chưa chắc đã xảy ra.
2. **Lỗi publish bị nuốt.** Queue chết không được phép làm học sinh không nộp được bài (SDD-006 §2). Đánh đổi: mất event. Chấp nhận được vì hiện **không có nghiệp vụ nào phụ thuộc vào event để đúng** — xem §4.

Tìm sự cố: lọc log `EVENT_PUBLISH_FAILED` trong Workers observability (đã bật trong `wrangler.jsonc`).

***

## 4. Consumer — trạng thái thật

```ts
async queue(batch: MessageBatch<unknown>, _env: Env): Promise<void> {
  for (const message of batch.messages) {
    console.log("event", { id: message.id, body: message.body });
    message.ack();
  }
}
```

::: warning Consumer hiện là stub
Consumer **chỉ log rồi ack**. Chưa có handler nghiệp vụ nào.

Điều này **có chủ ý** ở giai đoạn hiện tại: mọi thứ cần chạy sau một sự kiện đang được gọi **đồng bộ** ngay trong request (ví dụ `runModelsSafely()` sau khi nộp bài). Nhờ vậy hệ thống đúng ngay cả khi queue chết hoàn toàn, và event đóng vai trò **nhật ký + đường mở rộng**, chưa phải đường thi hành.

Hệ quả cần nhớ: **đừng chuyển nghiệp vụ nào sang queue mà không viết consumer trước.** Publish thành công không có nghĩa là có ai đó xử lý.
:::

### Khi viết consumer thật — bắt buộc

| Yêu cầu | Vì sao |
| --- | --- |
| **Idempotent theo `idempotency_key`** | Queue là *at-least-once*. Cùng message sẽ đến hai lần. |
| `ack()` khi xong, `retry()` khi lỗi tạm | `ack()` mù như hiện nay sẽ nuốt luôn lỗi thật |
| Không gọi lại API của chính mình | Vòng lặp event |
| Ghi run log nếu chạy engine | Truy vết được (xem [workflows](workflows.md)) |
| Chịu được message của version cũ | `version` trong envelope tồn tại để làm việc này |

### DLQ

Quá 5 lần retry → `nemo12-events-dlq`. Hiện **chưa có consumer và chưa có cảnh báo** — nghĩa là message chết ở đó im lặng. Trước khi có nghiệp vụ thật chạy qua queue, phải có ít nhất một cảnh báo khi DLQ có message (QG-009).

## Trace

* REQ-PLT-03 (event-driven), REQ-NFR-01.
* Thiết kế: [SDD-001](../architecture/sdd-001-platform.md) §8, [SDD-006](../architecture/sdd-006-reliability.md) §2.
* Kiểm chứng: QG-009.
* Liên quan: [Event Catalog](events.md) · [Schedules](schedules.md) · [Webhooks](webhooks.md).
