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

Архитектура бэкенда

Идемпотентность и retry: чтение кода и запросов

Суть Читай реальные сниппеты idempotency, retry и inbox, предсказывай их поведение под конкурентностью или сбоем и выбирай самый рычажный фикс.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

Баги идемпотентности прячутся в зазоре между двумя statement, в отсутствующем вызове jitter и в retry-условии, которое срабатывает на неверном статусе. Читай каждый сниппет как на ревью — предскажи сбой под конкурентностью или нагрузкой, затем выбери фикс, который senior делает первым.

Цель

Потренируй цикл ревью для этого юнита: замечай окно гонки, retry, усиливающий herd, потерянный переход и dedup, который молча ничего не делает — затем назови одно изменение, закрывающее каждый.

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

func handleCharge(ctx context.Context, db *sql.DB, key string, body []byte) (*Resp, error) {
    var existing Row
    err := db.QueryRowContext(ctx,
        `SELECT response_body, status FROM idempotency_keys WHERE key = $1`, key,
    ).Scan(&existing.Body, &existing.Status)
    if err == sql.ErrNoRows {
        // строки ещё нет -> трактуем как new
        db.ExecContext(ctx,
            `INSERT INTO idempotency_keys (key, fingerprint, status) VALUES ($1, $2, 'in_progress')`,
            key, fingerprint(body))
        return process(ctx, db, key, body) // выполняет списание
    }
    return replay(existing), nil
}
Викторина

Два запроса с одним ключом приходят в этот хендлер в пределах миллисекунды. Что идёт не так и какой единственный самый рычажный фикс?

Сниппет 2 — цикл retry

async function callWithRetry(fn, maxAttempts = 6) {
  let delay = 200;
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === maxAttempts - 1) throw err;
      await sleep(delay);          // фиксированное удвоение, без случайности
      delay = Math.min(30000, delay * 2);
    }
  }
}
Викторина

Этот цикл делает retry на каждую брошенную ошибку с удвоением задержки. Назови две разные production-опасности.

Сниппет 3 — переход состояния

-- запрос пришёл; строка для этого ключа уже есть
BEGIN;
  SELECT status FROM idempotency_keys WHERE key = $1;       -- читает 'in_progress'
  -- приложение видит in_progress, но всё равно вызывает платёжный провайдер...
  -- провайдер успешен, затем:
  UPDATE idempotency_keys
     SET status = 'completed', response_body = $2
   WHERE key = $1;
COMMIT;
Викторина

Retry гонится с исходным запросом: исходный ещё in_progress, когда выполняется этот блок. В чём дефект использования машины из четырёх состояний?

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

BEGIN;
  INSERT INTO processed_events (event_id) VALUES ($1)
    ON CONFLICT (event_id) DO NOTHING;
  -- применяется всегда, независимо от того, сделала ли вставка выше что-либо:
  UPDATE inventory SET reserved = reserved + $qty WHERE sku = $sku;
COMMIT;
Викторина

Relay переотправляет событие после сбоя, поэтому этот блок выполняется дважды для одного event_id. Что реально происходит и какой фикс?

Итог

Четыре рефлекса ревью для этого юнита: проверка idempotency через SELECT-потом-INSERT — это гонка, сделай создание атомарным через ON CONFLICT DO NOTHING RETURNING и обрабатывай только победителя; цикл retry без jitter и без retry-условия — это herd, который долбит постоянные ошибки — используй full jitter и retry только на 5xx/таймауты; чтение in_progress должно коротко замкнуть на 409, никогда не вызывая провайдера снова; а dedup в inbox, который не завязывает бизнес-запись на dedup-вставку, декоративен — дубликат всё равно применяет эффект. Каждый фикс — одна-две строки, и каждая отсутствующая строка — это дублированный side effect в production.

Продолжить восхождение ↑Идемпотентность и retry: собери effectively-once платёжный путь
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.