Ошибка на пятом шаге: как гарантировать откат всех изменений

Проблема: Надёжное выполнение последовательных операций в Supabase с возможностью отката

Недавно столкнулся с классической проблемой в distributed systems - необходимостью выполнить несколько последовательных операций в БД (Supabase), где при ошибке на любом этапе нужно откатить все предыдущие изменения.

Конкретная задача: Начисление баллов пользователю с:

  • Обновлением баланса
  • Изменением статуса билета
  • Созданием активности
  • Записью в лог
  • Отправкой email

Проблемы традиционного подхода:

  • Если ошибка происходит после начисления баллов, но до записи лога - баланс пользователя остаётся некорректным
  • Ручные откаты сложны и подвержены ошибкам
  • Возможны гонки данных при параллельных запросах

Рассматриваемые решения: 1. Транзакции - не подходят, так как операции затрагивают разные сервисы (БД, email) 2. Outbox Pattern - избыточен для простых сценариев 3. Saga Pattern — то что нужно! С компенсирующими операциями

Моё решение: Saga Pattern с Compensation Manager

// Основные компоненты решения:

// 1. Compensation Manager - централизованное управление откатами const cm = new CompensationManager();

// 2. Защита от гонок через updated_at .eq('updated_at', originalUpdated as string)

// 3. Строгая структура rollback с обратным порядком выполнения

Ключевые особенности реализации: ✅ Вынос компенсаторов в отдельный класс - чистая архитектура ✅ Защита от гонок - проверка updated_at предотвращает параллельное изменение ✅ Строгий порядок отката - компенсаторы выполняются в обратном порядке LIFO ✅ Идемпотентность - компенсаторы безопасны для повторного выполнения ✅ Сегрегация ответственности - email не откатывается (side effect)

Пример компенсирующих операций: // Начисление баллов → восстановление исходного значения cm.add('restore_points', async () => { await supabase .from('users') .update({ points: originalPoints, updated_at: originalUpdated }) .eq('user_id', userId); });

// Создание активности → удаление активности cm.add('delete_activity', async () => { if (activity.activityId) { await supabase.from('user_activities').delete().eq('id', activity.activityId); } });

Преимущества подхода: 1. Надёжность - гарантированная согласованность данных 2. Поддержка - чёткая структура упрощает развитие 3. Безопасность - защита от race conditions 4. Гибкость - легко добавлять новые шаги

Итог: Saga Pattern с компенсирующими операциями оказался идеальным решением для сценариев, где транзакции не подходят. Подход даёт уверенность, что при любом сбое система останется в согласованном состоянии.

Для тех, кто столкнулся с похожими проблемами — рекомендую рассмотреть этот паттерн!

Ошибка на пятом шаге: как гарантировать откат всех изменений | Сетка — социальная сеть от hh.ru