При работе с высоконагруженными системами часто возникает необходимость запустить несколько фоновых заданий одновременно и дождаться их завершения. Типичный пример: загрузка больших объемов данных, разбитая на несколько потоков. Однако стандартный механизм ожидания часто преподносит сюрпризы: если одно из заданий «падает» с ошибкой, разработчик теряет контроль над остальными. В этой статье мы разберем, почему старые методы работают именно так, и как правильно организовать мониторинг фоновых задач в современных версиях платформы 1С.
Рассмотрим стандартную ситуацию. Программист запускает массив заданий и использует метод ОжидатьЗавершения() объекта ФоновыеЗадания. Особенность этого метода заключается в его жесткой логике: он ожидает успешного завершения всех заданий из переданного списка. Если хотя бы одно задание завершается аварийно, метод немедленно генерирует исключение.
Проанализируем последствия такого поведения:
Исключение.Исключение, так как ошибочное задание все еще числится в списке и имеет статус ЗавершеноАварийно.Выясним, как обойти это ограничение на разных версиях платформы.
Начиная с версии 8.3.13, разработчики 1С реализовали более гибкий механизм. Вместо того чтобы ждать всех сразу и падать при первой ошибке, мы можем использовать метод ОжидатьЗавершенияВыполнения(). Он возвращает объект того задания, которое завершилось первым (независимо от результата — успешно или аварийно).
Разберем по шагам правильный алгоритм работы с этим методом:
// Создаем массив для хранения запущенных заданий
СписокЗаданий = Новый Массив;
СписокЗаданий.Добавить(ФоновыеЗадания.Выполнить("МойМодуль.ДлительнаяОперация", Параметры1));
СписокЗаданий.Добавить(ФоновыеЗадания.Выполнить("МойМодуль.ДлительнаяОперация", Параметры2));
// Цикл ожидания, пока массив не опустеет
Пока СписокЗаданий.Количество() > 0 Цикл
// Метод возвращает ОДНО завершившееся задание
ЗавершившеесяЗадание = ФоновыеЗадания.ОжидатьЗавершенияВыполнения(СписокЗаданий);
Если ЗавершившеесяЗадание <> Неопределено Тогда
// Проверяем состояние конкретного задания
Если ЗавершившеесяЗадание.Состояние = СостояниеФоновогоЗадания.ЗавершеноАварийно Тогда
ОписаниеОшибки = ЗавершившеесяЗадание.ИнформацияОбОшибке.Описание;
ЗаписатьЛогОшибки(ОписаниеОшибки);
ИначеЕсли ЗавершившеесяЗадание.Состояние = СостояниеФоновогоЗадания.Завершено Тогда
// Обработка успеха
КонецЕсли;
// Обязательно удаляем завершенное задание из списка ожидания
Индекс = СписокЗаданий.Найти(ЗавершившеесяЗадание);
Если Индекс <> Неопределено Тогда
СписокЗаданий.Удалить(Индекс);
КонецЕсли;
КонецЕсли;
КонецЦикла;
Этот подход позволяет нам обрабатывать ошибки каждого потока индивидуально, не прерывая ожидание остальных задач. Обратите внимание, что метод ОжидатьЗавершения() теперь официально считается устаревшим.
Если у вас нет возможности обновить платформу, придется использовать конструкцию с Попытка...Исключение внутри цикла. Главная хитрость здесь заключается в том, что после возникновения ошибки нужно программно «вычистить» упавшее задание из списка и продолжить ожидание.
Посмотрим на пример реализации такого обхода:
УбитыеЗадания = Новый Массив;
Пока лМассивЗаданий.Количество() > 0 Цикл
Попытка
// Пытаемся ждать все оставшиеся задания
ФоновыеЗадания.ОжидатьЗавершения(лМассивЗаданий);
// Если дошли сюда — все задания завершились успешно
лМассивЗаданий.Очистить();
Исключение
// Кто-то свалился. Нужно найти виновника.
Для Каждого ТекущееЗадание Из лМассивЗаданий Цикл
// ВАЖНО: Перечитываем состояние задания из базы!
ОбъектЗадания = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(ТекущееЗадание.УникальныйИдентификатор);
Если ОбъектЗадания.Состояние = СостояниеФоновогоЗадания.ЗавершеноАварийно
Или ОбъектЗадания.Состояние = СостояниеФоновогоЗадания.Отменено Тогда
// Сообщаем об ошибке
Если ОбъектЗадания.ИнформацияОбОшибке <> Неопределено Тогда
ЗаписьЖурналаРегистрации("ФоновыеЗадания", УровеньЖурналаРегистрации.Ошибка, , ,
ОбъектЗадания.ИнформацияОбОшибке.Описание);
КонецЕсли;
УбитыеЗадания.Добавить(ТекущееЗадание);
КонецЕсли;
КонецЦикла;
// Удаляем из рабочего списка те задания, что уже завершились с ошибкой
Для Каждого Стр Из УбитыеЗадания Цикл
Индекс = лМассивЗаданий.Найти(Стр);
Если Индекс <> Неопределено Тогда
лМассивЗаданий.Удалить(Индекс);
КонецЕсли;
КонецЦикла;
УбитыеЗадания.Очистить();
КонецПопытки;
КонецЦикла;
Рассмотрим важный технический нюанс, который часто сбивает с толку начинающих разработчиков. Если вы получили объект ФоновоеЗадание в переменную, его свойство Состояние не будет обновляться в реальном времени. Даже если задание уже завершилось в консоли кластера, в коде вы можете видеть статус Выполняется.
Чтобы гарантированно получить актуальные данные, мы должны использовать метод ФоновыеЗадания.НайтиПоУникальномуИдентификатору(). Это действие заставляет платформу обратиться к менеджеру заданий и получить свежую информацию из базы данных сервера 1С. Только после этого проверка СостояниеФоновогоЗадания.ЗавершеноАварийно будет иметь смысл.
Проанализируем ситуацию, когда задания выполняются очень долго. Держать открытым сеанс пользователя и ждать завершения на сервере через ОжидатьЗавершения() — плохая практика. Сетевые сбои или таймауты веб-сервера могут прервать ожидание.
В таких случаях рекомендуется:
СообщитьПрогресс() внутри фонового задания, чтобы пользователь видел, что процесс не завис.rphost.Бывают ситуации, когда свойство ИнформацияОбОшибке оказывается пустым, хотя задание завершилось аварийно. Это происходит при критических сбоях процесса или принудительном завершении задания администратором. В таких случаях выяснить причину поможет только настройка Технологического Журнала (ТЖ) с отбором по событию EXCP и свойству JobActualID.
Подведем итог: использование современного метода ОжидатьЗавершенияВыполнения() в сочетании с корректным перечитыванием состояния заданий по их идентификатору позволяет создать надежную и отказоустойчивую систему параллельной обработки данных в 1С.