При разработке интерактивных инструментов в среде «1С:Предприятие» мы часто сталкиваемся с необходимостью вынесения тяжелых расчетов в фоновые задания. Это позволяет не блокировать интерфейс пользователя и сохранять отзывчивость приложения. Однако при попытке передать текущий статус выполнения или промежуточные расчеты обратно на форму «на лету» разработчики часто совершают классическую ошибку: пытаются использовать для этого временное хранилище. В результате функция ПолучитьИзВременногоХранилища упорно возвращает Неопределено до тех пор, пока фоновое задание полностью не завершится.
В этой статье мы подробно разберем, почему платформа ведет себя именно так, проанализируем внутренние механизмы изоляции сеансов и реализуем правильную, отказоустойчивую схему интерактивного отображения прогресса выполнения длительной операции на базе Библиотеки стандартных подсистем (БСП).
Для того чтобы понять причину такого поведения платформы, давайте заглянем под капот механизма временного хранилища. Главное правило, о котором необходимо помнить: данные временного хранилища изолированы в рамках конкретного сеанса работы пользователя. Это архитектурное решение принято для исключения конфликтов блокировок и обеспечения высокой производительности распределенной системы.
Когда вы запускаете фоновое задание, платформа «1С:Предприятие» создает для него отдельный фоновый сеанс. Этот сеанс физически не имеет прямого доступа к временному хранилищу вашего основного (интерактивного) сеанса, в котором открыта форма. Единственным исключением из этого правила является передача данных по завершении фонового задания, которая работает по строгому регламенту:
АдресХранилища = ПоместитьВоВременноеХранилище(Неопределено, УникальныйИдентификатор).ПоместитьВоВременноеХранилище(РезультатВыполнения, АдресХранилища).Неопределено.Именно поэтому попытка использовать связку ПоместитьВоВременноеХранилище и ПолучитьИзВременногоХранилища для динамического обновления прогресс-бара во время работы фонового процесса обречена на неудачу.
Поскольку использовать временное хранилище для получения оперативных данных в режиме реального времени невозможно, разработчики Библиотеки стандартных подсистем задействовали другой системный механизм платформы — сообщения пользователю.
Давайте разберем по шагам, как это работает:
ДлительныеОперации.СообщитьПрогресс(Процент, Текст). Под капотом этот метод формирует стандартный объект СообщениеПользователю, записывая туда структурированную информацию (процент выполнения и текст состояния) в виде XML- или JSON-строки со специальным префиксом-маркером.ДлительныеОперации.ПрочитатьПрогресс().ФоновоеЗадание.ПолучитьСообщенияПользователю(Истина). Платформа без проблем позволяет читать сообщения активного фонового задания "на лету" из родительского сеанса.Перейдем к практической части и исправим код, представленный в вопросе автора темы. Нам потребуется скорректировать как серверную процедуру фонового задания, так и клиентскую логику управления формой.
Рассмотрим код фонового задания. Вместо попыток записи промежуточного состояния во временное хранилище, мы будем использовать метод ДлительныеОперации.СообщитьПрогресс. При этом крайне важно оптимизировать частоту отправки прогресса, чтобы не перегружать сервер и очередь сообщений.
// Функция выполняется в фоновом задании на сервере
Функция ОбновитьОстатокВНоменклатуре(СтруктураПараметров, АдресХранилища) Экспорт
РезультатВыполнения = Новый Структура("Задание, ПроцентВыполнения", "ОбновлениеОстатков", 0);
Запасы = СтруктураПараметров.Запасы;
ПроцентВыполнения = 0;
НомерПоПорядку = 0;
ОбщееКоличествоВТаблице = Запасы.Количество();
Для Каждого Сч Из Запасы Цикл
НомерПоПорядку = НомерПоПорядку + 1;
// Рассчитываем текущий процент выполнения операции
ТекущийПроцентВыполнения = Цел(НомерПоПорядку / ОбщееКоличествоВТаблице * 100);
// Сообщаем прогресс только при реальном изменении целой части процента
Если ТекущийПроцентВыполнения <> ПроцентВыполнения Тогда
ПроцентВыполнения = ТекущийПроцентВыполнения;
// ПРАВИЛЬНО: Передаем прогресс через механизм сообщений БСП
ДлительныеОперации.СообщитьПрогресс(ПроцентВыполнения, НСтр("ru='Обновление остатков поставщиков...'"));
КонецЕсли;
// Логика обработки данных
Если ЗначениеЗаполнено(Сч.Номенклатура) Тогда
ЗначениеСвойства = ПланыВидовХарактеристик.ДополнительныеРеквизитыИСведения.НайтиПоРеквизиту("Имя", "ОстатокПоставщика");
ОбъектНоменклатура = Сч.Номенклатура.ПолучитьОбъект();
ДопРеквизиты = ОбъектНоменклатура.ДополнительныеРеквизиты;
НайденныеСтроки = ДопРеквизиты.НайтиСтроки(Новый Структура("Свойство", ЗначениеСвойства));
Если НайденныеСтроки.Количество() = 0 Тогда
СтрокаТаблицы = ДопРеквизиты.Добавить();
Иначе
СтрокаТаблицы = НайденныеСтроки[0];
КонецЕсли;
Если СтрокаТаблицы.Значение <> Сч.ОстатокПоставщика Тогда
СтрокаТаблицы.Свойство = ЗначениеСвойства;
СтрокаТаблицы.Значение = Сч.ОстатокПоставщика;
ОбъектНоменклатура.ОбменДанными.Загрузка = Истина;
ОбъектНоменклатура.Записать();
КонецЕсли;
КонецЕсли;
КонецЦикла;
// Фиксируем финальный результат по завершении всей работы
РезультатВыполнения.ПроцентВыполнения = 100;
ПоместитьВоВременноеХранилище(РезультатВыполнения, АдресХранилища);
КонецФункции
Теперь настроим клиентскую часть нашей внешней обработки. Вместо ручного написания обработчиков ожидания и разбора статусов, мы задействуем штатный асинхронный механизм БСП — метод ДлительныеОперацииКлиент.ОжидатьЗавершение. Этот метод берет на себя всю рутину по опросу сервера, визуализации прогресс-бара и корректному завершению ожидания.
Добавим в модуль управляемой формы нашей обработки следующий код:
&НаКлиенте
Процедура ОбновитьОстаток(Команда)
// Подготавливаем параметры для фонового задания
ПараметрыВыполнения = ПолучитьПараметрыВыполненияНаСервере();
// Запускаем фоновое задание на сервере
ДлительнаяОперация = НачатьВыполнениеНаСервере(ПараметрыВыполнения);
Если ДлительнаяОперация.Статус = "Выполнено" Тогда
// Если операция выполнилась мгновенно (на малом объеме данных)
ПослеЗавершенияОперации(ДлительнаяОперация, Неопределено);
Иначе
// Настраиваем параметры ожидания
ПараметрыОжидания = ДлительныеОперацииКлиент.ПараметрыОжидания(ЭтотОбъект);
ПараметрыОжидания.ВыводитьПрогрессВыполнения = Истина;
ПараметрыОжидания.ВыводитьСообщения = Истина;
ПараметрыОжидания.ТекстСообщения = НСтр("ru = 'Выполняется обновление остатков номенклатуры...'");
// Описание оповещения, которое сработает после завершения фонового задания
ОповещениеОЗавершении = Новый ОписаниеОповещения("ПослеЗавершенияОперации", ЭтотОбъект);
// Запускаем стандартный интерактивный цикл ожидания БСП
ДлительныеОперацииКлиент.ОжидатьЗавершение(ДлительнаяОперация, ОповещениеОЗавершении, ПараметрыОжидания);
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Функция НачатьВыполнениеНаСервере(Знач ПараметрыВыполнения)
// Регистрируем параметры запуска длительной операции
НастройкиЗапуска = ДлительныеОперации.ПараметрыВыполненияВФоне(Новый УникальныйИдентификатор);
НастройкиЗапуска.НаименованиеФоновогоЗадания = НСтр("ru = 'Обновление остатков номенклатуры'");
// Имя метода, который будет запущен в фоне (должен быть в экспортном модуле)
ИмяМетода = "УправлениеНебольшойФирмойСервер.ОбновитьОстатокВНоменклатуре";
Возврат ДлительныеОперации.ВыполнитьВФоне(ИмяМетода, ПараметрыВыполнения, НастройкиЗапуска);
КонецФункции
&НаСервере
Функция ПолучитьПараметрыВыполненияНаСервере()
ПараметрыВыполнения = Новый Структура;
ПараметрыВыполнения.Вставить("Запасы", Объект.Запасы.Выгрузить());
Возврат ПараметрыВыполнения;
КонецФункции
&НаКлиенте
Процедура ПослеЗавершенияОперации(РезультатОперации, ДополнительныеПараметры) Экспорт
Если РезультатОперации.Статус = "Выполнено" Тогда
// Теперь задание выполнено, и мы МОЖЕМ безопасно прочитать финальные данные из временного хранилища
РезультатВычислений = ПолучитьИзВременногоХранилища(РезультатОперации.АдресХранилища);
ПоказатьПредупреждение(, НСтр("ru = 'Обработка данных успешно завершена!'"));
ИначеЕсли РезультатОперации.Статус = "Отменено" Тогда
ПоказатьПредупреждение(, НСтр("ru = 'Операция была отменена пользователем.'"));
Иначе
// В случае ошибки выводим подробный текст исключения
ВызватьИсключение РезультатОперации.КраткоеПредставлениеОшибки;
КонецЕсли;
КонецПроцедуры
Для того чтобы ваш код работал быстро и не создавал избыточной нагрузки на сервер СУБД и рабочие процессы платформы, придерживайтесь следующих рекомендаций:
СообщитьПрогресс только тогда, когда процент действительно изменился. Не отправляйте сообщения на каждой итерации цикла, если у вас обрабатываются десятки тысяч строк. Оптимальное количество обновлений прогресса за весь период работы фонового задания — не более 100 раз.СообщитьПрогресс имеет необязательный параметр для передачи дополнительных структур. Никогда не помещайте туда объемные таблицы значений или деревья данных. Данный параметр предназначен исключительно для легковесных сопутствующих статусов. Большие объемы информации должны передаваться строго один раз — через итоговое временное хранилище при успешном статусе завершения.