В практике разработки на платформе 1С:Предприятие 8 часто возникает задача выноса тяжелых вычислений в фоновый режим. При этом архитектура решения может предполагать использование различных видов обработок. Нам может потребоваться запустить один и тот же алгоритм в фоне, независимо от того, где физически находится обработка: встроена ли она в конфигурацию, зарегистрирована ли в справочнике "Дополнительные отчеты и обработки" (БСП) или просто лежит в виде внешнего файла .epf на диске (или передается с клиента).
В этой статье мы подробно разберем, как спроектировать и написать универсальный механизм запуска фоновых заданий, который будет корректно и стабильно работать со всеми тремя типами обработок, в том числе в условиях многосерверного кластера 1С.
Если со встроенными обработками и обработками из БСП все достаточно прозрачно (платформа и БСП предоставляют для них штатные методы инициализации), то с внешними файловыми обработками возникает серьезная проблема. В клиент-серверном варианте работы фоновое задание всегда выполняется на сервере. Если мы просто передадим путь к файлу обработки на диске (например, "C:\Users\User\Documents\MyProcessing.epf"), то:
Для решения этой проблемы мы рассмотрим наиболее надежный подход: передачу тела внешней обработки в виде двоичных данных через Временное хранилище.
Давайте пошагово разберем создание универсального механизма запуска.
Для начала определим структуру параметров, которую мы будем передавать в наш универсальный метод запуска. Нам необходимо знать, какую именно обработку запускать, какой метод вызвать и какие параметры передать в сам этот метод.
Создадим структуру параметров запуска, в которой предусмотрим следующие свойства:
"Встроенная", "Дополнительная", "ВнешняяФайл")..epf (для внешних файлов).Для запуска фонового задания мы будем использовать стандартный механизм БСП — общий модуль ДлительныеОперации. Начиная с современных версий БСП, в нем заложены мощные инструменты для работы с внешними объектами. Однако для обеспечения полной универсальности мы напишем собственную процедуру-диспетчер.
Разместим этот код в серверном общем модуле (например, ОптимизацияВыполненияСервер), установив для модуля свойства Сервер, Вызов сервера.
// Функция запускает выполнение процедуры обработки в фоновом режиме.
//
// Параметры:
// ПараметрыЗапуска - Структура - содержит описание запускаемой обработки.
// УникальныйИдентификатор - УникальныйИдентификаторФормы - для привязки хранилища.
//
// Возвращаемое значение:
// Структура - Результат запуска из ДлительныеОперации.ЗапуститьВыполнениеВФоне().
//
Функция ЗапуститьОбработкуВФоне(ПараметрыЗапуска, УникальныйИдентификатор) Экспорт
ПараметрыВызоваФона = Новый Структура;
ПараметрыВызоваФона.Вставить("ВидОбработки", ПараметрыЗапуска.ВидОбработки);
ПараметрыВызоваФона.Вставить("ИдентификаторОбработки", ПараметрыЗапуска.ИдентификаторОбработки);
ПараметрыВызоваФона.Вставить("ИмяМетода", ПараметрыЗапуска.ИмяМетода);
ПараметрыВызоваФона.Вставить("ПараметрыВыполнения", ПараметрыЗапуска.ПараметрыВыполнения);
// Ограничим время выполнения фонового задания, если требуется
ПараметрыВыполненияФонового = ДлительныеОперации.ПараметрыВыполненияВФоне(УникальныйИдентификатор);
ПараметрыВыполненияФонового.НаименованиеФоновогоЗадания = НСтр("ru = 'Универсальный запуск обработки в фоне'");
// Метод-прослойка, который будет выполняться непосредственно в фоновом задании
ИмяЭкспортногоМетода = "ОптимизацияВыполненияСервер.ВыполнитьПроцедуруОбработкиВФоне";
Результат = ДлительныеОперации.ВыполнитьВФоне(
ИмяЭкспортногоМетода,
ПараметрыВызоваФона,
ПараметрыВыполненияФонового
);
Возврат Результат;
КонецФункции
Теперь напишем метод ВыполнитьПроцедуруОбработкиВФоне, который запускается непосредственно внутри фонового процесса. Его задача — восстановить объект обработки в зависимости от ее вида, передать ему параметры и запустить целевой экспортный метод модуля объекта.
Обратите внимание, что данный метод должен быть экспортным и находиться в общем модуле с установленным флагом Сервер.
// Метод, выполняемый непосредственно в фоновом задании.
// Выполняет диспетчеризацию запуска в зависимости от типа обработки.
//
// Параметры:
// ПараметрыВызова - Структура - передается из родительского сеанса.
// АдресРезультата - Строка - адрес во временном хранилище для возврата данных.
//
Процедура ВыполнитьПроцедуруОбработкиВФоне(ПараметрыВызова, АдресРезультата) Экспорт
ВидОбработки = ПараметрыВызова.ВидОбработки;
ИдентификаторОбработки = ПараметрыВызова.ИдентификаторОбработки;
ИмяМетода = ПараметрыВызова.ИмяМетода;
ПараметрыВыполнения = ПараметрыВызова.ПараметрыВыполнения;
ОбъектОбработки = Неопределено;
Если ВидОбработки = "Встроенная" Тогда
// ИдентификаторОбработки в данном случае - это просто имя обработки в метаданных (Строка)
ОбъектОбработки = Обработки[ИдентификаторОбработки].Создать();
ИначеЕсли ВидОбработки = "Дополнительная" Тогда
// ИдентификаторОбработки - это Ссылка на справочник "ДополнительныеОтчетыИОбработки"
// Воспользуемся стандартным методом БСП для получения объекта внешней обработки
ОбъектОбработки = ДополнительныеОтчетыИОбработки.ОбъектВнешнейОбработки(ИдентификаторОбработки);
ИначеЕсли ВидОбработки = "ВнешняяФайл" Тогда
// ИдентификаторОбработки - это адрес во временном хранилище, где лежат двоичные данные файла .epf
// 1. Получаем двоичные данные из хранилища
ДвоичныеДанныеОбработки = ПолучитьИзВременногоХранилища(ИдентификаторОбработки);
Если ТипЗнч(ДвоичныеДанныеОбработки) <> Тип("ДвоичныеДанные") Тогда
ВызватьИсключение НСтр("ru = 'Не удалось получить двоичные данные внешней обработки из хранилища!'");
КонецЕсли;
// 2. Записываем двоичные данные во временный файл на текущем сервере кластера
ИмяВременногоФайла = ПолучитьИмяВременногоФайла("epf");
ДвоичныеДанныеОбработки.Записать(ИмяВременногоФайла);
// 3. Подключаем внешнюю обработку и создаем объект
// Используем БезопасныйРежим = Ложь, если обработка требует полных прав, либо Истина для изоляции
ИмяПодключеннойОбработки = ВнешниеОбработки.Подключить(ИмяВременногоФайла, , Ложь);
ОбъектОбработки = ВнешниеОбработки.Создать(ИмяПодключеннойОбработки);
// 4. Попытаемся удалить временный файл (платформа уже загрузила его в память)
Попытка
УдалитьФайлы(ИмяВременногоФайла);
Исключение
// Не критично, файлы из временной папки ОС со временем очистятся сами
КонецПопытки;
Иначе
ВызватьИсключение НСтр("ru = 'Указан неизвестный вид обработки!'");
КонецЕсли;
// Выполняем экспортный метод обработки
Если ОбъектОбработки <> Неопределено Тогда
// Подготовим параметры для самого метода.
// Передадим в него параметры выполнения и адрес для возврата результатов, если они будут.
МассивПараметров = Новый Массив;
МассивПараметров.Добавить(ПараметрыВыполнения);
МассивПараметров.Добавить(АдресРезультата);
// Вызываем метод объекта обработки с помощью универсального механизма платформы
ВыполнитьМетодОбъекта(ОбъектОбработки, ИмяМетода, МассивПараметров);
Иначе
ВызватьИсключение НСтр("ru = 'Не удалось инициализировать объект обработки!'");
КонецЕсли;
КонецПроцедуры
// Вспомогательный метод для динамического вызова экспортного метода объекта
Процедура ВыполнитьМетодОбъекта(Объект, ИмяМетода, МассивПараметров)
// Динамически формируем оператор Выполнить для передачи параметров
КоличествоПараметров = МассивПараметров.Количество();
СтрокаПараметров = "";
Для Индекс = 0 По КоличествоПараметров - 1 Цикл
СтрокаПараметров = СтрокаПараметров + ?(Индекс = 0, "", ", ") + "МассивПараметров[" + Индекс + "]";
КонецЦикла;
ШаблонВызова = "Объект.%1(%2);";
ОператорВызова = СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(ШаблонВызова, ИмяМетода, СтрокаПараметров);
Выполнить(ОператорВызова);
КонецПроцедуры
Рассмотрим подробнее логику вспомогательного метода ВыполнитьМетодОбъекта. Платформа 1С не имеет встроенной функции типа ВыполнитьМетод() с передачей массива параметров для произвольных объектов (как это реализовано для общих модулей через Вычислить()). Поэтому мы используем динамическое формирование строки вызова и команду Выполнить(). Это гарантирует, что мы сможем передать любое количество параметров в метод объекта обработки.
Чтобы наша обработка могла успешно выполниться в фоне (независимо от того, как она была запущена), её экспортный метод в Модуле объекта должен соответствовать единому стандарту. Он должен принимать ровно два параметра: структуру параметров выполнения и адрес хранилища для возврата результата.
Давайте посмотрим на пример реализации модуля объекта такой обработки:
// Экспортный метод, который будет вызван в фоне универсальным диспетчером
Процедура ВыполнитьОбработкуДанных(ПараметрыВыполнения, АдресРезультата) Экспорт
// 1. Извлекаем параметры для алгоритма
Организация = Неопределено;
ПараметрыВыполнения.Свойство("Организация", Организация);
Если Не ЗначениеЗаполнено(Организация) Тогда
ЗаписьЖурналаРегистрации("ФоновыеЗадания", УровеньЖурналаРегистрации.Ошибка, , , "Не указана организация.");
Возврат;
КонецЕсли;
// 2. Выполняем полезную работу
РезультатРаботы = НачатьТяжелыйРасчет(Организация);
// 3. Возвращаем результат вызывающему сеансу (клиенту) через временное хранилище
Если ЗначениеЗаполнено(АдресРезультата) Тогда
СтруктураРезультата = Новый Структура;
СтруктураРезультата.Вставить("Успешно", Истина);
СтруктураРезультата.Вставить("РезультатРасчета", РезультатРаботы);
ПоместитьВоВременноеХранилище(СтруктураРезультата, АдресРезультата);
КонецЕсли;
КонецПроцедуры
Функция НачатьТяжелыйРасчет(Организация)
// Имитация длительной операции
Сумма = 0;
Выборка = Справочники.Номенклатура.Выбрать();
Пока Выборка.Следующий() Цикл
Сумма = Сумма + 1;
КонецЦикла;
Возврат Сумма;
КонецФункции
Теперь проанализируем ситуацию, когда пользователь нажимает кнопку в форме и инициирует фоновое выполнение. Ниже приведен пример кода клиентской формы, демонстрирующий подготовку и запуск задания для всех трех случаев.
&НаКлиенте
Асинхронный Процедура ЗапуститьВФоне(Команда)
ПараметрыАлгоритма = Новый Структура;
ПараметрыАлгоритма.Вставить("Организация", Объект.Организация);
ПараметрыЗапуска = Новый Структура;
ПараметрыЗапуска.Вставить("ИмяМетода", "ВыполнитьОбработкуДанных");
ПараметрыЗапуска.Вставить("ПараметрыВыполнения", ПараметрыАлгоритма);
Если Объект.ВариантРаботы = "Встроенная" Тогда
ПараметрыЗапуска.Вставить("ВидОбработки", "Встроенная");
ПараметрыЗапуска.Вставить("ИдентификаторОбработки", "ЗагрузкаДанныхИзКлассификаторов");
ИначеЕсли Объект.ВариантРаботы = "Дополнительная" Тогда
ПараметрыЗапуска.Вставить("ВидОбработки", "Дополнительная");
// Ссылка на элемент справочника "Дополнительные отчеты и обработки"
ПараметрыЗапуска.Вставить("ИдентификаторОбработки", Объект.СсылкаНаДопОбработку);
ИначеЕсли Объект.ВариантРаботы = "ВнешняяФайл" Тогда
// Считываем файл с клиента в двоичные данные
// Начиная с платформы 8.3.15 рекомендуется использовать Асинхронные методы работы с файлами
ОписаниеФайла = Ждать ПоместитьФайлНаСерверАсинхронно(, , , , УникальныйИдентификатор);
Если ОписаниеФайла <> Неопределено И ОписаниеФайла.ПомещениеВыполнено Тогда
ПараметрыЗапуска.Вставить("ВидОбработки", "ВнешняяФайл");
// Передаем адрес временного хранилища, куда помещен файл обработки
ПараметрыЗапуска.Вставить("ИдентификаторОбработки", ОписаниеФайла.Адрес);
Иначе
ПоказатьПредупреждение(, НСтр("ru = 'Файл внешней обработки не выбран или не загружен!'"));
Возврат;
EndIf;
КонецЕсли;
// Вызываем наш серверный универсальный пускач
РезультатФона = ЗапуститьОбработкуВФоне(ПараметрыЗапуска, УникальныйИдентификатор);
// Передаем управление стандартному механизму ожидания длительной операции БСП
Если РезультатФона.Статус = "Выполняется" Тогда
ПараметрыОжидания = ДлительныеОперацииКлиент.ПараметрыОжидания(ЭтотОбъект);
ПараметрыОжидания.ВыводитьПрогрессВыполнения = Истина;
ОповещениеОЗавершении = Новый ОписаниеОповещения("ПослеЗавершенияОперации", ЭтотОбъект);
ДлительныеОперацииКлиент.ОжидатьЗавершение(РезультатФона, ОповещениеОЗавершении, ПараметрыОжидания);
ИначеЕсли РезультатФона.Статус = "Выполнено" Тогда
ПоказатьРезультат(РезультатФона.АдресРезультата);
КонецЕсли;
КонецПроцедуры
&НаКлиенте
Процедура ПослеЗавершенияОперации(Результат, ДополнительныеПараметры) Экспорт
Если Результат <> Неопределено Тогда
Если Результат.Статус = "Выполнено" Тогда
ПоказатьРезультат(Результат.АдресРезультата);
ИначеЕсли Результат.Статус = "Ошибка" Тогда
ВызватьИсключение Результат.ПодробноеПредставлениеОшибки;
КонецЕсли;
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Процедура ПоказатьРезультат(Знач АдресРезультата)
Если ЗначениеЗаполнено(АдресРезультата) Тогда
РезультатДеятельности = ПолучитьИзВременногоХранилища(АдресРезультата);
// Обрабатываем полученный из фонового задания результат (например, структуру с расчетами)
Если ТипЗнч(РезультатДеятельности) = Тип("Структура") Тогда
ЗаписьЖурналаРегистрации("ФоновыеЗадания", УровеньЖурналаРегистрации.Информация, , ,
"Фон успешно завершен. Результат: " + РезультатДеятельности.РезультатРасчета);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Мы спроектировали и реализовали по-настоящему гибкий и универсальный паттерн для асинхронного выполнения кода в 1С. Благодаря этому подходу:
ДлительныеОперации и ДлительныеОперацииКлиент) позволяет не заботиться о ручном мониторинге фонового процесса и предоставляет пользователю красивый стандартный индикатор загрузки на клиенте.