Очереди, потоки, события
Outbox: построй крах-безопасный конвейер событий
Читать про разрыв dual-write — не то же самое, что увидеть, как событие исчезает, а затем закрыть дыру самому. Построй небольшой сервис заказов, докажи, что он теряет события наивным способом, затем добавь transactional outbox плюс idempotent-консьюмера и покажи — с намеренно инъецированными крахами — что ничего не потеряно и ничего не применено дважды.
Преврати ментальную модель юнита в работающий конвейер: пиши бизнес-строку и outbox-строку в одной transaction, шли строки крах-безопасным polling relay, масштабируй relay без двойной публикации и подтверди, что гарантия at-least-once держится сквозь под инъецированными сбоями.
Построй сервис заказов, который публикует событие OrderPlaced для каждого закоммиченного заказа с нулём потерянных событий под инъекцией крахов, используя transactional outbox и polling relay, и докажи гарантию at-least-once idempotent-консьюмером, который никогда не применяет дважды.
- Сравнение до/после: наивный dual-write теряет хотя бы одно событие под инъекцией краха; outbox-версия теряет ноль при том же расписании крахов.
- Доказательство (логи или счётчик), что relay доставил хотя бы одно событие более одного раза под инъекцией краха, И что побочный эффект всё равно применён ровно один раз — доказывая идемпотентность консьюмера.
- При нескольких работающих репликах relay ни один event id не публикуется дважды за один проход — SKIP LOCKED захватывает непересекающиеся батчи.
- Краткий разбор: где был разрыв dual-write, почему одна локальная transaction его закрывает, почему доставка всё ещё at-least-once и как консьюмер дедупит.
- Замени polling relay на CDC relay (Debezium tailing WAL или логическая репликация твоей БД) и измерь падение сквозной задержки против polling с интервалом 500мс.
- Добавь очистку outbox, не вредящую пути записи: либо батчевые удаления старых sent-строк, либо дневные партиции, reaping через DROP PARTITION, и покажи, что poll неотправленных строк остаётся быстрым по мере роста таблицы.
- Сохрани порядок по aggregate: ключуй события по aggregate id, чтобы все события одного заказа приземлялись в одну партицию и одному воркеру, и продемонстрируй, что порядок держится даже с несколькими relay.
- Добавь sweep ретенции inbox (дропай обработанные event id старше N часов) и порассуждай об окне: слишком короткое рискует переприменить очень поздний дубликат, слишком длинное раздувает таблицу дедупа.
Это конвейер, к которому ты потянешься всякий раз, когда запись обязана надёжно триггерить уведомление: докажи, что наивный dual-write теряет события, затем пиши бизнес-строку и outbox-строку в одной локальной transaction, чтобы намерение публиковать было долговечным, шли строки relay, который захватывает непересекающиеся батчи через SKIP LOCKED, и делай консьюмеров idempotent по стабильному event id, чтобы неизбежный дубликат at-least-once был no-op. Построй это раз с намеренно инъецированными крахами, и production-версия — никогда тихо не теряющая запись — станет мышечной памятью.