Ошибка на пятом шаге: как гарантировать откат всех изменений
Проблема: Надёжное выполнение последовательных операций в 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 с компенсирующими операциями оказался идеальным решением для сценариев, где транзакции не подходят. Подход даёт уверенность, что при любом сбое система останется в согласованном состоянии.
Для тех, кто столкнулся с похожими проблемами — рекомендую рассмотреть этот паттерн!