Событие публикации и границы доставки
Редактор опубликовал материал, после чего отдельные сервисы должны обновить свои данные. Каталог сохраняет видимость статьи, аудит записывает факт изменения. Связывать оба действия одним длинным HTTP-запросом неудобно: один получатель может быть временно недоступен. В проекте «Доставка» введём RabbitMQ как брокер между издателем события и независимыми обработчиками.
Результат первого урока — понятный договор события публикации. Используем Python 3.13, Pika 1.3.2 и RabbitMQ 4.3.6; выбранный сервер указан в официальной информации о выпусках. Автор не запускал брокер, базы, приложения и тесты. Все результаты серии описаны как ожидаемые, а архив содержит один полный итоговый проект, чьи части вводятся последовательно.
Команда просит, событие сообщает
Команда «опубликовать article-107» просит владельца материала выполнить действие. Она может быть отвергнута: редакция устарела или заголовок некорректен. Событие material.published сообщает, что владелец уже принял соответствующее предметное изменение. Получатель не должен по одному имени события угадывать, состоялась ли публикация или ещё требуется её согласовать.
В нашем домене владельцем состояния является локальный API. Позднее он сохранит изменение и событие в одной транзакции outbox. На первом этапе изучаем отдельный уже подготовленный факт из sample-event.json. Этот файл помогает понять форму обмена, но не заменяет настоящую проверку публикации у владельца материала.
Полный учебный конверт события:
{
"schemaVersion": 1,
"eventId": "84b73cb0-1880-4a79-8510-3d0c1d5cb107",
"occurredAt": "2026-10-12T09:00:00+00:00",
"type": "material.published",
"articleId": "article-107",
"revision": 1,
"payload": {
"title": "HTTP и кеширование",
"url": "/articles/http-cache.html"
}
}
eventId определяет идентичность факта. Если доставка повторена, он остаётся прежним. Новый случай публикации следующей редакции получает новый eventId. Номер попытки не входит в него: иначе повтор выглядел бы самостоятельным фактом, и обработчик не смог бы связать его с уже выполненной работой.
articleId определяет материал, а revision — его предметную редакцию. У одной статьи бывает много событий разных редакций. Время occurredAt полезно для наблюдения, но не заменяет номер редакции: часы процессов могут расходиться, а два изменения происходить близко друг к другу. Поэтому последующее состояние каталога сравнивается по явной редакционной последовательности.
Конверт отделён от полезной нагрузки
Конверт содержит сведения, нужные большинству инфраструктурных компонентов: тип, версию, идентичность и время. Payload содержит предметные данные конкретного события. Такое разделение помогает обработчику сначала проверить, умеет ли он читать договор, затем работать с заголовком и адресом. Оно не отменяет проверки каждого поля внутри payload.
Функция validate_event из contracts.py принимает только известные поля. Версия схемы должна быть целым числом 1, eventId — UUID, дата — строкой с часовым поясом. Payload имеет строго title и url. Изменение этого набора позже требует осознанной версии договора либо согласованной политики расширения, а не молчаливого принятия случайного объекта.
Почему не отправляем весь Markdown? Каталогу достаточно небольшой проекции видимости и ссылки, аудиту — записи конверта. Передача полного исходника увеличила бы размер сообщения и распространение редакционных данных. Если отдельному получателю нужен текст, можно определить способ получения нужной редакции из доверенного источника. Это другой договор, который следует добавить явно.
Нельзя назвать payload из этой серии готовой поисковой записью OpenSearch. У него другая схема, другая цель и нет основного текста, анализаторов или манифеста корпуса. Подключение поискового сервиса потребовало бы отдельного адаптера, проверки редакций и правила удаления. RabbitMQ переносит байты по топологии; совместимость предметных данных создают разработчики сервисов.
Что знает каждая сторона
Издатель знает, какой факт он отправляет. Брокер знает exchange, маршруты и очереди, но не понимает редакционную полезность title. Получатель знает, какой эффект он должен выполнить в своём хранилище. Поэтому сообщение может быть технически доставлено и предметно отвергнуто как несовместимое. Границы проверки должны быть видны в коде каждой стороны.
Ожидаемые отношения для article-107:
Одна редакция → один предметный факт с устойчивым eventId
Один факт → возможно несколько попыток доставки
Несколько сервисов → отдельное завершение обработки у каждого
Подтверждение брокера → не подтверждение обновлённого каталога
Это схема, а не журнал выполненного обмена. Позднее добавим publisher confirms и consumer acknowledgements, чтобы каждая сторона могла осознанно передавать ответственность. Потеря ответа после успешного действия всё равно способна привести к повтору. Поэтому не обещаем универсальную обработку ровно один раз.
Для одного факта создадим две очереди: delivery.catalog и delivery.audit. Первая предназначена для обновления локальной проекции, вторая — для журнала событий. Если два процесса читают одну очередь, они обычно делят работу, а не получают каждый полную копию события. Топология должна отражать предметное желание, иначе «подписка двух сервисов» станет случайной конкуренцией за одну доставку.
Независимость имеет стоимость
Асинхронный обмен означает, что после ответа владельца состояния каталог может некоторое время отставать. Пользовательский интерфейс должен различать принятое изменение и завершение всех производных действий. Если продукт действительно требует немедленного согласованного ответа нескольких систем, брокер сам по себе не создаст такую транзакцию.
У каждого обработчика появляется ответственность за повторы, отказавшие сообщения и своё состояние. Брокер помогает удерживать работу, но не решает, можно ли безопасно повторить внешний платёж, публикацию письма или изменение файла. В нашем примере эффект хранится локально вместе с квитанцией, чтобы показать границу атомарности без скрытого внешнего действия.
Эта серия относится к обмену между сервисами. Она отличается от Redis/BullMQ, где часто рассматривается очередь фоновой работы одного приложения, и от Temporal, который сохраняет историю длительного процесса. Технологии могут сосуществовать, но выбор механизма должен исходить из владельца состояния, числа получателей и характера результата, а не из общего слова «очередь».
Полезная нагрузка фиксирует данные именно произошедшего изменения. Если получатель при каждом повторе читает только текущий материал, он может применить revision 3 вместо указанной в событии revision 1. Иногда это допустимый договор обновления последнего состояния, но тогда его нужно назвать явно. В нашей серии payload принадлежит конкретной редакции, поэтому повторный конверт не пересобирается из более позднего состояния.
Различайте версию схемы и редакцию материала. SchemaVersion говорит, как читать конверт и какие поля известны. Revision говорит, какое состояние article-107 описывает этот факт. Повышение revision не позволяет consumer читать неизвестную schemaVersion, а изменение схемы не означает новую публикацию каждого материала автоматически. Эти независимые числа сопровождают разные изменения системы.
Для самостоятельного разбора представьте вторую доставку sample-event.json. Укажите поля, которые должны остаться прежними, и сведения, которые можно вынести в заголовки попытки. Затем опишите новое событие withdrawn для revision 2. В следующем уроке создадим соединение и топологию, сохранив этот договор факта.