awesome-everything EN
↑ Обратно к восхождению

Очереди, потоки, события

Outbox: чтение кода и схемы

Суть Читай реальный SQL и сниппеты хендлеров — схему outbox, claim-запрос relay и баг dual-write — предсказывай поведение и выбирай фикс с наибольшим рычагом.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

Схема, запрос relay и хендлер — это место, где корректность outbox выигрывается или теряется. Прочитай каждый сниппет, предскажи, как он ведёт себя под крахом или нагрузкой, затем выбери фикс, который senior сделает первым.

Цель

Отработай цикл, который ты запускаешь в каждом ревью outbox: прочитай SQL и хендлер, найди разрыв, который вскрывает крах, и потянись за фиксом, закрывающим его без изобретения распределённой транзакции.

Сниппет 1 — хендлер

async function placeOrder(db, broker, order) {
  await db.query("INSERT INTO orders (id, status) VALUES ($1, 'placed')", [order.id]);
  await broker.publish("OrderPlaced", order);   // отдельная система, отдельный вызов
}
Викторина

Этот хендлер — тот самый dual-write, ради убийства которого существует юнит. Где разрыв и какой минимальный фикс?

Сниппет 2 — схема outbox

CREATE TABLE outbox (
  id          uuid PRIMARY KEY,
  aggregate   text        NOT NULL,
  event_type  text        NOT NULL,
  payload     jsonb       NOT NULL,
  created_at  timestamptz NOT NULL DEFAULT now(),
  sent_at     timestamptz                          -- NULL, пока relay не пометит sent
);
CREATE INDEX outbox_unsent ON outbox (created_at) WHERE sent_at IS NULL;
Викторина

Почему частичный индекс `WHERE sent_at IS NULL` — правильная форма для нагрузки relay?

Сниппет 3 — claim-запрос relay

BEGIN;
SELECT id, event_type, payload
  FROM outbox
 WHERE sent_at IS NULL
 ORDER BY created_at
 LIMIT 100
 FOR UPDATE SKIP LOCKED;
-- публикуем каждую строку в брокер, затем:
UPDATE outbox SET sent_at = now() WHERE id = ANY($claimed_ids);
COMMIT;
Викторина

Три реплики relay гоняют этот запрос конкурентно. Что даёт FOR UPDATE SKIP LOCKED и какая гарантия всё ещё НЕ держится?

Сниппет 4 — консьюмер

async function onOrderPlaced(db, event) {
  // event.id — стабильный id outbox-строки, пронесённый через брокер
  await db.query("INSERT INTO orders_processed (event_id) VALUES ($1)", [event.id]);
  await chargeCard(event.order);
}
Викторина

Доставка at-least-once, поэтому этот консьюмер может получить то же событие дважды. Что не так и как сделать его idempotent?

Итог

Каждое ревью outbox читается одинаково в коде: голый хендлер показывает разрыв dual-write, который вскрывает крах; частичный индекс схемы держит poll неотправленных строк дешёвым по мере роста таблицы; FOR UPDATE SKIP LOCKED в claim-запросе даёт репликам масштабироваться без двойной публикации, но разрыв publish-then-mark держит доставку at-least-once; а консьюмер замыкает петлю уникальным event id плюс ON CONFLICT DO NOTHING, чтобы повтор списал карту ровно раз. Найди разрыв, сделай намерение долговечным, захватывай строки непересекающимися батчами, дедупь downstream.

Продолжить восхождение ↑Outbox: построй крах-безопасный конвейер событий
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.