gifts2017

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

Опубликовал Роман Уничкин (unichkin) в раздел Программирование - Инструментарий

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

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

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

Сначала формируем набор данных - запрос. С его помощью будет сформирована та самая таблица "Порций" - передаем в экспортный метод объект "РезультатЗапроса" и переменную хранящую требуемое количество фоновых заданий. Также можно указать краткое представление фонового задания - чтобы без труда находить его в консоли. Вот так, например, может выглядеть команда формирования таблицы фоновых заданий для перепроведения массива документов:

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

Результат запроса "разрезается" на 10 таблиц-порций, каждая из которых помещается в строку таблицы фоновых задач. После этого необходимо разработать алгоритм обработки таблицы порции. Код можно писать сразу после получения результата запроса:

Выборка = РезультатЗапроса.Выбрать();
об = Выборка.Ссылка.ПолучитьОбъект();
об.Записать(РежимЗаписиДокумента.Проведение);

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

Следующий шаг - это вызов экспортной процедуры исполняющей код в фоне. Она имеет следующие параметры:

- ТаблицаФоновыхЗадач
- Код
- ОжидатьЗавершения
- ЦиклПоНаборуДанных
- МассивПолейДляСообщенияИсключения

С первыми тремя параметрами вопросов быть не должно - их назначение очевидно из названия. Для чего нужны последние два:

ЦиклПоНаборуДанных - Булево - это флаг, определяющий что выполнение строки кода будет выполняться в цикле по таблице фоновых задач:

Для каждого Выборка Из тзНаборДанных Цикл
 Попытка
  Выполнить(Код); 
 Исключение

 //Обработка полей-исключений.... 

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

КонецЦикла;

Цикл по переданному набору данных (порции) нужен не всегда: вполне возможно что понадобится обработать именно сам набор, обращаясь к нему как к таблице значений.

МассивПолейДляСообщенияИсключения - Строка, Массив - в коде выше закомментаренный участок "//Обработка полей исключений" - если при выполнении кода произошел сбой, то описание ошибки будет помещено в сообщение пользователю. Если кроме этого описания хочется увидеть большую детализацию - например на каком именно объекте произошел сбой, то в этот параметр можно передать одно (строкой) или несколько (массивом) именований полей запроса - тогда в описании ошибки будут выданы соотв. значения этих полей. Правда это не самый удобный способ анализировать сбои - все-же лучше завести РС, и записывать туда лог.
В итоге чтобы вызвать фоновую обработку приведенного абстрактного примера для перепроведения документов нужно выполнить следующий код:

Запрос = Новый Запрос("Выбрать Док.Ссылка ИЗ Документ.Накладная КАК Док");
РезультатЗапроса = Запрос.Выполнить();
тзФоновыеЗадачи = ОбщегоНазначенияВызовСервера.фз_ТаблицаФоновыхЗадач(РезультатЗапроса, 10, "Перепроведение документов");
СтрокаКода = "об = Выборка.Ссылка.ПолучитьОбъект(); об.Записать(РежимЗаписиДокумента.Проведение);";
ОбщегоНазначенияВызовСервера.фз_ВыполнитьВФоне(тзФоновыеЗадачи, СтрокаКода, Ложь, Истина, "Ссылка");

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

При запуске конфигурации (все-равно в каком режиме) откроется форма обработки "Пример" из которой можно создать/удалить в фоне документы. Не забывайте нажимать F5 :)

Демка


Для того чтобы мониторить активность фоновых заданий, я пользуюсь консолью инструментов разработчика, но почему-то в файловой базе она пасанула. Ну, для примера - можно и ЖР обойтись. Ниже приведен сам блок экспортных процедур - в таком виде он существует у меня в ОМ "ОбщегоНазначенияВызовСервера". Если захотите добавить себе  - не забудьте отредактировать переменную "ИмяМетода" в процедуре "фз_ВыполнитьВФоне".

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ПРОГРАММНЫЙ ИНТЕРФЕЙС: Выполнение произвольного кода в фоне (фз)

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

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

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

Скачать файлы

Наименование Файл Версия Размер
1Cv8.1CD 4
.1CD 3,73Mb
12.01.16
4
.1CD 3,73Mb Скачать

См. также

Подписаться Добавить вознаграждение
Комментарии
1. aspirator 23 (aspirator23) 23.01.16 16:55
Примерно также делал, только для контроля выполнения фоновых заданий использовал дополнительный регистр.
Позволяет наглядно выводить пользователю выполнение фоновых задач.
От ОжидатьЗавершения() отказался - не позволяет отражать процесс выполнения заданий.
С фоновыми заданиями кода получается много по сравнению с обычным последовательным выполнением, но оно того стоило.
Задачи которые выполнялись 40 минут, выполняются за 2-3 минуты. Для интерактивных операций то что нужно.
Да и для многих регламентных можно использовать.
2. Сергей Старых (tormozit) 30.01.16 02:12
пользуюсь консолью инструментов разработчика, но почему-то в файловой базе она пасанула
Не пробовал сообщить описание проблемы разработчику?
3. Роман Уничкин (unichkin) 30.01.16 16:01
(2) tormozit, сообщу. Просто не придал этому большого значения. Благодаря твоей работе разработка стала на порядок проще, опишу конечно.