INFOSTART EVENT 2018 EDUCATION

Второй тур голосования за доклады.
Окончание 5 сентября.

Ткаченко Денис | Конструктор | ОАО "РЖД"

«Корпоративные IT отделы на Дальнем Востоке обречены. Обречены на изменения»

Трансформируйся или умри, это Неизбежность корпоративного АйТи отдела. Модные слова про цифровизацию и клиентоориентированность спасут ненадолго, на срок провального проекта. Изменениям должна быть подвергнута система управления IT. Потому что IT изменилось и мы изменились. Эта отрасль уже не хобби энтузиастов и калька с учебников по управлению прошлого века не работает. Поднимаем голову от интриг и смотрим что случилось с кадровым рынком, если еще не посмотрели. Ну и где мы будем брать сотрудников в штат? А через пять лет? Из институтов? Готовим сами?, окей. Создали систему подготовки, мотивации и развития своих кадров? Эта система убъёт Ваш IT отдел. Отдали разработку и поддержку на аутсорс. Вы же понимаете, что это временно, их убьют те же проблемы. В общем нужно учиться управлять по другому, управлять не управляя. Нужно ли быть специалистом, чтобы управлять специалистами? Несомненно! это и есть ключевой момент. Заманчиво выглядит растворить систему менеджмента, в операционных процессах не увеличивая затраты? Очень. Формулируем Проблему. Рисуем Решение. Намечаем Шаги. Строим Систему.

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

Программирование - Универсальные функции

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

34

См. также

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

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

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

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


а в параметре код пишем в этот же параметр нужные данные чтобы потом отловить после выполенения фонового задания через "ПолучитьИзВременногоХранилища".
11. unichkin 993 23.06.17 21:33 Сейчас в теме
(9) Не понял
(10) У автора не было такой нужды. О том что такое многопоточность и с чем ее есть и так найдется 100500 статей. Я публикую только свой подход к решению задачи.
12. unoDosTres 25.06.17 14:37 Сейчас в теме
(11), ну тема называется "Выполнение произвольного кода в фоновых заданиях" если на бы например я хотел получить результат выполнения фонового задания на клиенте я бы этого не смог сделать, ну как мне видится, то что предложил в (9) я это решает
13. unichkin 993 25.06.17 18:58 Сейчас в теме
(12) У метода "ВыполнитьВФоне" второй параметр - ОжидатьЗавершения.
10. Frogger1971 23.06.17 12:28 Сейчас в теме
Оставьте свое сообщение