Асинхронность и сообщения: событийный конвейер заказов
Практический проект: построй маленький событийный конвейер заказов с идемпотентным at-least-once consumer'ом, транзакционным outbox и ограниченной очередью, затем сломай его намеренно — убей relay, переотправь, перегрузи — чтобы доказать, что гарантии держатся.
Прочитать, что outbox побеждает проблему двойной записи, — не то же, что увидеть, как твой конвейер переживает падение relay, шторм переотправок и перегрузку — и остаётся корректным. Построй маленький событийный конвейер заказов, затем намеренно сломай его: убей процесс между commit и publish, переотправь каждое сообщение дважды и завали consumer’а нагрузкой. Смысл не в отполированном сервисе; смысл — увидеть, как твои гарантии держатся (или падают) под ровно теми падениями, о которых предупреждал этот раздел.
Этот проект делает весь раздел операционным: ты реализуешь паттерны — at-least-once с идемпотентностью, транзакционный outbox, ограниченную очередь с backpressure — и затем прогонишь инъекции сбоев, что отделяют код, который выглядит корректным, от кода, который является корректным под падениями.
Построй минимальный событийный конвейер заказов — сервис заказов, что пишет заказ и испускает событие OrderPlaced через транзакционный outbox, очередь/лог, несущий событие, и один-два идемпотентных consumer'а (например, склад, email) — затем впрысни сбои этого раздела (падение relay, переотправка, перегрузка) и продемонстрируй, что ни одно событие не потеряно, ни один эффект не дублирован и система деградирует мягко под нагрузкой.
- Работающий конвейер, где оформление заказа надёжно приводит к ровно-effectively-once downstream-эффектам, продемонстрированный end-to-end.
- Инъекция сбоя A: записанный прогон (логи/скрины), где процесс убит в зазоре commit→publish и, после перезапуска, relay публикует ожидающую outbox-строку — доказывая 'никогда не потеряно' и что это обеспечивает outbox, а не везение.
- Инъекция сбоя B: записанный прогон, где каждое сообщение доставлено дважды, и хранилище дедупа consumer'а показывает пропуск второй доставки — доказывая, что эффект случился раз, несмотря на доставку at-least-once.
- Инъекция сбоя C: записанный прогон, где устойчивая перегрузка запускает backpressure (отказы/сброс или ограниченную блокировку) вместо вечно растущего бэклога, с графиком глубины очереди/лага и указанной выбранной границей.
- Краткий разбор: семантика доставки, с которой ты начал, где именно outbox возвращает атомарность, почему consumer идемпотентен и от чего защищают твои граница + DLQ + аларм на лаг — плюс абзац об окне согласованности в конечном счёте, что испытал бы пользователь, и как бы ты показал его в UI.
- Добавь вторую consumer-группу (аналитика) на том же потоке и продемонстрируй fan-out — обе группы видят каждое событие независимо — и на логе переиграй consumer аналитики с офсета 0 для бэкфилла.
- Добавь шторм ретраев: включи агрессивные клиентские ретраи во время перегрузки и покажи ускорение коллапса очередей, затем укроти его экспоненциальным backoff + jitter и circuit breaker'ом, сравнив две кривые бэклога.
- Преврати поток в маленькую оркестрацию (сагу): координируй заказ → списание → резерв координатором, что запускает компенсирующее действие (возврат/освобождение), когда поздний шаг падает, и покажи, что состояние процесса запрашиваемо ('где заказ #X?').
- Добавь мультитенантную справедливость: shuffle-shard'ь арендаторов по малому пулу очередей и покажи, что всплеск одного арендатора больше не заморяет остальных.
- 01Что доказывает каждая из трёх инъекций сбоя и зачем их впрыскивать?
- 02Где именно outbox возвращает атомарность и что consumer всё равно обязан делать?
Этот проект превращает асинхронный раздел в повторяемый рабочий процесс: построй минимальный событийный конвейер заказов — сервис заказов, что пишет заказ и outbox-строку в одной локальной транзакции, relay, что публикует OrderPlaced из outbox, брокер/лог и идемпотентный at-least-once consumer, что дедуплицирует по order_id — затем заставь каждую гарантию отработать, ломая систему намеренно. Инъекция A убивает процесс в зазоре commit→publish и показывает, что relay всё равно публикует после перезапуска (outbox = никогда не потеряно). Инъекция B доставляет каждое сообщение дважды и показывает, как хранилище дедупа пропускает второе (идемпотентность = once-эффект). Инъекция C перегружает consumer’а и показывает, как ограниченная очередь применяет backpressure вместо безграничного бэклога, с DLQ для ядовитых сообщений и алярмом на лаг как ранним предупреждением. Разбор связывает всё: семантика доставки, с которой ты начал, где возвращается атомарность, почему consumer идемпотентен и как бы ты показал окно согласованности в конечном счёте в UI. Инженер, что построил и сломал это однажды, проектирует асинхронные системы под падение, а не только под счастливый путь — что и есть весь смысл этого раздела.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.