Как написать универсальный код для запуска фонового задания из встроенной, дополнительной или внешней обработки в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

В практике разработки на платформе 1С:Предприятие 8 часто возникает задача выноса тяжелых вычислений в фоновый режим. При этом архитектура решения может предполагать использование различных видов обработок. Нам может потребоваться запустить один и тот же алгоритм в фоне, независимо от того, где физически находится обработка: встроена ли она в конфигурацию, зарегистрирована ли в справочнике "Дополнительные отчеты и обработки" (БСП) или просто лежит в виде внешнего файла .epf на диске (или передается с клиента).

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

В чем заключается основная сложность?

Если со встроенными обработками и обработками из БСП все достаточно прозрачно (платформа и БСП предоставляют для них штатные методы инициализации), то с внешними файловыми обработками возникает серьезная проблема. В клиент-серверном варианте работы фоновое задание всегда выполняется на сервере. Если мы просто передадим путь к файлу обработки на диске (например, "C:\Users\User\Documents\MyProcessing.epf"), то:

  1. У сервера 1С может не быть прав на доступ к этой папке.
  2. В кластере серверов фоновое задание может запуститься на рабочем сервере №2, в то время как файл физически находится на диске рабочего сервера №1. В этом случае система выдаст ошибку о том, что файл не найден.

Для решения этой проблемы мы рассмотрим наиболее надежный подход: передачу тела внешней обработки в виде двоичных данных через Временное хранилище.

Давайте пошагово разберем создание универсального механизма запуска.

Шаг 1. Проектирование универсального интерфейса для запуска

Для начала определим структуру параметров, которую мы будем передавать в наш универсальный метод запуска. Нам необходимо знать, какую именно обработку запускать, какой метод вызвать и какие параметры передать в сам этот метод.

Создадим структуру параметров запуска, в которой предусмотрим следующие свойства:

Шаг 2. Реализация универсального метода запуска фонового задания

Для запуска фонового задания мы будем использовать стандартный механизм БСП — общий модуль ДлительныеОперации. Начиная с современных версий БСП, в нем заложены мощные инструменты для работы с внешними объектами. Однако для обеспечения полной универсальности мы напишем собственную процедуру-диспетчер.

Разместим этот код в серверном общем модуле (например, ОптимизацияВыполненияСервер), установив для модуля свойства Сервер, Вызов сервера.


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

Шаг 3. Реализация метода-диспетчера на сервере

Теперь напишем метод ВыполнитьПроцедуруОбработкиВФоне, который запускается непосредственно внутри фонового процесса. Его задача — восстановить объект обработки в зависимости от ее вида, передать ему параметры и запустить целевой экспортный метод модуля объекта.

Обратите внимание, что данный метод должен быть экспортным и находиться в общем модуле с установленным флагом Сервер.


// Метод, выполняемый непосредственно в фоновом задании.
// Выполняет диспетчеризацию запуска в зависимости от типа обработки.
//
// Параметры:
//  ПараметрыВызова - Структура - передается из родительского сеанса.
//  АдресРезультата - Строка - адрес во временном хранилище для возврата данных.
//
Процедура ВыполнитьПроцедуруОбработкиВФоне(ПараметрыВызова, АдресРезультата) Экспорт
	
	ВидОбработки           = ПараметрыВызова.ВидОбработки;
	ИдентификаторОбработки = ПараметрыВызова.ИдентификаторОбработки;
	ИмяМетода              = ПараметрыВызова.ИмяМетода;
	ПараметрыВыполнения    = ПараметрыВызова.ПараметрыВыполнения;
	
	ОбъектОбработки = Неопределено;
	
	Если ВидОбработки = "Встроенная" Тогда
		
		// ИдентификаторОбработки в данном случае - это просто имя обработки в метаданных (Строка)
		ОбъектОбработки = Обработки[ИдентификаторОбработки].Создать();
		
	ИначеЕсли ВидОбработки = "Дополнительная" Тогда
		
		// ИдентификаторОбработки - это Ссылка на справочник "ДополнительныеОтчетыИОбработки"
		// Воспользуемся стандартным методом БСП для получения объекта внешней обработки
		ОбъектОбработки = ДополнительныеОтчетыИОбработки.ОбъектВнешнейОбработки(ИдентификаторОбработки);
		
	ИначеЕсли ВидОбработки = "ВнешняяФайл" Тогда
		
		// ИдентификаторОбработки - это адрес во временном хранилище, где лежат двоичные данные файла .epf
		// 1. Получаем двоичные данные из хранилища
		ДвоичныеДанныеОбработки = ПолучитьИзВременногоХранилища(ИдентификаторОбработки);
		
		Если ТипЗнч(ДвоичныеДанныеОбработки) <> Тип("ДвоичныеДанные") Тогда
			ВызватьИсключение НСтр("ru = 'Не удалось получить двоичные данные внешней обработки из хранилища!'");
		КонецЕсли;
		
		// 2. Записываем двоичные данные во временный файл на текущем сервере кластера
		ИмяВременногоФайла = ПолучитьИмяВременногоФайла("epf");
		ДвоичныеДанныеОбработки.Записать(ИмяВременногоФайла);
		
		// 3. Подключаем внешнюю обработку и создаем объект
		// Используем БезопасныйРежим = Ложь, если обработка требует полных прав, либо Истина для изоляции
		ИмяПодключеннойОбработки = ВнешниеОбработки.Подключить(ИмяВременногоФайла, , Ложь);
		ОбъектОбработки = ВнешниеОбработки.Создать(ИмяПодключеннойОбработки);
		
		// 4. Попытаемся удалить временный файл (платформа уже загрузила его в память)
		Попытка
			УдалитьФайлы(ИмяВременногоФайла);
		Исключение
			// Не критично, файлы из временной папки ОС со временем очистятся сами
		КонецПопытки;
		
	Иначе
		
		ВызватьИсключение НСтр("ru = 'Указан неизвестный вид обработки!'");
		
	КонецЕсли;
	
	// Выполняем экспортный метод обработки
	Если ОбъектОбработки <> Неопределено Тогда
		
		// Подготовим параметры для самого метода. 
		// Передадим в него параметры выполнения и адрес для возврата результатов, если они будут.
		МассивПараметров = Новый Массив;
		МассивПараметров.Добавить(ПараметрыВыполнения);
		МассивПараметров.Добавить(АдресРезультата);
		
		// Вызываем метод объекта обработки с помощью универсального механизма платформы
		ВыполнитьМетодОбъекта(ОбъектОбработки, ИмяМетода, МассивПараметров);
		
	Иначе
		
		ВызватьИсключение НСтр("ru = 'Не удалось инициализировать объект обработки!'");
		
	КонецЕсли;
	
КонецПроцедуры

// Вспомогательный метод для динамического вызова экспортного метода объекта
Процедура ВыполнитьМетодОбъекта(Объект, ИмяМетода, МассивПараметров)
	
	// Динамически формируем оператор Выполнить для передачи параметров
	КоличествоПараметров = МассивПараметров.Количество();
	СтрокаПараметров = "";
	
	Для Индекс = 0 По КоличествоПараметров - 1 Цикл
		СтрокаПараметров = СтрокаПараметров + ?(Индекс = 0, "", ", ") + "МассивПараметров[" + Индекс + "]";
	КонецЦикла;
	
	ШаблонВызова = "Объект.%1(%2);";
	ОператорВызова = СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(ШаблонВызова, ИмяМетода, СтрокаПараметров);
	
	Выполнить(ОператорВызова);
	
КонецПроцедуры

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

Шаг 4. Требования к коду внутри самой обработки

Чтобы наша обработка могла успешно выполниться в фоне (независимо от того, как она была запущена), её экспортный метод в Модуле объекта должен соответствовать единому стандарту. Он должен принимать ровно два параметра: структуру параметров выполнения и адрес хранилища для возврата результата.

Давайте посмотрим на пример реализации модуля объекта такой обработки:


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

Функция НачатьТяжелыйРасчет(Организация)
	// Имитация длительной операции
	Сумма = 0;
	Выборка = Справочники.Номенклатура.Выбрать();
	Пока Выборка.Следующий() Цикл
		Сумма = Сумма + 1;
	КонецЦикла;
	Возврат Сумма;
КонецФункции

Шаг 5. Пример вызова на клиенте

Теперь проанализируем ситуацию, когда пользователь нажимает кнопку в форме и инициирует фоновое выполнение. Ниже приведен пример кода клиентской формы, демонстрирующий подготовку и запуск задания для всех трех случаев.


&НаКлиенте
Асинхронный Процедура ЗапуститьВФоне(Команда)
	
	ПараметрыАлгоритма = Новый Структура;
	ПараметрыАлгоритма.Вставить("Организация", Объект.Организация);
	
	ПараметрыЗапуска = Новый Структура;
	ПараметрыЗапуска.Вставить("ИмяМетода", "ВыполнитьОбработкуДанных");
	ПараметрыЗапуска.Вставить("ПараметрыВыполнения", ПараметрыАлгоритма);
	
	Если Объект.ВариантРаботы = "Встроенная" Тогда
		
		ПараметрыЗапуска.Вставить("ВидОбработки", "Встроенная");
		ПараметрыЗапуска.Вставить("ИдентификаторОбработки", "ЗагрузкаДанныхИзКлассификаторов");
		
	ИначеЕсли Объект.ВариантРаботы = "Дополнительная" Тогда
		
		ПараметрыЗапуска.Вставить("ВидОбработки", "Дополнительная");
		// Ссылка на элемент справочника "Дополнительные отчеты и обработки"
		ПараметрыЗапуска.Вставить("ИдентификаторОбработки", Объект.СсылкаНаДопОбработку); 
		
	ИначеЕсли Объект.ВариантРаботы = "ВнешняяФайл" Тогда
		
		// Считываем файл с клиента в двоичные данные
		// Начиная с платформы 8.3.15 рекомендуется использовать Асинхронные методы работы с файлами
		ОписаниеФайла = Ждать ПоместитьФайлНаСерверАсинхронно(, , , , УникальныйИдентификатор);
		
		Если ОписаниеФайла <> Неопределено И ОписаниеФайла.ПомещениеВыполнено Тогда
			ПараметрыЗапуска.Вставить("ВидОбработки", "ВнешняяФайл");
			// Передаем адрес временного хранилища, куда помещен файл обработки
			ПараметрыЗапуска.Вставить("ИдентификаторОбработки", ОписаниеФайла.Адрес);
		Иначе
			ПоказатьПредупреждение(, НСтр("ru = 'Файл внешней обработки не выбран или не загружен!'"));
			Возврат;
		EndIf;
		
	КонецЕсли;
	
	// Вызываем наш серверный универсальный пускач
	РезультатФона = ЗапуститьОбработкуВФоне(ПараметрыЗапуска, УникальныйИдентификатор);
	
	// Передаем управление стандартному механизму ожидания длительной операции БСП
	Если РезультатФона.Статус = "Выполняется" Тогда
		
		ПараметрыОжидания = ДлительныеОперацииКлиент.ПараметрыОжидания(ЭтотОбъект);
		ПараметрыОжидания.ВыводитьПрогрессВыполнения = Истина;
		
		ОповещениеОЗавершении = Новый ОписаниеОповещения("ПослеЗавершенияОперации", ЭтотОбъект);
		ДлительныеОперацииКлиент.ОжидатьЗавершение(РезультатФона, ОповещениеОЗавершении, ПараметрыОжидания);
		
	ИначеЕсли РезультатФона.Статус = "Выполнено" Тогда
		
		ПоказатьРезультат(РезультатФона.АдресРезультата);
		
	КонецЕсли;
	
КонецПроцедуры

&НаКлиенте
Процедура ПослеЗавершенияОперации(Результат, ДополнительныеПараметры) Экспорт
	
	Если Результат <> Неопределено Тогда
		Если Результат.Статус = "Выполнено" Тогда
			ПоказатьРезультат(Результат.АдресРезультата);
		ИначеЕсли Результат.Статус = "Ошибка" Тогда
			ВызватьИсключение Результат.ПодробноеПредставлениеОшибки;
		КонецЕсли;
	КонецЕсли;
	
КонецПроцедуры

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

Подведем итоги

Мы спроектировали и реализовали по-настоящему гибкий и универсальный паттерн для асинхронного выполнения кода в 1С. Благодаря этому подходу:

← На главную