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

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

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

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

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

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

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

Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл

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

КонецЦикла;

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

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

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

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

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

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

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

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

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

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

Если ЦиклПоНаборуДанных Тогда

МассивЗарегистрированныхОшибок = Новый Массив;

Для каждого Выборка Из ТаблицаНабораДанных Цикл

Попытка
Выполнить(Код);

Исключение

ТекстОшибки = ОписаниеОшибки();
Если МассивЗарегистрированныхОшибок.Найти(ТекстОшибки) = Неопределено Тогда
ЗаписьЖурналаРегистрации("ФоновоеВыполнениеКода"
, УровеньЖурналаРегистрации.Ошибка
,
,
, ТекстОшибки);

МассивЗарегистрированныхОшибок.Добавить(ТекстОшибки);
КонецЕсли;

Если ПрерыватьПоИсключению Тогда
Прервать;
КонецЕсли;

КонецПопытки;

Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "";
Сообщение.Сообщить();
КонецЦикла;
Иначе
Попытка
Выполнить(Код);

Исключение

ЗаписьЖурналаРегистрации("ФоновоеВыполнениеКода"
, УровеньЖурналаРегистрации.Ошибка
,
,
, ОписаниеОшибки());
КонецПопытки;

КонецЕсли;

ТаблицаНабораДанных = Неопределено;

КонецПроцедуры

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

// Формирование таблицы распределения по количеству фоновых задач
ТаблицаФоновыхЗадач = Новый ТаблицаЗначений;
ТаблицаФоновыхЗадач.Колонки.Добавить("КлючПотока");
ТаблицаФоновыхЗадач.Колонки.Добавить("ПредставлениеПотока");
ТаблицаФоновыхЗадач.Колонки.Добавить("НаборДанных");

ТаблицаПорций = Новый ТаблицаЗначений;
Для каждого Колонка Из Источник.Колонки Цикл
ТаблицаПорций.Колонки.Добавить(Колонка.Имя, Колонка.ТипЗначения);
КонецЦикла;

Для Счетчик = 1 По КоличествоПотоков Цикл
СтрокаТаблицы = ТаблицаФоновыхЗадач.Добавить();
СтрокаТаблицы.КлючПотока = Новый УникальныйИдентификатор;
СтрокаТаблицы.НаборДанных = ТаблицаПорций.Скопировать();
КонецЦикла;

// Распределение записей выборки по порциям:
ТекущийПоток = 0;

Если ТипЗнч(Источник) = Тип("ТаблицаЗначений") Тогда
Для каждого Выборка Из Источник Цикл
СтрокаТаблицы = ТаблицаФоновыхЗадач[ТекущийПоток];

НовыйНаборДанных = СтрокаТаблицы.НаборДанных.Добавить();
ЗаполнитьЗначенияСвойств(НовыйНаборДанных, Выборка);

ТекущийПоток = ТекущийПоток + 1;

Если ТекущийПоток = КоличествоПотоков Тогда
ТекущийПоток = 0;
КонецЕсли;
КонецЦикла;

Иначе
Выборка = Источник.Выбрать();
Пока Выборка.Следующий() Цикл
СтрокаТаблицы = ТаблицаФоновыхЗадач[ТекущийПоток];

НовыйНаборДанных = СтрокаТаблицы.НаборДанных.Добавить();
ЗаполнитьЗначенияСвойств(НовыйНаборДанных, Выборка);

ТекущийПоток = ТекущийПоток + 1;

Если ТекущийПоток = КоличествоПотоков Тогда
ТекущийПоток = 0;
КонецЕсли;
КонецЦикла;
КонецЕсли;

МассивУдаляемыхСтрок = Новый Массив();

Счетчик = 0;
Для каждого СтрокаТаблицы Из ТаблицаФоновыхЗадач Цикл
КоличествоЗаписейНабора = СтрокаТаблицы.НаборДанных.Количество();

Если КоличествоЗаписейНабора = 0 Тогда
МассивУдаляемыхСтрок.Добавить(СтрокаТаблицы);
Продолжить;
КонецЕсли;

СтрЗаписей = "("+ КоличествоЗаписейНабора +" записей)";

Счетчик = Счетчик + 1;
НомерПотока = Формат(Счетчик, "ЧЦ=2; ЧВН=");

Если ПустаяСтрока(Представление) Тогда
СтрокаТаблицы.ПредставлениеПотока = "ФЗ №" + НомерПотока + ", ключ " + СтрокаТаблицы.КлючЗадачи + " " + СтрЗаписей;
Иначе
СтрокаТаблицы.ПредставлениеПотока = Представление + ", поток №" + НомерПотока + " " + СтрЗаписей + ".";
КонецЕсли;
КонецЦикла;

Для каждого СтрокаТаблицы Из МассивУдаляемыхСтрок Цикл
ТаблицаФоновыхЗадач.Удалить(СтрокаТаблицы);
КонецЦикла;

Возврат ТаблицаФоновыхЗадач;

КонецФункции

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

ИмяМетода = "ФоновыеЗаданияСервер.ВыполнитьКодПотокаПоНаборуДанных";

МассивФоновыхЗаданий = Новый Массив();

Для каждого СтрокаТаблицы Из ТаблицаФоновыхЗадач Цикл

МассивПараметров = Новый Массив;
МассивПараметров.Добавить(Код);
МассивПараметров.Добавить(СтрокаТаблицы.НаборДанных);
МассивПараметров.Добавить(ЦиклПоНаборуДанных);
МассивПараметров.Добавить(ПрерыватьПоИсключению);

ФЗ = ФоновыеЗадания.Выполнить(
ИмяМетода
, МассивПараметров
, СтрокаТаблицы.КлючПотока
, СтрокаТаблицы.ПредставлениеПотока);

МассивФоновыхЗаданий.Добавить(ФЗ);
КонецЦикла;

Если ОжидатьЗавершения Тогда
ФоновыеЗадания.ОжидатьЗавершения(МассивФоновыхЗаданий);
КонецЕсли;

Возврат МассивФоновыхЗаданий;

КонецФункции

13 Comments

  1. aspirator23

    Примерно также делал, только для контроля выполнения фоновых заданий использовал дополнительный регистр.

    Позволяет наглядно выводить пользователю выполнение фоновых задач.

    От ОжидатьЗавершения() отказался — не позволяет отражать процесс выполнения заданий.

    С фоновыми заданиями кода получается много по сравнению с обычным последовательным выполнением, но оно того стоило.

    Задачи которые выполнялись 40 минут, выполняются за 2-3 минуты. Для интерактивных операций то что нужно.

    Да и для многих регламентных можно использовать.

    Reply
  2. tormozit
    пользуюсь консолью инструментов разработчика, но почему-то в файловой базе она пасанула

    Не пробовал сообщить описание проблемы разработчику?

    Reply
  3. unichkin

    (2) tormozit, сообщу. Просто не придал этому большого значения. Благодаря твоей работе разработка стала на порядок проще, опишу конечно.

    Reply
  4. dimpson

    Возникла идея: можно написать универсальное фоновое задание, которое будет выполнять код из какого-нибудь справочника аля «Справочник фоновых заданий», т.е. в этом справочнике можно прописать выполняемый код, параметры и даже расписание.

    Одно универсальное фоновое задание будет пытаться выполниться каждые, допустим, 10 секунд и если одно из заданий из справочника попадет под расписание, то исполнится код через Выполнить()…

    Reply
  5. unichkin

    (4) Тогда уж лучше регламент, если меняем конфу. Сейчас на платформе 8.3.9.217 с этим определенные проблемы… Так что лучше регламент)

    Reply
  6. AlX0id

    (4)

    И этот справочник = «Дополнительные внешние обработки» из БСП ) И тут нужно не писать, а читать ИТС )

    Пишешь код во внешней обработке, отлаживаешь ее, запихиваешь в дополнительные внешние и запускаешь по расписанию.

    Reply
  7. kiruha

    При всем уважении — потенциальная дыра в безопасности.

    Reply
  8. unichkin

    (7) Это да. Зато удобно. Правда «дыра» прямо скажем неочевидная… Я думаю если злоумышленник получает доступ к базе на таком уровне — то вся база одна сплошная дыра.

    Reply
  9. unoDosTres

    практика известная.

    я бы еще добавил в эту процедуру ВыполнитьКодПотокаПоНаборуДанных один или несколько не значимых параметров.

    практическое применение этому, ну например: передаем в фоновое задание в качестве параметра

    АдресВХранилище = ПоместитьВоВременноеХранилище(Неопределено);

    а в параметре код пишем в этот же параметр нужные данные чтобы потом отловить после выполенения фонового задания через «ПолучитьИзВременногоХранилища».

    Reply
  10. Frogger1971

    автор даже не удосужился полностью раскрыть тему

    Многопоточность как способ ускорения некоторых процедур

    Reply
  11. unichkin

    (9) Не понял

    (10) У автора не было такой нужды. О том что такое многопоточность и с чем ее есть и так найдется 100500 статей. Я публикую только свой подход к решению задачи.

    Reply
  12. unoDosTres

    (11), ну тема называется «Выполнение произвольного кода в фоновых заданиях» если на бы например я хотел получить результат выполнения фонового задания на клиенте я бы этого не смог сделать, ну как мне видится, то что предложил в (9) я это решает

    Reply
  13. unichkin

    (12) У метода «ВыполнитьВФоне» второй параметр — ОжидатьЗавершения.

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *