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

Распределённые системы

Саги: чтение кода

Суть Читай реальные сниппеты саги — compensating action, шаг оркестратора и повторно вызванный handler, — предскажи сбой и выбери фикс с наибольшим рычагом.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

Баги саги в коде не выглядят как баги саги — они выглядят как обычный handler, который при retry списывает дважды, или как compensation, которая ждёт rollback, что никогда не случится. Прочитай каждый сниппет и выбери фикс, который сеньор сделал бы до релиза.

Цель

Потренируй чтение, которое делаешь в каждом ревью саги: заметь отсутствующий idempotency key, compensation, которая на деле rollback в маскировке, и шаг оркестратора, теряющий процесс на лету при краше.

Сниппет 1 — compensating action

// T2 уже закоммитилась: карта списана и записана строка платежа.
// C2 должна отменить это после падения более позднего шага.
func compensateCharge(ctx context.Context, orderID string) error {
    // удаляем строку платежа, чтобы выглядело, будто списания не было
    return db.Exec(ctx, "DELETE FROM payments WHERE order_id = $1", orderID)
}
Викторина

Почему эта compensation неверна и что она должна делать вместо этого?

Сниппет 2 — шаг оркестратора

# Оркестратор ведёт сагу вперёд, шаг за шагом, в памяти.
def run_order_saga(order):
    book_flight(order)          # T1
    book_hotel(order)           # T2
    try:
        charge_card(order)      # T3
    except StepFailed:
        cancel_hotel(order)     # C2
        cancel_flight(order)    # C1
    # прогресс живёт только в этом кадре вызова
Викторина

Процесс оркестратора падает сразу после возврата book_hotel, но до charge_card. В чём сбой и что его чинит?

Сниппет 3 — идемпотентный шаг

// Доставка at-least-once, так что этот handler может быть вызван
// больше одного раза для одного шага саги.
func handleChargeCard(msg ChargeMsg) error {
    amount := msg.Amount
    if err := gateway.Charge(msg.CustomerID, amount); err != nil {
        return err
    }
    return savePaymentRow(msg.OrderID, amount)
}
Викторина

При доставке at-least-once у этого handler реальный денежный баг. В чём он и каков минимальный фикс?

Сниппет 4 — semantic lock

-- Сага A начинает работать с заказом. Сага B может тронуть тот же
-- заказ одновременно, потому что между шагами саги ничего не заблокировано.
UPDATE orders SET status = 'PENDING_PAYMENT' WHERE id = $1;
-- ... позже выполняются шаги саги A, затем при успехе ...
UPDATE orders SET status = 'CONFIRMED' WHERE id = $1;
Викторина

Что здесь делает статус PENDING_PAYMENT и что должны делать другие саги, чтобы это работало?

Итог

Каждый дефект саги в этом наборе — известная форма: compensation, которая удаляет локальную запись вместо реального обратного действия; оркестратор, держащий прогресс только в памяти и теряющий процесс при краше; шаг, списывающий дважды, потому что at-least-once встретился с неидемпотентным handler; и поле статуса, работающее как блокировка, только если каждая сага его соблюдает. Читай на отсутствующий idempotency key, на compensation-в-маскировке-под-rollback и на неперсистентный шаг — там и живут баги саги.

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

Trademarks belong to their respective owners. Editorial reference only.