Фоновые задачи и RabbitMQ: когда подтверждать обработку и что делать с повторной доставкой
Клиент создаёт заказ через API. Запись появляется в PostgreSQL, а покупателю нужно отправить подтверждение. Отправка письма зависит от внешнего сервиса и иногда занимает несколько секунд. Держать HTTP-запрос открытым всё это время неудобно: почтовый сервис может ответить медленно или временно оказаться недоступным.
Команда выносит отправку подтверждения в фоновую задачу. API сохраняет заказ, публикует сообщение в RabbitMQ и отвечает клиенту. Отдельный обработчик получает сообщение и отправляет письмо.
Теперь представим сбой: письмо уже отправлено, но обработчик выключился за мгновение до подтверждения задачи. RabbitMQ доставит сообщение повторно. Если обработчик снова отправит письмо, покупатель получит два подтверждения.
Это центральный вопрос главы: что именно подтверждает обработчик и как система ведёт себя, когда результат работы и подтверждение сообщения разделены сбоем?
Устройство HTTP-обработчиков и небольших фоновых действий уже показано в статье о FastAPI. Повторную доставку входящего события и основы идемпотентности мы разбирали в материале о webhook. Здесь проследим весь путь задачи через очередь и применим эти идеи к работающему сервису заказов.
Где очередь появляется в нашем сервисе
В сквозном примере учебного цикла FastAPI принимает запросы на api.example.com, PostgreSQL хранит заказы, Redis служит кэшем, а контейнеры запускаются через Compose. Теперь добавляется RabbitMQ и отдельный процесс-обработчик. Caddy продолжает принимать внешние HTTPS-запросы; обработчик очереди не открывает публичный HTTP-адрес.
Один заказ проходит такой путь:
HTTP-запрос -> запись заказа в PostgreSQL -> событие о новом заказе -> RabbitMQ -> обработчик -> попытка отправить подтверждение -> результат в PostgreSQL.
API и обработчик имеют разные обязанности. API отвечает за создание заказа и за честный ответ клиенту. Обработчик выполняет действие, которое допустимо завершить позже. RabbitMQ хранит и доставляет сообщение между ними.
Фоновую задачу можно запустить и внутри процесса FastAPI. Это полезно для коротких действий, которым подходит жизненный цикл того же приложения. Для работы на отдельных процессах или серверах нужен самостоятельный исполнитель и механизм передачи задач; официальная документация FastAPI приводит RabbitMQ как один из возможных компонентов такой схемы. Документация FastAPI.
Очередь вводит асинхронность, но не сообщает клиенту, что письмо уже отправлено. Если API вернул 201 Created, это означает, что заказ создан. Статус уведомления следует показывать отдельно: например, pending, sent или failed. Иначе пользователь увидит обещание, которого система ещё не выполнила.
Коротко: успешный HTTP-ответ, принятая брокером задача и завершённое фоновое действие представляют три разных результата.
Путь сообщения: от публикации до подтверждения
Для этой главы достаточно понимать четыре элемента RabbitMQ:
- Publisher, или отправитель, публикует сообщение. В нашем примере им становится отдельный процесс, передающий события о заказах.
- Exchange получает публикацию и направляет её по правилам маршрутизации.
- Queue, или очередь, хранит сообщение до доставки обработчику.
- Consumer, или обработчик, получает сообщение и выполняет работу.
Пусть exchange называется orders.events, очередь подтверждений - orders.confirmations, а ключ маршрутизации - order.created. Привязка очереди к exchange по этому ключу означает: сообщения о создании заказа должны попасть в очередь подтверждений.
Содержимое сообщения лучше сделать небольшим и устойчивым:
{
"event_id": "6b7c7f19-8f48-4e73-9af5-7ecfc9f54d8f",
"event_type": "order.created",
"schema_version": 1,
"order_id": 1045
}
event_id постоянно идентифицирует логическое событие. Он понадобится для разбора повторов. event_type сообщает обработчику, какое действие требуется. schema_version позволяет менять формат сообщения контролируемо. order_id связывает задачу с заказом в PostgreSQL.
Сообщение не обязано содержать весь заказ, адрес, электронную почту и другие персональные данные. Обработчик может получить необходимые сведения из базы по order_id. При этом нужно определить, какие сведения допустимо читать в их текущем виде, а какие должны отражать состояние на момент создания события. Например, если покупатель изменил адрес уже после оформления заказа, содержание исходного письма может требовать сохранённого снимка заказа. Это решение относится к смыслу задачи, а не к настройке RabbitMQ.
После маршрутизации сообщение может быть в одном из двух основных состояний очереди:
| Состояние | Что означает |
|---|---|
| Ready | Сообщение находится в очереди и ждёт доставки обработчику |
| Unacknowledged | Сообщение уже передано обработчику, но RabbitMQ ещё не получил подтверждение результата |
При ручном подтверждении сообщение не исчезает окончательно в момент доставки. Брокер ждёт решения обработчика. После положительного подтверждения basic.ack доставка считается завершённой. Если соединение обработчика оборвётся до подтверждения, RabbitMQ возвращает неподтверждённое сообщение для новой доставки. Документация RabbitMQ о состояниях очереди, документация о подтверждениях.
Коротко: «обработчик получил сообщение» означает начало работы. Для RabbitMQ работа завершается только после подтверждения.
Два подтверждения решают разные задачи
В системе есть два независимых направления передачи ответственности.
Первое: отправитель -> RabbitMQ. Отправителю нужно узнать, принял ли брокер сообщение. Для этого служат publisher confirms.
Второе: RabbitMQ -> обработчик. Брокеру нужно узнать, можно ли считать доставку обработанной. Для этого служит consumer acknowledgement, обычно коротко ack.
Publisher confirm не означает, что обработчик прочитал сообщение или отправил письмо. Consumer ack не сообщает исходному HTTP-запросу, что он был успешен. RabbitMQ прямо разделяет эти два механизма. Документация RabbitMQ.
Как отправитель проверяет публикацию
В примере отправитель публикует сообщение как постоянное, а очередь настроена как долговечная. Эти два свойства дополняют друг друга: сохранение самой очереди при перезапуске не делает временное сообщение постоянным.
Канал отправителя включает publisher confirms. Отправитель ждёт подтверждения публикации и отдельно проверяет маршрутизацию. Это важно потому, что RabbitMQ может подтвердить публикацию, которую exchange не направил ни в одну очередь. Флаг mandatory просит вернуть такое сообщение отправителю. При ошибке маршрутизации отправитель оставляет событие для повторной попытки и поднимает сигнал о неправильной конфигурации. Документация RabbitMQ о publisher confirms, о маршрутизации публикаций.
Если доступность RabbitMQ сама должна переживать потерю узла, нужна архитектура брокера с соответствующей репликацией и хранилищем. Создание долговечной очереди в единственном контейнере защищает от части перезапусков, но не превращает один сервер в отказоустойчивый кластер. Quorum queue может быть подходящим типом очереди для реплицируемых важных задач; решение требует отдельного расчёта числа узлов, дискового пространства и поведения при отказе. Документация RabbitMQ о quorum queues.
Почему неопределённый результат публикации приводит к повтору
Отправитель отправил сообщение, RabbitMQ его принял, но сетевое соединение оборвалось до получения publisher confirm. Отправитель не знает, принято ли сообщение. Чтобы не потерять задачу, он публикует его ещё раз.
В очереди могут оказаться два сообщения с одним event_id. Это нормальный исход неопределённости в распределённой системе. Получение publisher confirm после повторной попытки не удаляет уже созданный дубликат. Поэтому идентификатор события должен переживать повтор публикации: при новой попытке отправляется тот же event_id, а не создаётся новый. RabbitMQ описывает эту возможность дублирования при восстановлении соединения. Руководство RabbitMQ по надёжности.
Коротко: подтверждение публикации отвечает за ответственность брокера; оно не избавляет обработчик от защиты против дублей.
Между заказом в PostgreSQL и сообщением есть разрыв
Допустим, API выполняет два действия подряд:
- Сохраняет заказ в PostgreSQL.
- Публикует задачу в RabbitMQ.
Если приложение выключится между ними, заказ останется в базе, а письмо никогда не будет поставлено в очередь.
Обратный порядок тоже неудобен: сообщение может попасть обработчику раньше фиксации заказа. Обработчик запросит order_id, которого ещё не видно в базе. При последующем откате транзакции заказа не будет вовсе.
Транзакция PostgreSQL охватывает изменения внутри PostgreSQL. Она не делает публикацию в RabbitMQ частью той же транзакции. Для согласования используют transactional outbox: вместе с заказом в одной транзакции записывают строку о предстоящем событии.
Упрощённая структура:
CREATE TABLE outbox_events (
id uuid PRIMARY KEY,
event_type text NOT NULL,
aggregate_id bigint NOT NULL REFERENCES orders(id),
schema_version integer NOT NULL,
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
published_at timestamptz
);
id станет постоянным event_id. aggregate_id хранит номер заказа. payload содержит данные публикации. published_at остаётся пустым, пока отдельный отправитель не получит подтверждение брокера.
Создание заказа и события выполняется в одной транзакции:
BEGIN;
INSERT INTO orders (id, status, total)
VALUES (1045, 'created', 2500.00);
INSERT INTO outbox_events (
id,
event_type,
aggregate_id,
schema_version,
payload
)
VALUES (
'6b7c7f19-8f48-4e73-9af5-7ecfc9f54d8f',
'order.created',
1045,
1,
'{"order_id": 1045}'::jsonb
);
COMMIT;
В реальном приложении id заказа и события создаются кодом или базой; фиксированные значения здесь нужны, чтобы проследить одну задачу. Если транзакция откатится, не останется ни заказа, ни события. Если зафиксируется, отправитель позже найдёт событие даже после перезапуска API.
Отдельный relay-процесс выбирает строки с пустым published_at, публикует их с publisher confirms и mandatory, затем отмечает подтверждённые строки как отправленные. При нескольких relay-процессах строки нужно распределять через блокировку или ограниченную по времени аренду. Один из вариантов выбора пачки - FOR UPDATE SKIP LOCKED: каждый исполнитель пропускает строки, уже занятые другим.
Здесь сохраняется небольшой, но принципиальный разрыв:
RabbitMQ подтвердил публикацию -> relay выключился -> published_at не записан.
После перезапуска relay отправит событие повторно. Это допустимо: задача outbox состоит в том, чтобы не потерять событие при сбое. Обработчик обязан переносить повторы. Паттерн и структура события подробно описаны в документации Debezium об outbox; в нашем примере relay самостоятельно читает таблицу и публикует в RabbitMQ.
Коротко: outbox связывает запись заказа с намерением отправить задачу. Он превращает возможную потерю события в возможный повтор, который затем контролирует обработчик.
В какой момент отправлять ack
Рассмотрим одну доставку сообщения обработчику подтверждений. Он должен проверить заказ, подготовить результат, зафиксировать состояние и отправить ack брокеру.
Есть три возможных момента подтверждения.
Момент ack |
Сбой сразу после него | Результат |
|---|---|---|
| До работы | Обработчик выключился, письмо ещё не отправлено | RabbitMQ считает доставку завершённой; задачу можно потерять |
| После действия, но до записи результата | Письмо могло уйти, база этого не знает | При повторе возможна вторая отправка |
| После фиксации проверяемого результата | База знает о результате; ack ещё мог не дойти |
Повтор возможен, но его можно распознать |
Для важных задач выбирают ручное подтверждение после фиксации результата. В AMQP 0-9-1 basic.ack относится к конкретной доставке, обозначенной delivery_tag. Этот номер действителен внутри канала, на котором пришло сообщение. Подтверждение на другом канале или повторное подтверждение того же тега закрывает канал с ошибкой. Документация RabbitMQ.
Принцип обработчика:
получить сообщение -> проверить формат и заказ -> выполнить или обнаружить
ранее выполненное действие -> зафиксировать результат -> отправить ack
Когда обработчик использует несколько потоков, подтверждение всё равно должно выполняться через поток и канал соединения, которому принадлежит доставка. Например, Pika ограничивает доступ к своему BlockingConnection из других потоков и предоставляет специальный способ запланировать callback в потоке соединения. Документация Pika.
Если процесс выключится после фиксации результата, но до ack, RabbitMQ доставит сообщение вновь. Поэтому позднее подтверждение необходимо сочетать с идемпотентной обработкой. Само по себе оно не обещает выполнение действия строго один раз.
ack, nack и reject
basic.ackозначает, что обработчик закончил работу и брокер может завершить доставку.basic.nackсообщает о неуспехе; он также умеет работать с несколькими доставками.basic.rejectсообщает о неуспехе одной доставки.- Параметр
requeue=trueвозвращает сообщение для новой доставки. - При
requeue=falseсообщение направляется в dead-letter exchange, если он настроен; без подходящего маршрута оно может быть удалено.
Автоматическое подтверждение считает сообщение обработанным уже при отправке потребителю. Для важной работы с заказами такой режим создаёт окно потери задачи при выключении обработчика. Документация RabbitMQ о режимах подтверждения.
Коротко: ack отправляют после устойчивого результата или после доказательства, что тот же результат уже был получен ранее.
Повторная доставка не означает повторное бизнес-действие
RabbitMQ может доставить сообщение повторно после выключения обработчика, закрытия канала или явного возврата сообщения в очередь. При повторной доставке у сообщения появляется признак redelivered. Он помогает диагностировать ситуацию, но не служит ключом защиты: один и тот же логический event_id может попасть в очередь двумя отдельными публикациями, и первая доставка второй копии не обязана иметь этот признак. Документация RabbitMQ.
Для проверки используется постоянный идентификатор события и ограничение в PostgreSQL. Допустим, первая фоновая операция создаёт запись подтверждения заказа, которую затем можно показать в кабинете пользователя:
CREATE TABLE order_confirmations (
event_id uuid PRIMARY KEY,
order_id bigint NOT NULL REFERENCES orders(id),
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (order_id)
);
Обработчик выполняет транзакцию:
INSERT INTO order_confirmations (event_id, order_id)
VALUES (
'6b7c7f19-8f48-4e73-9af5-7ecfc9f54d8f',
1045
)
ON CONFLICT DO NOTHING;
Если сообщение получено впервые, запись появляется. Если то же событие пришло ещё раз, уникальное ограничение сохраняет один результат. После фиксации транзакции обработчик отправляет ack.
Почему нужна уникальность в базе, а не предварительный запрос SELECT? Два обработчика могут одновременно проверить отсутствие записи и оба начать действие. Уникальное ограничение разрешает эту гонку в момент записи. Здесь полезны два ключа: event_id защищает от повторов события, а UNIQUE(order_id) выражает предметное правило «одно подтверждение этого типа на заказ». Если для заказа возможны разные виды подтверждений, в таблицу добавляют kind и используют UNIQUE(order_id, kind).
Это пример идемпотентного изменения PostgreSQL. Отправка письма создаёт дополнительную границу.
Где заканчивается защита базы
Предположим, обработчик вызывает почтовый сервис, сервис отправляет письмо, а затем обработчик выключается до записи sent в PostgreSQL. Повторная доставка заставит его вызвать почтовый сервис снова. Уникальная строка order_confirmations сама по себе не докажет, что первое письмо ушло.
Если внешний сервис поддерживает ключ идемпотентности, ему передают постоянный event_id либо отдельный ключ вида order-confirmation:1045. При повторе с тем же ключом сервис должен возвращать результат прежней операции без новой отправки. Перед использованием этого механизма нужно проверить его действительные гарантии и срок хранения ключа у конкретного поставщика.
Если такой гарантии нет, строгое «ровно одно письмо при любых сбоях» обещать нельзя. Команда выбирает допустимое поведение: возможный дубль, ручную сверку, проверку состояния через API поставщика или другой канал доставки. Для денежных операций такая граница особенно существенна: повторный запрос к платёжному сервису требует его собственного идемпотентного ключа и сверки результата.
Коротко: RabbitMQ обеспечивает повторную возможность выполнить задачу. Один бизнес-результат при повторах обеспечивают правила в базе и, для внешних действий, гарантии соответствующего внешнего сервиса.
Какие ошибки повторять, а какие отправлять на разбор
Повторная попытка имеет смысл, когда причина может исчезнуть сама. Например, почтовый сервис временно отвечает 503, соединение оборвалось или база кратковременно недоступна. Если адрес письма некорректен либо формат сообщения невозможно прочитать, многократная немедленная попытка ничего не исправит.
Полезно разделить исходы:
| Исход | Пример | Действие |
|---|---|---|
| Успех | Результат записан или уже существовал | ack |
| Временный сбой | Внешний сервис недоступен | Отложенная повторная попытка |
| Постоянная ошибка данных | В сообщении отсутствует order_id |
Зафиксировать ошибку и отправить на разбор |
| Ошибка кода | Неожиданное исключение на конкретном сообщении | Ограничить повторы, сохранить контекст и исправить обработчик |
| Выключение процесса | Работа прервалась до ack |
Брокер доставит неподтверждённое сообщение вновь |
Простой nack с requeue=true может немедленно вернуть сообщение тому же или другому обработчику. Если причина не исчезла, получится быстрый цикл ошибок, который нагружает брокер, обработчики и внешний сервис. Поэтому временным сбоям назначают задержку, а числу попыток - понятный предел или отдельное правило остановки.
Одна из возможных последовательностей задержек:
Задержка(n) = min(60 секунд, 5 секунд × 2^(n−1)), где n - номер попытки начиная с единицы.
Тогда первые задержки равны 5, 10, 20, 40 и 60 секундам. Небольшой случайный разброс помогает избежать одновременного повтора множества задач после восстановления внешнего сервиса. Числа служат примером: короткая операция отправки письма и долгая операция формирования отчёта требуют разных настроек.
Отложенный повтор можно организовать средствами брокера, отдельной очередью с задержкой или через состояние задачи и планировщик в PostgreSQL. Способ зависит от версии RabbitMQ и требований к наблюдаемости. Например, актуальная документация quorum queues описывает встроенные механизмы delayed retry и особенности счётчика повторных доставок. Поэтому нельзя без проверки версии считать, что каждый nack увеличивает один и тот же счётчик попыток. Документация RabbitMQ о quorum queues.
Очередь ошибок
Dead-letter exchange, или DLX, принимает сообщения, выведенные из основной очереди по определённым причинам: например, после reject без возврата либо после превышения настроенного предела доставок. Отдельная очередь ошибок нужна, чтобы сохранить само сообщение, причину, event_id, номер заказа и время сбоя для расследования. Наличие DLX ещё не гарантирует, что сообщение надёжно дошло до целевой очереди: маршрут, доступность цели и режим передачи тоже требуют проверки. Для quorum queues RabbitMQ предоставляет режим более надёжной передачи dead-letter сообщений; поведение по умолчанию имеет ограничения. Документация RabbitMQ о DLX.
Оператору нужен порядок повторного запуска задач из очереди ошибок:
- Установить причину и исправить её.
- Проверить текущее состояние заказа и прежние попытки.
- Повторно отправить событие с тем же логическим идентификатором, если это всё ещё та же задача.
- Наблюдать результат и зафиксировать, какие сообщения обработаны.
Бездумная команда «перекинуть всю очередь ошибок обратно» может сразу воспроизвести исходную аварию.
Коротко: повтор нужен для временной причины; сообщения с неисправимыми данными должны оставаться видимыми для разбора.
Сколько задач выдавать одному обработчику
RabbitMQ может доставить обработчику несколько сообщений до получения подтверждений. Число неподтверждённых доставок ограничивает prefetch_count.
Например:
channel.basic_qos(prefetch_count=4)
channel.basic_consume(
queue="orders.confirmations",
on_message_callback=handle_message,
auto_ack=False,
)
prefetch_count=4 разрешает одному потребителю получить до четырёх неподтверждённых сообщений. auto_ack=False включает ручное подтверждение. Callback handle_message обрабатывает каждую доставку. Особенность RabbitMQ состоит в том, что при обычной настройке QoS ограничение применяется к каждому потребителю на канале; при нескольких потребителях нужно понимать фактическую область действия настройки. Документация RabbitMQ о prefetch.
Prefetch ограничивает количество выданной работы, но не создаёт параллельное выполнение внутри одного последовательного обработчика. Если процесс выполняет только одну задачу одновременно, остальные три сообщения будут ждать у него как неподтверждённые. Для расширения мощности нужны дополнительные процессы, потоки с корректной работой клиентской библиотеки либо асинхронная обработка, если характер задачи это допускает.
Пример расчёта: пять обработчиков выполняют по одной задаче одновременно, средняя задача занимает две секунды. Теоретическая скорость без дополнительных задержек - около 5 / 2 = 2,5 задачи в секунду. Очередь из 600 готовых задач потребует как минимум около 600 / 2,5 = 240 секунд, если новые задачи не поступают. В реальности время увеличат сетевые вызовы, ограничения почтового сервиса, повторы и неравномерность длительности задач.
Высокий prefetch может скрыть часть очереди в состоянии unacknowledged и увеличить объём работы, который вернётся после падения обработчика. Низкий prefetch ограничивает этот риск, но иногда снижает пропускную способность. Его подбирают измерением для конкретной задачи.
Для длинных операций учитывают время ожидания подтверждения и работу heartbeat соединения. Если библиотека перестаёт обслуживать сетевой цикл на время длительного расчёта, RabbitMQ может счесть соединение потерянным и выдать задачу снова. Pika отдельно описывает это ограничение для долгой обработки в потоке соединения. Документация RabbitMQ о heartbeat, документация Pika.
Коротко: скорость определяется числом реально выполняемых задач и их длительностью; prefetch управляет количеством задач, уже выданных без подтверждения.
Порядок сообщений нельзя принимать за порядок бизнес-событий
Для одного заказа могут возникнуть события order.created, order.paid и order.cancelled. На первый взгляд кажется, что обработчик увидит их в этом порядке. Несколько отправителей, параллельные обработчики и повторные доставки способны изменить наблюдаемый порядок. RabbitMQ описывает условия, при которых порядок выдачи и обработки расходится. Документация RabbitMQ об очередях.
Поэтому обработчик уведомления должен проверять актуальное состояние заказа. Если к моменту обработки заказа order.created заказ уже отменён, отправлять письмо «ваш заказ готовится» может быть неверно. Для событий, которым нужен исторический снимок, этот снимок следует явно сохранить. Для событий, которые должны отражать актуальное состояние, обработчик перечитывает заказ и проверяет допустимый переход.
Если строгий порядок для каждого заказа необходим, его проектируют отдельно: например, ограничивают параллельную обработку связанных событий, распределяют заказы по ключу или проверяют версии состояния. Глобальное последовательное выполнение всех заказов часто оказывается слишком дорогим решением.
Что наблюдать после запуска
Само число сообщений в очереди не говорит, почему работа задерживается. Полезны как минимум четыре группы сигналов:
messages_readyрастёт: задачи поступают быстрее, чем обработчики их забирают, либо обработчиков нет.messages_unacknowledgedрастёт: обработчики получили работу, но долго её не подтверждают. Причиной могут быть зависшие внешние вызовы, слишком высокий prefetch или медленная обработка.- Повторные доставки растут: обработчики теряют соединение, завершаются с ошибками либо сообщения возвращаются в очередь.
- Очередь ошибок растёт: появились задачи, для которых автоматический путь завершился без успеха.
RabbitMQ предоставляет показатели готовых и неподтверждённых сообщений. К ним нужно добавить прикладные показатели: возраст самого старого события в outbox, задержку от создания заказа до подтверждения, число ошибок поставщика писем, количество заказов со статусом уведомления failed. Документация RabbitMQ о показателях очередей.
Проверка работоспособности API не заменяет эти метрики. API может успешно создавать заказы, пока relay не публикует события или обработчики остановлены. Внешне сервис отвечает, а письма копятся часами.
Для практической диагностики полезно искать одну задачу по event_id через все этапы:
заказ -> строка outbox -> публикация -> доставка -> запись результата -> внешний сервис -> ack.
Одинаковый идентификатор в логах и записях позволяет понять, произошёл ли повтор публикации, повтор доставки или повтор самого внешнего действия.
Что меняется при восстановлении PostgreSQL
В предыдущей главе мы разобрали резервные копии и восстановление базы. Добавление RabbitMQ вводит ещё одно состояние: часть сообщений может находиться в брокере, пока PostgreSQL восстанавливается к более раннему моменту.
Предположим, очередь содержит задачу для заказа №1045, а восстановленная база создана из копии, сделанной до появления этого заказа. Обработчик получит сообщение, но не найдёт заказ. Обратная ситуация тоже возможна: в восстановленной базе событие outbox снова имеет пустой published_at, хотя до аварии оно уже было доставлено и обработано. Relay опубликует его повторно.
Поэтому после восстановления базы обработчики и relay не следует включать без проверки соответствия состояний. Команда сопоставляет точку восстановления PostgreSQL с состоянием RabbitMQ, определяет задачи для отсутствующих заказов и готовится к повторной публикации событий из outbox. Постоянный event_id, идемпотентная обработка и наблюдаемое состояние задачи делают такую сверку выполнимой.
Это граница применения резервной копии PostgreSQL: она восстанавливает PostgreSQL, но не переносит автоматически RabbitMQ и результаты сторонних сервисов к той же секунде.
Проверка схемы сбоями
Перед публикацией главы полезно мысленно выполнить короткий набор испытаний на тестовом окружении.
Сбой после записи заказа, до публикации. Заказ и строка outbox остаются в PostgreSQL. После запуска relay событие будет опубликовано.
Сбой после публикации, до отметки published_at. Relay повторно публикует тот же event_id. Обработчик распознаёт повтор и не создаёт второе подтверждение.
Сбой после изменения PostgreSQL, до consumer ack. RabbitMQ повторяет доставку. Уникальные ограничения не дают создать второй результат в базе; обработчик отправляет ack после проверки уже выполненного действия.
Сбой после действия во внешнем сервисе, до записи sent. Возможен повтор внешнего вызова. Его безопасность зависит от идемпотентного ключа и возможностей поставщика; без такой поддержки требуется отдельная политика сверки или принятия дублей.
Неверный ключ маршрутизации. Publisher confirm сам по себе недостаточен. Публикация с mandatory должна быть возвращена отправителю; outbox остаётся неотправленным, а ошибка становится видимой.
Постоянно падающая задача. Она не должна бесконечно и мгновенно занимать обработчик. После ограниченных попыток сообщение сохраняется на разбор, а статус уведомления показывает сбой.
Если для каждого случая можно назвать сохранённое состояние, следующего исполнителя и способ проверки результата, процесс обработки достаточно определён для эксплуатации.
Итог
RabbitMQ доставляет фоновую работу между процессами. Надёжность возникает из согласованной работы нескольких механизмов:
транзакция заказа и outbox -> подтверждённая публикация -> долговечная очередь -> ручное подтверждение после результата -> безопасная обработка повтора -> контролируемые ошибки и наблюдение.
Самое важное правило для обработчика: сообщение может прийти снова после того, как действие уже произошло. Поэтому ack ставят после устойчивого результата, постоянный event_id используют для проверки повторов, а внешние действия разбирают по их собственным гарантиям идемпотентности.
Тогда фоновая задача перестаёт быть «функцией, которая когда-нибудь запустится». У неё появляются проверяемые состояния, понятные последствия сбоя и путь от созданного заказа до завершённого действия.