Transactional Outbox: rollback и повтор публикации¶
Тип материала: учебная fault-injection лаборатория. Все данные, IDs, интервалы и суммы — HYPOTHETICAL. NOT_RUN относится к интеграционной части; локальные результаты фиксируются отдельно в отчёте Этапа 4.
Зависимости и reuse¶
Reuse ledger.sql: ledger.post_transfer сохраняет transfer, entries и outbox. Новая production relay реализация не создаётся; ниже manual harness воспроизводит две commit boundaries. PG 17.6 и Kafka 4.1 берутся из existing compose.
Подготовка и команды¶
Команды PowerShell выполняются из корня handbook. Используйте отдельные namespace/workdir; не подключайте production endpoints. Доступность Docker проверяется docker info; одного docker compose config недостаточно. Нужны свободные localhost ports и синтетические fixture.
docker compose -f examples/processing/compose.yaml config --quiet
docker compose -f examples/processing/compose.yaml up -d kafka postgres
Existing compose — один broker без HA; credentials только локальные demo. Kafka bootstrap для host client: localhost:19092. Установите dependencies по existing setup. Здесь и ниже $labPython указывает на существующий Python venv с processing requirements.
Создайте отдельную пустую БД rel4_outbox командой psql (один раз), затем загрузите existing schema:
docker compose -f examples/processing/compose.yaml exec -T postgres psql -U demo -d banking -c 'CREATE DATABASE rel4_outbox;'
Get-Content 02-databases-and-storage/snippets/ledger.sql -Raw | docker compose -f examples/processing/compose.yaml exec -T postgres psql -v ON_ERROR_STOP=1 -U demo -d rel4_outbox
Откройте psql сессии через exec postgres psql -U demo -d rel4_outbox. Existing schema уже содержит sample accounts1/2 и operation suffix001. Для теста используйте operation suffix201 и перевод 20 minor units с overdraft account1 на account2. Kafka topic rel4-outbox-unique-run — только для этого эксперимента.
Точка fault injection и последовательность¶
- В PG выполните
BEGIN; SELECT ledger.post_transfer(1,'00000000-0000-0000-0000-000000000201',1,2,'EUR',20); ROLLBACK;. Oracle: transfer/entries/outbox для ID201 отсутствуют. - Повторите с COMMIT и убедитесь, что все три сущности существуют.
- Получите outbox row и вручную отправьте её в Kafka console producer с event_id как key; broker acceptance — отдельный boundary.
- Смоделируйте crash relay: не записывайте delivered marker, затем отправьте ту же строку снова. Для manual harness marker может быть отдельным тестовым журналом; он не меняет исходный ledger schema.
- Прочитайте Kafka raw copies. Для проверяемого downstream effect используйте ниже отдельный schema adapter: он формирует synthetic reporting event, а не новый ledger debit.
Проверки и evidence¶
Rollback даёт ноль persisted business/outbox rows с ID201. Commit даёт один transfer, две balanced entries и одну outbox row для ID201; sample ID001 считаются отдельно. Повтор публикации может дать две Kafka copies с одним event_id. Для downstream проверки выполните в psql запрос адаптации и сохраните единственную JSON-строку без заголовка в новый UTF-8 без BOM файл вне репозитория, по пути из переменной $outboxEvent. Подготовьте пути в PowerShell, затем выполните SQL:
$labDir = New-Item -ItemType Directory (Join-Path ([IO.Path]::GetTempPath()) ("handbook-outbox-" + [guid]::NewGuid()))
$outboxEvent = Join-Path $labDir.FullName "outbox-event.jsonl"
$outboxTarget = Join-Path $labDir.FullName "outbox-target.sqlite"
SELECT jsonb_build_object(
'tenant_id',tenant_id::text,'event_id',event_id::text,
'currency',payload->>'currency',
'amount_minor',(payload->>'amount_minor')::bigint,
'occurred_at','2026-11-01T00:00:00Z',
'entity_version',1,'schema_version',1)::text
FROM ledger.outbox
WHERE tenant_id=1 AND event_id='00000000-0000-0000-0000-000000000201';
occurred_at здесь — фиксированная synthetic fixture дата для reporting adapter, не настоящее время публикации. Сохраните строку psql в unaligned tuples-only режиме (-At). Producer читает именно этот файл:
& $labPython examples/processing/kafka_producer.py --topic rel4-outbox-adapted-unique-run --mode idempotent --data $outboxEvent
# Точка отказа relay: broker подтвердил публикацию, marker не сохранён.
& $labPython examples/processing/kafka_producer.py --topic rel4-outbox-adapted-unique-run --mode idempotent --data $outboxEvent
& $labPython examples/processing/kafka_consumer.py --topic rel4-outbox-adapted-unique-run --group rel4-outbox-adapted-unique-run --database $outboxTarget --max-records 2
Ожидается два disposed records, одна inbox identity, reporting total tenant 1/EUR=20. Consumer DB и topic должны быть свежими; broker acceptance обеих application sends проверьте по producer output. Idempotent producer не устраняет намеренную повторную application send.
Команда console producer (введите строку event_id|json_payload из outbox, затем завершите stdin):
docker compose -f examples/processing/compose.yaml exec kafka /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server kafka:9092 --topic rel4-outbox-unique-run --property parse.key=true --property 'key.separator=|'
Записывайте versions, исходные offsets/cut, момент injection, состояние после recovery и independent oracle. Отсутствующий prerequisite даёт NOT_RUN; несработавший ожидаемый fault — FAIL эксперимента, а не PASS correctness.
Recovery и cleanup¶
Повторно публикуйте исходные IDs из durable backlog; не удаляйте outbox до broker acceptance. Сверьте upstream transfer IDs и published IDs. Удаление тестовой БД/топика — отдельное действие после evidence, не обязательный автоматический cleanup.
Остановите созданные тестовые процессы; очистка допускается только для собственных синтетических namespaces. Не используйте down -v для общей лаборатории 3B: это удалит evidence предыдущих работ. Сохраните protocol до cleanup.
Ограничения и статус¶
Manual harness проверяет crash matrix только при реально зафиксированном broker outcome. SQL adapter — учебная проекция turnover, он не проверяет downstream ledger. Production relay и cross-database delivery transaction не реализованы; подготовленные команды не считаются исполненным E2E test.
Интеграционный статус в текущем окружении: NOT_RUN — Docker daemon недоступен. Ожидаемые результаты выше являются acceptance criteria, а не фактически полученным runtime output.
Источники¶
- PostgreSQL isolation — 17.
- Kafka 4.1 design — 4.1.