Как откатить транзакцию с фоновыми заданиями в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке высоконагруженных систем на платформе 1С:Предприятие мы часто сталкиваемся с необходимостью распараллелить тяжелые процессы для ускорения работы. Стандартный подход — использование фоновых заданий. Однако, если запуск фоновых заданий происходит внутри транзакции главного сеанса, возникает серьезная архитектурная проблема: как откатить изменения, сделанные фоновыми заданиями, если главная транзакция завершилась с ошибкой?

Давайте проанализируем ситуацию и разберем по шагам, почему возникает эта проблема и какие существуют надежные способы ее решения.

Суть проблемы и архитектурные ограничения 1С

Рассмотрим типичный упрощенный сценарий, описанный разработчиком:

  1. Мы начинаем транзакцию и создаем главный документ (например, реализацию или самописный документ).
  2. Для ускорения процесса мы запускаем пул фоновых заданий, которые параллельно создают сотни связанных объектов (номенклатуру, расходные ордера и т.д.).
  3. Фоновые задания помещают данные о созданных объектах в буфер (например, в регистр сведений).
  4. Главный сеанс читает буфер, заполняет табличную часть документа и пытается его записать.
  5. Если запись документа не удается по какой-либо причине, мы вызываем ОтменитьТранзакцию().

Что происходит на самом деле? Главный документ и записи регистра буфера из главного сеанса откатываются. Но сотни созданных объектов (номенклатура или ордера) остаются в базе данных! Почему? Выясним причину.

Дело в том, что фоновые задания в платформе 1С всегда запускаются в изолированных сеансах рабочего сервера. Каждый такой сеанс устанавливает собственное соединение с СУБД и имеет собственный, полностью независимый транзакционный контекст. Платформа 1С не поддерживает распределенные транзакции (DTC) между разными сеансами. Как только фоновое задание выполняет метод Записать() для объекта и завершает свою локальную работу, данные физически фиксируются в СУБД. Откат транзакции в главном сеансе на них никак не влияет.

Посмотрим на несколько архитектурных подходов, которые помогут нам решить эту задачу элегантно и надежно.

Решение 1. Паттерн "Сага" (Компенсирующие транзакции)

Поскольку глобальная транзакция невозможна, мы можем применить подход из мира распределенных систем — компенсирующие действия. Если процесс падает в исключение, мы должны явно (вручную) удалить то, что успели создать фоновые задания.

Разберем алгоритм:

  1. В процессе работы фоновые задания складывают ссылки на созданные объекты в специальный регистр сведений (лог созданных объектов), привязанный к уникальному идентификатору (GUID) текущего главного процесса.
  2. В главном сеансе мы оборачиваем запись документа в конструкцию Попытка ... Исключение.
  3. Если возникает исключение, мы читаем наш лог и явно удаляем все созданные объекты.

Пример реализации в коде:


НачатьТранзакцию();
Попытка
    // ... запуск фоновых заданий, ожидание их завершения ...
    
    // Попытка записи главного документа
    ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
    
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    
    // Выполняем компенсирующие действия (удаление мусора)
    УдалитьСозданныеФоновымиЗаданиямиОбъекты(ИДПроцесса);
    
    ВызватьИсключение;
КонецПопытки;

В процедуре УдалитьСозданныеФоновымиЗаданиямиОбъекты можно запустить отдельное асинхронное задание, которое прочитает лог по ИДПроцесса, вызовет метод УдалитьОбъект() для каждого элемента и очистит сам лог, чтобы не задерживать пользователя.

Решение 2. Разделение "Вычислений" и "Записи" (Временное хранилище)

Часто фоновые задания нужны не столько для параллельной записи в БД, сколько для распараллеливания тяжелых вычислений (распределение товаров по ордерам, сложные запросы к остаткам). Изменим подход: пусть фоновые задания вообще ничего не пишут в базу!

Рассмотрим, как это работает:

  1. Фоновые задания выполняют расчеты, формируют структуру данных (или Таблицу Значений) с подготовленными реквизитами будущих объектов.
  2. Результат расчетов помещается во временное хранилище с помощью метода ПоместитьВоВременноеХранилище().
  3. Главный сеанс дожидается завершения фоновых заданий, получает данные из хранилища методом ПолучитьИзВременногоХранилища().
  4. Главный сеанс в одной своей транзакции создает объекты, заполняет их из структур, записывает и затем записывает главный документ. Для ускорения записи множества объектов можно использовать ОбменДанными.Загрузка = Истина.

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

Решение 3. Предварительная аллокация GUID (УстановитьСсылкуНового)

Если мы заранее можем определить, сколько объектов нам потребуется создать, мы можем изменить последовательность действий.

Разберем архитектуру:

  1. Главный сеанс рассчитывает необходимое количество подчиненных объектов.
  2. С помощью метода Документы.ИмяДокумента.ПолучитьСсылку() или генерации нового УИД, мы заранее резервируем ссылки для будущих объектов.
  3. Мы заполняем табличную часть главного документа этими еще не существующими ссылками и успешно записываем главный документ в транзакции.
  4. Только если запись прошла успешно (все проверки пройдены), мы передаем массив зарезервированных ссылок (GUID-ов) в фоновые задания.
  5. Фоновые задания создают объекты, используя метод УстановитьСсылкуНового(), чтобы объект записался именно с тем GUID, который мы указали в главном документе.

Если на этапе 3 главная транзакция отвалится, зарезервированные GUID-ы просто "сгорят", и в базе не останется никакого мусора, так как фоновые задания даже не будут запущены.

Решение 4. Архитектура через статусы (Отложенное проведение)

В сложных системах с множеством документов (например, пулы из 500 заказов) попытка уместить весь процесс в одну синхронную транзакцию почти всегда приводит к блокировкам и таймаутам. Проанализируем более надежный подход с использованием состояний (State Machine).

  1. Главный документ создается и записывается в статусе "Черновик" (или "К обработке"). Запись без проведения выполняется быстро и почти никогда не падает.
  2. Запускается пул фоновых заданий. В них передается ссылка на этот "Черновик".
  3. Фоновые задания создают ордера, записывают их в базу с привязкой к нашему документу-черновику и отмечают статус своего завершения (например, в регистре сведений).
  4. Отдельное регламентное задание (или обработчик ожидания на форме) проверяет этот регистр. Как только 100% фоновых заданий по документу завершены успешно, статус документа автоматически меняется на "Готов" и выполняется его штатное проведение.
  5. Если какое-то фоновое задание завершилось с ошибкой, статус документа меняется на "Ошибка обработки". Пользователь видит это, исправляет проблему, либо нажимает кнопку "Отменить операцию", которая корректно удаляет "Черновик" и все привязанные к нему ордера.

Такой подход делает систему отказоустойчивой, прозрачной для пользователя (он видит прогресс выполнения) и исключает длительные блокируемые транзакции.

Итоги

Мы подробно разобрали, почему стандартный метод ОтменитьТранзакцию() не работает для фоновых заданий. Чтобы избежать появления "мусорных" данных в базе, необходимо менять архитектурный подход. В зависимости от вашей задачи, выберите наиболее подходящий паттерн: разделяйте расчеты и запись, используйте компенсирующие транзакции для явного удаления мусора, или переходите на асинхронную модель работы через статусы документов.

← На главную