Перейти к содержанию

Exchanges и независимые маршруты получателей

Издатель направляет событие в delivery.events, а не прямо в конкретного Python-обработчика. Между ними стоит маршрутизация RabbitMQ. В этом уроке объясним, почему каталог и аудит получают независимые копии, как binding выбирает очередь и зачем возвращать отложенную попытку по отдельному direct-exchange. Результат — топология, отражающая предметные обязанности сервисов.

Продолжаем declare из broker.py. Топология и сообщения не запускались; схемы результата являются ожидаемыми. Официальный Python-урок о topic exchange определяет соответствие routing key и шаблонов binding. Наши имена событий и очередей выбраны для домена публикации материала.

Exchange не хранит адрес процесса

Издатель знает exchange и routing key. Exchange выбирает подходящие bindings, а они ведут к очередям. Consumer подписывается на очередь и может подключаться после публикации. Таким образом, издатель не должен знать сетевой адрес процесса каталога или число его экземпляров. Условия хранения относятся к очередям, куда направлено сообщение.

Это помогает разделить инфраструктурные и предметные имена. delivery.events обозначает место обмена фактами библиотеки, material.published — тип факта, delivery.catalog — очередь одной обязанности. Если назвать exchange просто именем текущего consumer-процесса, архитектура начнёт зависеть от его случайной реализации и будет сложнее добавлять независимого получателя.

В нашем binding используется material.*. Звёздочка соответствует одному сегменту, поэтому material.published и material.withdrawn подходят. Тип material.review.started имеет уже три сегмента и не подходит этому шаблону. Для нуля или нескольких сегментов применяется #, но расширение подписки должно иметь предметную причину, а не быть способом скрыть неверные типы.

Сопоставление для текущих очередей:

Routing key catalog: material.* audit: material.*
material.published Копия в очередь Копия в очередь
material.withdrawn Копия в очередь Копия в очередь
material.review.started Не подходит Не подходит
user.created Не подходит Не подходит

Таблица описывает маршруты, но не предметную поддержку сообщения consumer. Получатель дополнительно проверяет schemaVersion и известные type. Подходящий routing key ещё не делает случайный JSON допустимым событием. Не переносите проверку данных исключительно в имя маршрута.

Одна очередь делит работу

Предположим, запускаем два процесса catalog, оба подписаны на delivery.catalog. Они помогают обрабатывать работу одной обязанности и обычно получают разные доставки из общего накопленного набора. Это способ увеличения числа исполнителей, а не подписка двух независимых систем на каждый факт. Для полной независимой копии нужна отдельная очередь.

Именно поэтому audit имеет delivery.audit. Если каталог завершил свою обработку и отправил ack, сообщение аудита продолжает ждать собственного получателя. Очереди имеют независимое состояние и могут отставать на разное время. Владелец события не должен считать успех одной очереди завершением всех производных задач.

Если очередь аудита временно не объявлена, topic exchange не начинает автоматически хранить будущую подписку за неё. Для накопления важной работы соответствующая очередь и binding должны существовать к моменту публикации. Позднее добавленный получатель не получает прежние факты автоматически. Историю или повторное построение состояния нужно организовать отдельным способом.

Это отличается от системы журнала, где независимые читатели сохраняют позиции в общем потоке. В нашей серии изучаем RabbitMQ-очереди для передачи работы и копий события. Нельзя обещать произвольный replay всей истории только потому, что сообщение однажды прошло через exchange. Удалённая после ack доставка больше не является доступным архивом фактов.

Direct-маршрут возвращает попытку одному сервису

Допустим, catalog временно не смог записать проекцию, а audit уже успешно сохранил факт. Если отложенную попытку снова опубликовать в delivery.events с material.published, она попадёт обоим. Аудит снова получит дубликат, хотя его работа завершена. Идемпотентность способна защитить эффект, но ненужная доставка всё равно расходует ресурсы.

Поэтому retry-очередь каталога возвращает сообщение в direct-exchange delivery.resume с routing key catalog. Этот exchange имеет точный binding к delivery.catalog. Аудит возвращается по ключу audit. Путь повторения относится к конкретной обязанности, а исходный предметный тип сохраняется внутри события и свойств.

Фрагмент полного объявления возвратного маршрута:

channel.exchange_declare(
    exchange='delivery.resume', exchange_type='direct', durable=True
)
channel.queue_bind(
    queue='delivery.catalog', exchange='delivery.resume',
    routing_key='catalog'
)

Default exchange с пустым именем также является direct-обменом с известным правилом направления по имени очереди. Его используем при явной публикации в delivery.catalog.retry или delivery.catalog.dead. Это удобный адресный путь, но имя назначения должно принадлежать нашей конфигурации, а не приходить от произвольного внешнего пользователя.

Несовпадение маршрута видно отдельно

В publish_event установлен mandatory=True. Если ни одна очередь не подходит, RabbitMQ возвращает сообщение издателю через механизм basic.return. В режиме подтверждений Pika сообщает это как непригодную публикацию. Такое наблюдение помогает отличить «брокер доступен» от «работа действительно имеет хотя бы одно назначение».

Но mandatory не доказывает наличие каждой предполагаемой очереди. Если binding каталога есть, а аудит ошибочно отсутствует, сообщение маршрутизировано хотя бы куда-то. Для предметного требования обоих сервисов нужно контролировать топологию и отдельное завершение получателей. Один return-флаг не является распределённой проверкой всех обязанностей.

Ожидаемые схемы доставки:

delivery.events + material.published → catalog и audit
delivery.resume + catalog → только catalog
default exchange + delivery.catalog.dead → очередь отказов каталога
неподходящий routing key + mandatory → возврат издателю

Схема не является выполненным trace. При будущем наблюдении важно сопоставлять queue, eventId и тип операции. Повторный publish того же события увеличивает транспортную работу, хотя идентичность факта остаётся прежней. Это различие потребуется для ack, retry и идемпотентной записи.

Переименование события также меняет маршруты. Если издатель начал отправлять content.published, а bindings всё ещё ждут material.*, новое имя не «похоже достаточно», чтобы совпасть. Версия договора и migration топологии должны согласовываться между сторонами. Молчаливое изменение строки type способно выглядеть как остановка consumer, хотя сообщения до его очереди вообще не дошли.

Совпадение нескольких bindings одной очереди не означает несколько независимых обязанностей этой очереди. Поэтому не пытайтесь обозначить два вида работы добавлением двух похожих шаблонов к одному имени. Отдельная квитанция и состояние получателя привязаны к consumer-домену. Если два эффекта должны завершаться независимо, у них должны быть отдельные маршруты и способ наблюдать незавершённую работу каждого.

Для упражнения добавьте третью очередь уведомлений и решите, нужна ли ей подписка на withdrawn. Затем представьте повтор только каталога и укажите правильный exchange. В следующем уроке свяжем ack с фиксацией эффекта, чтобы сообщение исчезало из очереди в определённый момент ответственности.

Оглавление серии