Khai topology bằng bean trông như một bản thiết kế bạn giao cho thợ: đọc là biết nhà sẽ ra sao. Nhưng có hai bất ngờ. Thợ chỉ ra công trường lúc chuyến xe hàng đầu tiên tới — khai báo là lazy, chờ tới connection đầu — nên bạn nhận con dấu "khởi công xong" cả khi lô đất đang ngập. Và nếu bản vẽ ghi ba phòng mà nhà đã dựng hai, thợ chỉ ghi một dòng vào sổ chẳng ai đọc rồi làm tiếp — nhà vẫn hai phòng, bản vẽ vẫn ba, mà cả hai đều "tồn tại". Phần 24 đã điểm qua rằng auto-config cho sẵn một RabbitAdmin tự khai mọi bean topology lúc khởi động. Bài này đo xem "lúc khởi động" thật ra là lúc nào — câu trả lời không như tên gọi.

Khai bằng bean

@Bean org.springframework.amqp.core.Queue viec() {
    return QueueBuilder.durable("tp-viec").build();
}
@Bean TopicExchange suKien() { return new TopicExchange("tp-su-kien", true, false); }
@Bean Binding noi()          { return BindingBuilder.bind(viec()).to(suKien()).with("don.#"); }

Nhiều thứ cùng lúc thì gom bằng Declarables cho đỡ rối:

@Bean Declarables nhom() {
    return new Declarables(
        QueueBuilder.durable("tp-a").build(),
        QueueBuilder.durable("tp-b").build(),
        new DirectExchange("tp-x", true, false));
}

Cả hai kiểu đều được khai lên broker:

hàng đợi tp-viec  : đã khai
hàng đợi tp-a     : đã khai
hàng đợi tp-b     : đã khai

QueueBuilder mang theo mọi tham số của phần 6 — .autoDelete(), .exclusive(), .deadLetterExchange("dlx"), .ttl(1000). Đọc bean là biết hàng đợi thật trông thế nào.

Nhưng nó khai lúc nào

Đây là phần chính của bài. Tắt hẳn broker rồi khởi động ứng dụng:

ứng dụng khởi động: THÀNH CÔNG sau 0,4 giây (dù broker không có)
Khai báo topology là lazy: nó chỉ chạy khi connection đầu tiên được mở. Không có connection thì không có gì được khai, và ứng dụng báo khởi động thành công — không cảnh báo, không lỗi, health check mặc định vẫn xanh nếu bạn chưa bật cái của RabbitMQ.

Hệ quả thực tế: deploy trúng lúc broker đang bảo trì thì mọi pod lên xanh, và sự cố chỉ lộ ra khi có thông điệp đầu tiên cần đi — có thể là nhiều giờ sau, ở một chỗ chẳng liên quan gì tới lần deploy đó.

Muốn thất bại sớm thì phải hỏi broker ngay lúc khởi động: bật management health indicator, hoặc gọi rabbitAdmin.getQueueInfo("hang-doi-quan-trong") trong một ApplicationRunner và ném ngoại lệ nếu nhận null.

Khai lệch tham số: 406, nhưng ứng dụng vẫn chạy

Hàng đợi tp-viec đã tồn tại với autoDelete=false. Khai lại với autoDelete=true:

ERROR o.s.a.r.c.CachingConnectionFactory : Shutdown Signal: channel error;
  reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'auto_delete'
  for queue 'tp-viec' in vhost '/': received 'true' but current is 'false'

ứng dụng vẫn đang chạy: true
khai một hàng đợi khác sau đó -> vẫn khai được

Đúng lỗi 406 mà phần 6 đã mổ, chỉ khác là ở đây nó chỉ là một dòng log ERROR. Ứng dụng lên bình thường, những khai báo khác vẫn chạy tiếp, và nếu bạn không đọc log lúc khởi động thì hàng đợi đó cứ giữ cấu hình cũ mãi mãi trong khi mã nguồn nói khác.

Đây là kiểu hỏng tệ nhất: mã và thực tế lệch nhau mà cả hai đều "hoạt động".

Khi nào để ứng dụng tự khai

Nên, khi ứng dụng là chủ sở hữu duy nhất của những hàng đợi đó — hàng đợi nội bộ, hàng đợi tạm, môi trường dev. Đặt topology cạnh mã dùng nó là cách tài liệu tốt nhất.

Không nên, khi hàng đợi được nhiều dịch vụ dùng chung. Phần 15 đã kết luận: một nơi duy nhất khai báo, các nơi còn lại chỉ kiểm tra. Với Spring, "chỉ kiểm tra" nghĩa là đừng khai bean Queue — chỉ trỏ @RabbitListener vào tên hàng đợi. Container sẽ thất bại rõ ràng nếu hàng đợi chưa có, thay vì âm thầm tạo một cái hơi khác.

Tuyệt đối không, với hàng đợi sản xuất có tham số quan trọng — DLX, TTL, quorum. Những thứ đó không sửa được tại chỗ: đổi ý là phải xoá hàng đợi và mất hết thông điệp đang nằm trong đó. Chúng thuộc về công cụ hạ tầng, nơi thay đổi được xem xét trước khi chạy.

Muốn biết mã và broker có lệch nhau không, so trực tiếp tham số thật với QueueBuilder trong mã:

# so tham so that tren broker voi tham so trong ma nguon
docker exec -u rabbitmq rmq rabbitmqctl -q list_queues name durable auto_delete arguments

Nếu một dòng nào khác với QueueBuilder trong mã của bạn, nghĩa là đã có một dòng 406 trong log khởi động mà không ai đọc — và hàng đợi đang chạy theo cấu hình của lần đầu tiên nó được tạo ra.

Mẫu số chung

Một cấu hình tự xưng là "khai báo" nhưng được áp lazy và nuốt lỗi của chính nó thật ra là một lời đề nghị, không phải một bảo đảm. Chữ "khai" nghe đầy quyền lực, nhưng ở đây nó chỉ chạy lúc connection đầu và một cú lệch (406) chỉ thành một dòng log — nên ý định trong mã và trạng thái trên broker trôi xa nhau trong khi cả hai vẫn "chạy". Đó là cái bẫy của mọi trạng thái mong muốn mà không có gì cưỡng chế: thiếu một bước đối chiếu hay một luật fail-khi-lệch, thì mã đọc như sự thật mà không là sự thật. Cùng sự trôi dạt ấy sống trong một manifest Kubernetes không có controller nào đối chiếu, một entity ORM ánh xạ tới một schema chưa migration nào áp, một đoạn IaC chạy một lần năm ngoái rồi để mặc. Muốn biến trạng-thái-khai-báo thành thật thì cần cưỡng chế: hoặc báo lỗi to khi lệch, hoặc một vòng lặp liên tục kéo thực tế về đúng bản khai.

Điều thứ hai: "đã khởi động" không phải "sẵn sàng phục vụ", và khởi tạo lazy dời cái hỏng tới đúng thời điểm tệ nhất. Vì topology chỉ khai lúc connection đầu, một lần deploy trúng lúc broker tắt vẫn lên xanh, rồi cú sập hiện ra nhiều giờ sau ở thông điệp đầu tiên — chẳng có gì chỉ ngược về lần deploy gây ra nó. Lazy rẻ lúc đầu và đắt lúc sự cố; với một phụ thuộc thiết yếu, hãy ép kiểm tra sớm (một ApplicationRunner ping thử, một readiness probe hỏi thẳng broker) để cái hỏng rơi đúng lúc deploy, nơi có người đang nhìn. Và giữ một chủ sở hữu duy nhất cho topology dùng chung — khai một lần, còn lại chỉ nghe — để một cú khai lại thành thật không bao giờ âm thầm rẽ đôi định nghĩa.

Bài sau: xử lý lỗi trong listener — ErrorHandler, RepublishMessageRecoverer, và chỗ ngoại lệ thật sự đi tới.