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

Если надо быстро провести 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 не будет опубликован. Обязательные поля помечены *