Сага помилок у багатокрокових процесах: як відкотити часткову невдачу

багатокрокові процесивідкатsaga

Пояснюємо на простому прикладі, чому звичайний повтор запуску може створити дублікати, що таке компенсаційні кроки та як підготувати процес до безпечного відновлення після збою

Почнімо з аварії, яку легко впізнати

Уявіть процес оформлення замовлення. Система створює запис у базі, резервує товар, списує оплату, ставить задачу на доставку й надсилає лист клієнту. На папері це виглядає як пряма лінія: крок 1, крок 2, крок 3, крок 4.

А тепер реальний день. Запис у базі створився. Резервування товару пройшло. Платіжний сервіс довго не відповідав, і наш процес отримав таймаут. Автоматичний повтор запускає проблемний фрагмент ще раз, але зовнішній сервіс міг уже прийняти платіж. У результаті в системі може бути один запис замовлення, два запити на оплату, одна черга доставки й незрозумілий статус для підтримки.

Це називається частковою помилкою: частина роботи вже відбулася, але процес не дійшов до нормального фіналу. Головна небезпека тут не в самому збої. Небезпека в тому, що повторний запуск без правил може зробити ситуацію гіршою.

Чому простий повтор не рятує

Повтор запуску корисний, коли крок нічого не змінив або вміє безпечно розпізнати дубль. Наприклад, якщо ми читаємо дані з API й отримали тимчасову помилку мережі, спробувати ще раз нормально.

Інша історія — кроки з побічними ефектами. Побічний ефект означає, що після кроку щось змінилося у світі: списані гроші, створене відправлення, надісланий лист, зарезервований товар, записана подія в чергу. Такі дії не можна бездумно повторювати, бо кожен повтор може створити новий наслідок.

Тому для кожного кроку треба заздалегідь відповісти на три питання:

  • чи можна цей крок повторити без дубля;
  • чи можна його компенсувати, якщо наступний крок упаде;
  • чи потрібна ручна перевірка, бо автоматично відкотити дію небезпечно.

Що таке saga простими словами

Saga — це спосіб мислити про довгий процес не як про одну велику транзакцію, а як про ланцюжок маленьких дій. Для кожної важливої дії ми описуємо зворотну дію.

Приклад:

  • зарезервували товар — можемо зняти резерв;
  • створили чернетку доставки — можемо скасувати чернетку;
  • списали оплату — можемо запустити повернення або поставити задачу фінансам;
  • надіслали лист — не можемо «забрати» лист, але можемо не надсилати другий і додати пояснення в наступне повідомлення.

Важлива деталь: відкат іде у зворотному порядку. Якщо процес успішно виконав кроки 1, 2 і 3, а на кроці 4 впав, то спершу компенсуємо крок 3, потім 2, потім 1. Інакше легко скасувати основу, на якій ще тримається наступний крок.

Дані, без яких відкат перетворюється на вгадування

Компенсаційний крок має знати, що саме він скасовує. Недостатньо записати «платіж створено» або «доставка запущена». Потрібні конкретні маркери:

  • operation_id — спільний ідентифікатор усього запуску;
  • step_id — назва кроку, який виконувався;
  • зовнішній id: номер платежу, доставки, резервування або задачі;
  • статус кроку: почато, виконано, невідомо, відкотано, потребує ручної перевірки;
  • час останньої спроби й кількість повторів;
  • коротка причина помилки.

Ці дані потрібні не для краси в логах. Вони відповідають на практичне питання: «Чи маємо ми право зараз відкотити цей крок, і якщо так, то який саме зовнішній об’єкт чіпати?»

Як спроєктувати крок до написання коду

Перед реалізацією корисно зробити просту таблицю. Для кожного кроку запишіть:

  • що крок робить у нормальному напрямку;
  • який стан або зовнішній сервіс він змінює;
  • які дані треба зберегти після успіху;
  • що буде компенсаційною дією;
  • коли компенсацію запускати не можна;
  • чи підтримує крок ключ ідемпотентності.

Ключ ідемпотентності — це стабільний ідентифікатор для повторів. Якщо платіжний сервіс отримує два однакові запити з тим самим ключем, він має зрозуміти: це не два платежі, а повтор однієї операції. Не кожен сервіс так уміє, тому це треба перевіряти окремо.

Правила, які зменшують хаос

Перше правило: один процес має одного власника відкату. Якщо два воркери одночасно вирішать «полагодити» той самий запуск, вони можуть створити нову гонку.

Друге правило: відсутність результату кроку не означає, що крок не відбувся. Таймаут після зовнішнього виклику часто означає «ми не знаємо». Перед відкатом треба перевірити реальний стан у зовнішньому сервісі або перевести процес у ручну чергу.

Третє правило: кількість повторів має межу. Нескінченний повтор може забити чергу, створити шум в алертах і приховати справжню причину.

Четверте правило: помилка відкату — це окрема подія, а не той самий збій. Її треба логувати й показувати окремо, бо після невдалого відкату система часто перебуває в найризикованішому стані.

Коли повторювати, коли відкочувати, коли зупиняти

Орієнтир простий:

  • Повторити крок — якщо він не має побічного ефекту або гарантовано ідемпотентний.
  • Запустити компенсацію — якщо крок уже змінив стан, а для нього є перевірений зворотний шлях.
  • Зупинити й передати людині — якщо наслідок незворотний, фінансово або юридично чутливий, або стан зовнішнього сервісу невідомий.

Наприклад, повторити читання профілю користувача безпечно. Повторити списання коштів без ключа ідемпотентності — ні. Скасувати чернетку доставки можна автоматично, якщо ми маємо її id. А от із уже відправленим листом треба працювати інакше: не «відкотити», а не допустити наступних дублювань і правильно пояснити стан клієнту.

Що тестувати до production

Звичайний успішний сценарій майже нічого не доводить. Для таких процесів потрібні тести часткових невдач:

  • процес падає після кожного кроку окремо;
  • процес падає після зовнішнього виклику, але до запису результату в БД;
  • зовнішній сервіс повертає таймаут, хоча дію міг виконати;
  • повторний запуск приходить із тим самим operation_id;
  • два воркери одночасно беруть один і той самий запуск;
  • компенсаційний крок теж падає.

Для кожного тесту треба описати очікуваний фінальний стан: що залишилось активним, що компенсовано, що чекає ручної перевірки, який алерт має з’явитися.

Де тут Cloudflare Workflows

Cloudflare описав rollback для Workflows як платформену можливість, але сама ідея ширша за одного провайдера. Вона корисна всюди, де є довгі процеси з кількома станами: черги, фонові воркери, інтеграції з оплатами, деплой-пайплайни, обробка замовлень.

Суть не в назві інструмента. Суть у дисципліні: кожен крок має явний стан, збережений результат, правило повтору й правило компенсації. Тоді збій перестає бути нічним ручним розслідуванням і стає керованим сценарієм.

Джерела

Короткий чеклист

  • Розбийте процес на кроки й для кожного запишіть, який стан він змінює.
  • Для кожної зовнішньої дії додайте компенсаційний крок або чесно позначте її як ручне відновлення.
  • Зберігайте результат кожного критичного кроку до переходу далі.
  • Перевірте сценарії: збій після кроку, повторний запуск, таймаут і падіння самого відкату.

Спланувати відновлення багатокрокового процесу після часткової невдачі

Твоя задача — допомогти команді підготувати план безпечного відновлення для багатокрокового процесу. Вхідні дані: 1) Список кроків процесу (до 20), для кожного — стабільний ідентифікатор. 2) Для кожного кроку: що він змінює (БД, зовнішній API, черга, email, платіж, резервування). 3) Чи можна безпечно повторювати зовнішній виклик з тим самим ключем ідемпотентності (так/ні/невідомо). 4) Де зараз зберігається стан процесу: БД, key-value сховище, таблиця jobs, журнал подій або state machine. 5) Які засоби спостереження є: логи, метрики, трасування, черга помилок, алерти. Сформуй на виході: - Таблицю кроків: ідентифікатор, основна дія, зворотна дія, умова для відкату, які дані треба зберегти, ознаки успіху й помилки. - Порядок відкату у зворотному напрямку: від останнього успішного кроку до першого. - Правило на випадок, коли сам відкат падає: що повторювати, що ставити в ручну чергу, кому сигналити. - Матрицю рішень: коли достатньо повторити крок, коли запускати відкат, коли зупиняти процес для ручної перевірки. - Набір тестів часткової невдачі: - збій після кожного кроку; - аварійне завершення перед записом стану; - таймаут під час зовнішнього виклику; - повторний запуск після часткового успіху; - два паралельні запуски з одним operation_id. - Чекліст готовності до production: мінімум 8 пунктів, окремо для коду й для операційної підтримки. Пиши конкретно: правила, таблиці, приклади станів. Не обмежуйся загальною порадою «перевірте документацію».