Выполнение произвольного кода в фоновых заданиях

Опубликовал Роман Уничкин (unichkin) в раздел Программирование - Универсальные функции

Если надо быстро провести 100`000 документов...

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

Описание \ пример использования

Пусть исходно есть некий источник данных для их последующей обработки - РезультатЗапроса, или ТаблицаЗначений. Т.е. собрали запросом необходимые к проведению документы, или - поместили их в таблицу значений (в том случае если алгоритм выборки слишком сложный для того, чтобы обойтись только запросом). Для демонстрации возьму с потолка такую задачу: необходимо отобрать реализацию, и для клиента с ФИО = "Иванов Иван Иванович" все документы провести. Составим запрос:

Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|   РеализацияТоваровИУслуг.Ссылка,
|   РеализацияТоваровИУслуг.Клиент.ФИО КАК ФИОКлиента
|ИЗ
|   Документ.РеализацияТоваровИУслуг КАК РеализацияТоваровИУслуг
|";
РезультатЗапроса = Запрос.Выполнить();

... и реализуем код для обработки данных:

Выборка = РезультатЗапроса.Выбрать();    
Пока Выборка.Следующий() Цикл
    
    об = Выборка.Ссылка.ПолучитьОбъект();    
    Если Выборка.ФИОКлиента = "Иванов Иван Иванович" Тогда
        об.Записать(РежимЗаписиДокумента.Проведение);
    КонецЕсли;     
    
КонецЦикла;

Это не очень оптимальное решение, исключительно для демонстрации условие осталось внутри цикла - хотя конечно логичнее было бы сразу отобрать в запросе. Теперь запускаем этот код на выполнение в фоне. Распределим нагрузку: предположим что в нашей выборке 100`000 документов. Тогда распределяя это количество на 10 фоновых заданий получаем 10`000 документов на одно задание. Распределение выполняется функцией "ТаблицаФоновыхЗадач":

ТаблицаФоновыхЗадач = ФоновыеЗаданияСервер.ТаблицаФоновыхЗадач(РезультатЗапроса, 10, "Проведение документов");

После того, как таблица фоновых задач сформирована - необходимо подготовить код к передаче, параметром. Цикл по набору данных будет выполнен автоматически, поэтому понадобится только тело цикла. Этот кусок будет передан в метод ГК "Выполнить", его требуется преобразовать в строку.  После того, как параметры подготовлены, их остается передать в функцию "ВыполнитьВФоне". В итоге текущий пример примет такой вид:

Код = "
|    об = Выборка.Ссылка.ПолучитьОбъект();    
|    Если Выборка.ФИОКлиента = ""Иванов Иван Иванович"" Тогда
|        об.Записать(РежимЗаписиДокумента.Проведение);
|    КонецЕсли;";

ТаблицаФоновыхЗадач = ФоновыеЗаданияСервер.ТаблицаФоновыхЗадач(РезультатЗапроса, 10, "Проведение документов");
ФоновыеЗаданияСервер.ВыполнитьВФоне(ТаблицаФоновыхЗадач, Код);

Мониторить результат можно с помощью консоли инструментов разработчика (скриншот). Обратите внимание на колонку "Сообщения": при выполнении очередной итерации цикла по порции набора выполняется пустое "Сообщение пользователю". По количеству этих сообщений можно судить о количестве прошедших итераций, получаем своеобразный прогресс-бар: на скриншоте выделена строка фонового задания №5, и можно увидеть что из 10000 операции выполнено 4160.

Блок программного интерфейса

Код, расположенный ниже помещается в неглобальный общий модуль. Удобно для этого использовать "ФоновыеЗаданияСервер".

////////////////////////////////////////////////////////////////////////////////
// Выполнение произвольного кода в фоне

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

// Формирование таблицы порций фоновых задач, распределение строк источника пропорционально количеству фоновых заданий
//
//Параметры:
// Источник - РезультатЗапроса, ТаблицаЗначений
// КоличествоПотоков - Число
// Представление - Строка, описание задачи фонового задания
//
//Возвращаемое значение: 
// ТаблицаЗначений
//  *КлючПотока - УникальныйИдентификатор, ключ  фонового задания
//    *ПредставлениеПотока - Строка
//    *НаборДанных - ТаблицаЗначений, по структуре аналогична источнику - порция данных текущего потока
//
Функция ТаблицаФоновыхЗадач(Источник, КоличествоПотоков, Представление = "") ЭКСПОРТ
    
    // Формирование таблицы распределения по количеству фоновых задач
    ТаблицаФоновыхЗадач = Новый ТаблицаЗначений;
    ТаблицаФоновыхЗадач.Колонки.Добавить("КлючПотока"); 
    ТаблицаФоновыхЗадач.Колонки.Добавить("ПредставлениеПотока"); 
    ТаблицаФоновыхЗадач.Колонки.Добавить("НаборДанных");
    
    ТаблицаПорций = Новый ТаблицаЗначений; 
    Для каждого Колонка Из Источник.Колонки Цикл 
        ТаблицаПорций.Колонки.Добавить(Колонка.Имя, Колонка.ТипЗначения);
    КонецЦикла;
    
    Для Счетчик = 1 По КоличествоПотоков Цикл
        СтрокаТаблицы = ТаблицаФоновыхЗадач.Добавить();
        СтрокаТаблицы.КлючПотока = Новый УникальныйИдентификатор;
        СтрокаТаблицы.НаборДанных = ТаблицаПорций.Скопировать();
    КонецЦикла; 
    
    // Распределение записей выборки по порциям:
    ТекущийПоток = 0;  
    
    Если ТипЗнч(Источник) = Тип("ТаблицаЗначений") Тогда
        Для каждого Выборка Из Источник Цикл            
            СтрокаТаблицы = ТаблицаФоновыхЗадач[ТекущийПоток];
            
            НовыйНаборДанных = СтрокаТаблицы.НаборДанных.Добавить();
            ЗаполнитьЗначенияСвойств(НовыйНаборДанных, Выборка);
            
            ТекущийПоток = ТекущийПоток + 1;
            
            Если ТекущийПоток = КоличествоПотоков Тогда
                ТекущийПоток = 0; 
            КонецЕсли;                        
        КонецЦикла; 
        
    Иначе
        Выборка = Источник.Выбрать();
        Пока Выборка.Следующий() Цикл        
            СтрокаТаблицы = ТаблицаФоновыхЗадач[ТекущийПоток];
            
            НовыйНаборДанных = СтрокаТаблицы.НаборДанных.Добавить();
            ЗаполнитьЗначенияСвойств(НовыйНаборДанных, Выборка);
            
            ТекущийПоток = ТекущийПоток + 1;
            
            Если ТекущийПоток = КоличествоПотоков Тогда
                ТекущийПоток = 0; 
            КонецЕсли;        
        КонецЦикла;         
    КонецЕсли; 
    
    МассивУдаляемыхСтрок = Новый Массив(); 
    
    Счетчик = 0;
    Для каждого СтрокаТаблицы Из ТаблицаФоновыхЗадач Цикл 
        КоличествоЗаписейНабора = СтрокаТаблицы.НаборДанных.Количество();
        
        Если КоличествоЗаписейНабора = 0 Тогда
            МассивУдаляемыхСтрок.Добавить(СтрокаТаблицы);
            Продолжить;
        КонецЕсли; 
        
        СтрЗаписей = "("+ КоличествоЗаписейНабора +" записей)";
        
        Счетчик = Счетчик + 1; 
        НомерПотока = Формат(Счетчик, "ЧЦ=2; ЧВН=");
        
        Если ПустаяСтрока(Представление) Тогда
            СтрокаТаблицы.ПредставлениеПотока = "ФЗ №" + НомерПотока + ", ключ " + СтрокаТаблицы.КлючЗадачи + " " + СтрЗаписей;
        Иначе
            СтрокаТаблицы.ПредставлениеПотока = Представление + ", поток №" + НомерПотока + " " + СтрЗаписей + "."; 
        КонецЕсли; 
    КонецЦикла; 
    
    Для каждого СтрокаТаблицы Из МассивУдаляемыхСтрок Цикл
        ТаблицаФоновыхЗадач.Удалить(СтрокаТаблицы); 
    КонецЦикла; 
    
    Возврат ТаблицаФоновыхЗадач;
    
КонецФункции

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

См. также

Комментарии
1. aspirator 23 (aspirator23) 293 23.01.16 16:55 Сейчас в теме
Примерно также делал, только для контроля выполнения фоновых заданий использовал дополнительный регистр.
Позволяет наглядно выводить пользователю выполнение фоновых задач.
От ОжидатьЗавершения() отказался - не позволяет отражать процесс выполнения заданий.
С фоновыми заданиями кода получается много по сравнению с обычным последовательным выполнением, но оно того стоило.
Задачи которые выполнялись 40 минут, выполняются за 2-3 минуты. Для интерактивных операций то что нужно.
Да и для многих регламентных можно использовать.
2. Сергей Старых (tormozit) 4168 30.01.16 02:12 Сейчас в теме
пользуюсь консолью инструментов разработчика, но почему-то в файловой базе она пасанула
Не пробовал сообщить описание проблемы разработчику?
3. Роман Уничкин (unichkin) 346 30.01.16 16:01 Сейчас в теме
(2) tormozit, сообщу. Просто не придал этому большого значения. Благодаря твоей работе разработка стала на порядок проще, опишу конечно.
4. Дмитрий Солдатов (dimpson) 11 03.04.17 18:33 Сейчас в теме
Возникла идея: можно написать универсальное фоновое задание, которое будет выполнять код из какого-нибудь справочника аля "Справочник фоновых заданий", т.е. в этом справочнике можно прописать выполняемый код, параметры и даже расписание.
Одно универсальное фоновое задание будет пытаться выполниться каждые, допустим, 10 секунд и если одно из заданий из справочника попадет под расписание, то исполнится код через Выполнить()...
5. Роман Уничкин (unichkin) 346 03.04.17 23:42 Сейчас в теме
(4) Тогда уж лучше регламент, если меняем конфу. Сейчас на платформе 8.3.9.217 с этим определенные проблемы... Так что лучше регламент)
6. Алексей 1 (AlX0id) 04.04.17 15:39 Сейчас в теме
(4)
И этот справочник = "Дополнительные внешние обработки" из БСП ) И тут нужно не писать, а читать ИТС )
Пишешь код во внешней обработке, отлаживаешь ее, запихиваешь в дополнительные внешние и запускаешь по расписанию.
unichkin; +1 Ответить
7. kiruha Дронов (kiruha) 357 17.04.17 19:25 Сейчас в теме
При всем уважении - потенциальная дыра в безопасности.
8. Роман Уничкин (unichkin) 346 18.04.17 00:03 Сейчас в теме
(7) Это да. Зато удобно. Правда "дыра" прямо скажем неочевидная... Я думаю если злоумышленник получает доступ к базе на таком уровне - то вся база одна сплошная дыра.