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

Практикум: повторная доставка без второго счёта

Запускаемый код из статьи. Нужен работающий Docker. Скрипт создаёт и удаляет собственные контейнеры PostgreSQL и Kafka. Выполните команду из docs/blog/lab/2026-09-07-exactly-once-effects в клоне репозитория:

uv run --no-project --python 3.13 --with-requirements requirements.txt effects_lab.py

В requirements.txt закреплены omni-box 0.3.0, aiokafka 0.14.0, SQLAlchemy 2.0.54, asyncpg 0.31.0 и testcontainers 4.15.0. Образы: postgres:17-alpine и confluentinc/cp-kafka:7.6.0 в режиме KRaft. Для PostgreSQL явно указан драйвер asyncpg.

В models.py определены заказы, счета и две ORM-модели omni-box. event_flow.py содержит код из статьи и настройку producer для relay. lab_support.py запускает контейнеры и читает Kafka с ограничением времени; его используют все три практикума. Для простоты два сервиса здесь работают с одной БД.

Проверяются следующие результаты:

  • commit заказа и сбой до отправки оставляют заказ без события;
  • отправка события и откат транзакции оставляют событие без заказа;
  • заказ и его событие Outbox откатываются вместе;
  • сбой после подтверждения Kafka, но до commit Outbox приводит к повторной публикации;
  • у двух записей Kafka разные offset и один event_id;
  • Inbox обрабатывает первую запись, пропускает вторую и оставляет один счёт.

Идентификатор заказа в счёте намеренно не уникален: ограничение не должно скрывать повторный вызов обработчика. В рабочей схеме правило об одном счёте на заказ стоит дополнительно закрепить собственным ограничением.

Сбои на границах транзакций воспроизводятся исключениями, а не завершением процесса. Стенд с одним брокером проверяет доставку и изменения в БД; репликация и переключение брокеров сюда не входят. Проверка Kafka читает до заранее полученного конечного offset, а не полагается на один опрос с таймаутом. Каждая строка PASS появляется после проверок; при нарушении условия скрипт завершается с ошибкой.

Продолжение: сбой обработчика и границы Inbox или недоступность брокера и восстановление.

Исходный код практикума на GitHub.