Суть Читай реальный код консьюмера, SQL и строку метрики, предсказывай поведение гарантий доставки и выбирай фикс с наибольшим рычагом, который senior делает первым.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Баги гарантий доставки находят в коде консьюмера, в SQL и в строке метрики — редко в брокере. Прочитай каждый сниппет, предскажи, откуда дубликат или потеря, затем выбери фикс, к которому senior тянется первым.
Цель
Отработай цикл, который запускаешь в каждом инциденте двойного списания: прочитай хендлер, найди окно краша или гонку и тянись к структурному фиксу, а не к подкрутке таймаута.
Сниппет 1 — консьюмер SELECT-then-act
def handle(msg): if db.execute("SELECT 1 FROM processed WHERE msg_id=%s", msg.id): return ack(msg) # already done, skip stripe.charge(msg.amount) # side effect db.execute("INSERT INTO processed (msg_id) VALUES (%s)", msg.id) ack(msg)
Викторина
Completed
Два консьюмера получают одно сообщение при ребалансе. Что происходит и какой фикс с наибольшим рычагом?
Heads-up UNIQUE-ограничения тут нет, и даже с ним списание происходит ДО INSERT — обе карты списаны раньше, чем выполнится любой INSERT. Фикс — INSERT первым внутри той же транзакции, что и side effect.
Heads-up At-least-once явно допускает одновременную передоставку после visibility timeout или при ребалансе. Предположение об единственной доставке — корневая ошибка.
Heads-up Больший таймаут сужает окно, но гонка SELECT/act — баг корректности независимо от тайминга; ребаланс снова её открывает вне зависимости от таймаута. Сделай дедуп атомарным.
Сниппет 2 — транзакционный дедуп
BEGIN; INSERT INTO processed (msg_id) VALUES ('msg-7a3f'); -- UNIQUE(msg_id) UPDATE orders SET status = 'paid' WHERE id = 'O-123';COMMIT;-- on UNIQUE violation: ROLLBACK, log "duplicate", ack the broker
Викторина
Completed
Консьюмер делает COMMIT этой транзакции, затем падает до ack брокеру. Что происходит при передоставке?
Heads-up Нарушение UNIQUE на INSERT прерывает транзакцию до того, как UPDATE сможет повториться. INSERT-first это ровно то, что предотвращает второй side effect.
Heads-up Краш случился ДО ack, поэтому у брокера всё ещё есть сообщение и он передоставляет — потери нет. Это безопасный порядок: закоммить работу, затем ack.
Heads-up Здесь в один момент один консьюмер (исходный упал). Даже одновременно UNIQUE-ограничение сериализует их через нарушение, а не дедлок.
Сниппет 3 — консьюмер с внешним API
def handle(msg): resp = stripe.charge(msg.amount, idempotency_key=msg.id) # external dedup db.execute("UPDATE intents SET charge_id=%s WHERE msg_id=%s", resp.id, msg.id) ack(msg)
Викторина
Completed
Вызов Stripe удался, затем процесс умирает до UPDATE и ack. Брокер передоставляет через минуты. Каково реальное поведение?
Heads-up Idempotency-Key привязан к msg.id, а не к строке БД. Stripe дедуплицирует на своей стороне независимо от того, выполнился ли твой UPDATE; он возвращает закэшированное списание.
Heads-up При передоставке тот же ключ возвращает закэшированное списание, UPDATE наконец приземляется, и ack очищает сообщение. Intent-строка плюс стабильный ключ делают ретрай безопасным.
Heads-up Stripe возвращает HTTP 200 с закэшированным ответом на повторный ключ внутри TTL; это не путь ошибки.
Сниппет 4 — метрика дедупа
metric: dedup_check_hit_rate steady state: 0.05% 14:02-14:12 : 8.0% (no deploy in this window) 14:13+ : 0.06%
Викторина
Completed
Читая эту метрику, что вероятнее всего случилось — и это ли аварийная ситуация корректности?
Heads-up СКАЧОК hit-rate означает, что UNIQUE-ограничение срабатывает ЧАЩЕ — дубликаты ловятся, а не протекают. Сбойная таблица показала бы ошибки или падение успешных вставок, а не рост hit-rate.
Heads-up Ретраи idempotent producer дедуплицируются на брокере раньше, чем консьюмер их видит, поэтому они не могут двигать dedup_hit_rate консьюмера. Причина — передоставка консьюмеру.
Heads-up Exactly-once delivery никогда не действовала — система at-least-once по дизайну. Транзиентный скачок передоставки, поглощаемый идемпотентным консьюмером, это ожидаемое поведение, а не сломанная гарантия.
Итог
Каждый инцидент гарантий доставки читается в коде, SQL и метриках: SELECT-then-act гонится при одновременной доставке; INSERT-first в одной транзакции делает передоставку безвредной; Idempotency-Key, выведенный из стабильного ID сообщения, расширяет дедуп через внешний API; а скачок dedup_hit_rate означает, что брокер передоставляет, а не что дедуп сломался. Диагностируй окно краша или гонку, примени структурный фикс (атомарный дедуп, стабильный idempotency-ключ), затем подтверди, что путь дубликата закрыт — прежде чем трогать хоть один таймаут.