Khai báo một hàng đợi giống như đăng ký một quầy ở chợ bằng một tờ đơn có bốn ô đánh dấu: ghi vào sổ cái để còn sau khi chợ đóng rồi mở lại (durable), chỉ mình tôi dùng (exclusive), tự trả lại khi người thuê cuối rời đi (autoDelete). Có một tổ hợp ô mà ban quản lý không còn nhận — và tuỳ lỗi, họ hoặc gạch tờ đơn của bạn, hoặc mời bạn ra khỏi chợ. Mở bất kỳ hướng dẫn RabbitMQ nào, dòng đầu tiên sau khi kết nối đều là thế này:

ch.queueDeclare("hello", false, false, false, null);

Chạy nó trên RabbitMQ 4.3.5 (bản mà phần 2 dựng lên):

541 INTERNAL_ERROR - Feature `transient_nonexcl_queues` is deprecated.
By default, this feature is not permitted anymore.
The feature will be removed from a future major RabbitMQ version...

Và không chỉ lệnh đó hỏng — cả connection bị đóng. Bài này đi qua bốn cờ khai báo, tám tổ hợp của chúng, và bốn mã lỗi mà mỗi cái có phạm vi sát thương khác nhau.

Bốn tham số

ch.queueDeclare(name, durable, exclusive, autoDelete, arguments);
Cờ Nghĩa Sống sót qua
durable Định nghĩa hàng đợi được ghi xuống đĩa Broker khởi động lại
exclusive Chỉ connection khai báo được dùng Không: chết cùng connection
autoDelete Tự xoá khi consumer cuối rời đi Không: chết khi hết consumer
arguments TTL, độ dài tối đa, kiểu hàng đợi, DLX… —

Chú ý cột cuối: durable nói về định nghĩa hàng đợi, không nói gì về thông điệp bên trong. Phần sau của sê-ri sẽ đo chuyện đó.

Tám tổ hợp, hai cái không còn được phép

Thử cả tám trên broker sạch:

durable exclusive auto-delete Kết quả
true true true cho phép
true true false cho phép
true false true cho phép
true false false cho phép
false true true cho phép
false true false cho phép
false false true 541, đóng connection
false false false 541, đóng connection

Luật rút gọn: hàng đợi không bền mà lại dùng chung cho nhiều connection thì không còn được phép. Không bền thì phải là của riêng một connection.

Lý do RabbitMQ chặn: một hàng đợi transient dùng chung là thứ không ai gọi lại được sau khi broker khởi động lại, nhưng lại vẫn nhận thông điệp như thường trong lúc chạy — nguồn mất dữ liệu âm thầm. Chặn ở tầng khai báo là cách duy nhất bắt được nó.

Sửa thì đơn giản: đổi false đầu tiên thành true. Hàng đợi bền không tốn gì thêm cho tới khi có thông điệp trong đó.

Bốn mã lỗi, hai phạm vi sát thương

Đây là chỗ đáng nhớ hơn cả danh sách cờ:

Mã Khi nào Giết cái gì
404 NOT_FOUND queueDeclarePassive hàng đợi không có channel
405 RESOURCE_LOCKED Chạm vào hàng đợi exclusive của connection khác channel
406 PRECONDITION_FAILED Khai lại với tham số khác lần trước channel
541 INTERNAL_ERROR Tổ hợp cờ bị cấm cả connection
Lỗi ở tầng channel thì channel đó đã đóng, mọi lời gọi sau trên nó ném AlreadyClosedException. Nhiều mã nguồn bắt ngoại lệ của queueDeclare, ghi log rồi đi tiếp trên cùng channel — và mọi thứ sau đó hỏng vì một lý do chẳng liên quan gì.

406 PRECONDITION_FAILED: khai lại khác tham số

khai lần 1 (durable=true)   : OK
khai lần 2 (auto-delete=true): 406 PRECONDITION_FAILED - inequivalent arg 'auto_delete'
                               for queue 'co-thu': received 'true' but current is 'false'
channel sau đó              : ĐÃ ĐÓNG

Thông báo này rất tử tế — nó nói rõ cờ nào lệch, nhận gì và đang là gì. Đây là lỗi hay gặp nhất khi hai dịch vụ cùng khai một hàng đợi mà chỉ một bên được sửa. Cách tránh: một nơi duy nhất khai báo topology, các nơi còn lại dùng queueDeclarePassive — chỉ hỏi, không tạo.

exclusive: riêng tư và chết cùng connection

chủ sở hữu tự hỏi         : rieng-tu
connection khác chạm vào  : 405 RESOURCE_LOCKED - cannot obtain exclusive access
sau khi chủ đóng kết nối  : KHÔNG CÒN   (dù khai durable=true)

Dòng cuối là chỗ dễ hiểu nhầm nhất: exclusive thắng durable. Khai bền đến mấy thì hàng đợi exclusive vẫn biến mất khi connection tạo ra nó đóng lại. Đó là ý đồ thiết kế — nó dành cho hàng đợi tạm của một client, kiểu hàng đợi trả lời trong mẫu RPC.

autoDelete: chỉ tính từ consumer đầu tiên

khai xong, CHƯA có consumer: CÒN     <- vẫn còn
đang có 1 consumer         : CÒN
consumer cuối rời đi       : KHÔNG CÒN

Hàng đợi autoDelete không tự xoá lúc vừa khai. Nó chờ có ít nhất một consumer rồi mới bắt đầu đếm; consumer cuối cùng rời đi thì nó biến mất. Nếu producer khai hàng đợi rồi gửi trước khi consumer kịp kết nối, thông điệp vẫn nằm đó chờ — và đó là hành vi đúng.

Nên khai thế nào

Mặc định: durable=true, hai cờ kia false. Đây là hàng đợi nghiệp vụ bình thường, và giờ cũng là tổ hợp duy nhất không có bẫy.

exclusive=true cho hàng đợi tạm của một client — hàng đợi trả lời, hàng đợi theo dõi. Để tên rỗng cho broker tự đặt.

autoDelete=true chỉ khi hàng đợi thật sự vô nghĩa lúc không ai nghe — kiểu phát tán sự kiện thời gian thực. Với hàng đợi công việc thì đây là cách mất việc.

Muốn soi cờ thật của mọi hàng đợi đang có thì một lệnh là đủ:

# xem cờ thật của mọi hàng đợi đang có
docker exec -u rabbitmq rmq rabbitmqctl -q list_queues \
    name durable auto_delete exclusive messages

Nếu có hàng đợi nghiệp vụ nào hiện durable=false, bạn vừa tìm ra thứ sẽ biến mất trong lần bảo trì tiếp theo.

Mẫu số chung

Phạm vi sát thương của một lỗi quan trọng ngang với thông điệp của nó: có lỗi chỉ vô hiệu cái channel hiện tại, có lỗi kéo sập cả connection — và bắt một lỗi mà không biết nó thuộc loại nào là bạn đang làm việc tiếp trên một cái tay cầm đã chết. Đó đúng là cái bẫy ở đây: mã bọc queueDeclare, ghi log rồi dùng lại chính channel đó — mọi lời gọi sau đều hỏng vì một lý do chẳng dính gì tới con bug thật. Luật chung: trước khi bắt-và-đi-tiếp, phải biết lỗi vừa vô hiệu hoá phạm vi nào, vì một ngoại lệ bắt được không phải lời hứa rằng tài nguyên còn dùng được. Cùng hình dạng với một giao dịch CSDL đã rollback mà bạn vẫn gửi câu lệnh tiếp, một socket sau lỗi giao thức, một chuỗi promise sau khi đã reject — hãy hồi phục lên một tay cầm mới, đừng lên cái đã nhiễm độc.

Điều thứ hai: một cái chốt chặn tốt nhất là đặt ở nơi lỗi còn nhìn thấy được. RabbitMQ cấm tổ hợp transient-dùng-chung ngay lúc khai báo chính vì cái hỏng nó gây ra — một hàng đợi âm thầm biến mất sau khởi động lại nhưng vẫn nhận thông điệp trong lúc chạy — là thứ vô hình lúc chạy; bắt nó sớm, kể cả bằng cách đóng connection ầm ĩ, vẫn hơn để nó mất dữ liệu lặng lẽ. Đây là kiểm-tại-ranh-giới / fail-fast: một schema từ chối dòng rác lúc chèn, một trình biên dịch từ chối một kiểu bất khả, một trình đọc cấu hình báo lỗi ở một khoá lạ — tất cả dời cái hỏng từ "im lặng và muộn" sang "ầm ĩ và sớm". Và ca 406 inequivalent-args là cùng bản năng đó hướng vào phối hợp: một định nghĩa mà hai bên cùng sở hữu cần một nguồn sự thật duy nhất (một nơi khai, còn lại queueDeclarePassive), nếu không cú khai thành thật của bên thứ hai sẽ thành một sự cố.

Bài sau: một thông điệp gồm những gì, và vì sao deliveryMode=2 đo ra nhanh hơn deliveryMode=1.