Каждый разработчик 1С рано или поздно сталкивается с ситуацией, когда запущенная на сервере обработка (например, групповое перепроведение документов, закрытие месяца или импорт сотен тысяч записей) выполняется очень долго. В этот момент клиентское приложение визуально «зависает», а все стандартные вызовы процедуры Сообщить() накапливаются на стороне сервера и лавиной выводятся пользователю только после полного завершения работы алгоритма. Пользователь не понимает, работает ли программа или она зависла, что часто приводит к преждевременному принудительному завершению сеанса.
В этой статье мы подробно разберем, почему так происходит, и проанализируем четыре эффективных способа решения этой проблемы: от классических фоновых заданий до новейших механизмов платформы 1С:Предприятие.
Прежде чем перейти к решениям, выясним причину такого поведения платформы. Когда мы вызываем серверный метод непосредственно из клиентского контекста, управление полностью передается на сервер. Клиент переходит в режим ожидания ответа. Все вызовы функции Сообщить() на сервере не отправляются на клиент мгновенно, так как для этого потребовалось бы постоянно прерывать выполнение серверного кода и совершать ресурсоемкие сетевые вызовы (round-trips). Платформа оптимизирует этот процесс: она собирает все сообщения в специальный буфер и передает их клиенту единым пакетом вместе с возвратом управления.
Для решения этой проблемы нам нужно реализовать асинхронное взаимодействие, при котором серверный код выполняется независимо, а клиент имеет возможность получать информацию о его состоянии в реальном времени.
---
Для того чтобы клиентский интерфейс оставался отзывчивым, длительную операцию необходимо перенести в фоновое задание. Фоновое задание выполняется в отдельном сеансе на сервере, не блокируя основной сеанс пользователя. Мы можем запустить такое задание и периодически опрашивать его состояние с клиента.
Рассмотрим по шагам, как настроить передачу прогресса и сообщений из фонового задания:
ФоновыеЗадания.ЗаписатьПрогресс() для отправки текущего состояния. Это гораздо производительнее, чем передача больших объемов текста.Посмотрим на пример реализации серверной части:
// Код в общем модуле "ДлительныеПроцедурыСервер" (Сервер, Вызов сервера)
Процедура ВыполнитьОбработкуДанных(ПараметрыПроцесса) Экспорт
Выборка = Документы.РеализацияТоваровУслуг.Выбрать();
ВсегоДокументов = Выборка.Количество();
Счетчик = 0;
Пока Выборка.Следующий() Цикл
// Выполняем длительные расчеты или проведение
ОбработатьДокумент(Выборка.Ссылка);
Счетчик = Счетчик + 1;
// Записываем прогресс каждые 100 документов, чтобы не нагружать СУБД
Если Счетчик % 100 = 0 Или Счетчик = ВсегоДокументов Тогда
ПроцентВыполнения = Окр((Счетчик / ВсегоДокументов) * 100, 1);
СтруктураСостояния = Новый Структура;
СтруктураСостояния.Вставить("Процент", ПроцентВыполнения);
СтруктураСостояния.Вставить("Текст", "Обработано документов: " + Счетчик + " из " + ВсегоДокументов);
// Метод записывает данные в структуру фонового задания
ФоновыеЗадания.ЗаписатьПрогресс(СтруктураСостояния);
// Также можно отправлять сообщения пользователю, они запишутся в буфер задания
Сообщить("Успешно обработан пакет до документа: " + Выборка.Ссылка);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Теперь спроектируем клиентскую форму, которая будет запускать этот процесс и отображать ход его выполнения:
// Код в модуле управляемой формы
&НаКлиенте
Перем ИдентификаторЗадания;
&НаКлиенте
Процедура ЗапуститьОбработку(Команда)
// Запускаем фоновое задание на сервере
ИдентификаторЗадания = ЗапуститьФоновоеЗаданиеНаСервере();
// Подключаем опрос состояния каждые 2 секунды
ПодключитьОбработчикОжидания("ПроверитьСтатусВыполнения", 2, Ложь);
// Делаем элементы формы недоступными на время работы
Элементы.ГруппаПараметры.Доступность = Ложь;
Элементы.КнопкаСтарт.Доступность = Ложь;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ЗапуститьФоновоеЗаданиеНаСервере()
ПараметрыВыполнения = Новый Массив;
ПараметрыВыполнения.Добавить(Новый Структура); // Передаем параметры если нужно
// Запускаем асинхронное фоновое задание
Задание = ФоновыеЗадания.Выполнить("ДлительныеПроцедурыСервер.ВыполнитьОбработкуДанных", ПараметрыВыполнения);
Возврат Задание.УникальныйИдентификатор;
EndFunction
&НаКлиенте
Процедура ПроверитьСтатусВыполнения()
РезультатОпроса = ПолучитьДанныеВыполненияНаСервере(ИдентификаторЗадания);
// 1. Отображаем прогресс на форме
Если РезультатОпроса.ПрогрессЗаписан Тогда
ТекущийПроцент = РезультатОпроса.ДанныеПрогресса.Процент;
ТекстПрогресса = РезультатОпроса.ДанныеПрогресса.Текст;
// Обновляем стандартный индикатор (прогресс-бар) на форме
ИмяИндикатора = ТекущийПроцент;
Состояние(ТекстПрогресса, ТекущийПроцент);
КонецЕсли;
// 2. Выводим накопленные сообщения "Сообщить()"
Для Каждого ТекстСообщения Из РезультатОпроса.Сообщения Пользователю Цикл
ОбщегоНазначенияКлиентСервер.СообщитьПользователю(ТекстСообщения.Текст);
КонецЦикла;
// 3. Проверяем завершение задания
Если РезультатОпроса.Статус = "Завершено" Тогда
ОтключитьОбработчикОжидания("ПроверитьСтатусВыполнения");
Элементы.ГруппаПараметры.Доступность = Истина;
Элементы.КнопкаСтарт.Доступность = Истина;
ПоказатьПредупреждение(, "Обработка успешно завершена!");
ИначеЕсли РезультатОпроса.Статус = "АварийноЗавершено" Или РезультатОпроса.Статус = "Отменено" Тогда
ОтключитьОбработчикОжидания("ПроверитьСтатусВыполнения");
Элементы.ГруппаПараметры.Доступность = Истина;
Элементы.КнопкаСтарт.Доступность = Истина;
ВызватьИсключение "Выполнение операции завершилось ошибкой на сервере!";
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьДанныеВыполненияНаСервере(УИДЗадания)
Результат = Новый Структура;
Результат.Вставить("Статус", "Активно");
Результат.Вставить("ПрогрессЗаписан", Ложь);
Результат.Вставить("ДанныеПрогресса", Неопределено);
Результат.Вставить("СообщенияПользователю", Новый Массив);
Задание = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(УИДЗадания);
Если Задание <> Неопределено Тогда
Результат.Статус = Строка(Задание.Состояние);
// Получаем информацию о прогрессе
Прогресс = Задание.ПолучитьПрогресс();
Если Прогресс <> Неопределено Тогда
Результат.ПрогрессЗаписан = Истина;
Результат.ДанныеПрогресса = Прогресс;
КонецЕсли;
// Получаем накопившиеся сообщения пользователя и очищаем буфер на сервере
Сообщения = Задание.ПолучитьСообщенияПользователю(Истина);
Для Каждого Сообщение Из Сообщения Цикл
Результат.СообщенияПользователю.Добавить(Новый Структура("Текст", Сообщение.Текст));
КонецЦикла;
КонецЕсли;
Возврат Результат;
КонецФункции
Если в вашей конфигурации используется Библиотека стандартных подсистем (БСП), вам не нужно писать этот код вручную. Вы можете использовать готовую подсистему «Длительные операции». Достаточно вызвать метод ДлительныеОперацииКлиент.ОжидатьЗавершения(), который автоматически откроет стандартное диалоговое окно с бегущей строкой прогресса, обеспечит опрос сервера и корректно выведет все возникшие ошибки.
---
В современных версиях платформы 1С:Предприятие (начиная с 8.3.26) появился долгожданный штатный инструмент, который полностью избавляет разработчика от необходимости настраивать периодические таймеры и опросы сервера на клиенте. Этот механизм работает по технологии Push — сервер сам отправляет информацию на клиент в режиме реального времени.
Данная технология работает через глобальный контекст УведомленияКлиента и тип данных МенеджерУведомленийКлиента. Связь между фоновым заданием на сервере и клиентским приложением устанавливается благодаря новому свойству объекта фонового задания — НомерРодительскогоСеанса.
Проанализируем пример настройки такого сквозного оповещения:
// 1. Код на клиенте при открытии формы или инициализации процесса
&НаКлиенте
Процедура ПодключитьПриемУведомлений()
// Подключаем обработчик для конкретного ключа уведомления
ОписаниеОбработчика = Новый ОписаниеОповещения("ОбработатьСигналПрогресса", ЭтотОбъект);
УведомленияКлиента.ПодключитьОбработчик("КаналПрогрессаОбработки", ОписаниеОбработчика);
КонецПроцедуры
&НаКлиенте
Процедура ОбработатьСигналПрогресса(ИмяУведомления, ДанныеУведомления, ДополнительныеПараметры) Экспорт
// Сервер прислал данные! Обновляем интерфейс мгновенно
Если ИмяУведомления = "КаналПрогрессаОбработки" Тогда
Элементы.ИндикаторПрогресса.Значение = ДанныеУведомления.Процент;
ПоказатьОповещениеПользователя("Выполнение алгоритма",, ДанныеУведомления.Сообщение, БиблиотекаКартинок.Информация);
КонецЕсли;
КонецПроцедуры
Теперь посмотрим, как просто отправить это уведомление непосредственно из серверного контекста во время выполнения тяжелого цикла:
// 2. Код на сервере (внутри длительной транзакции или фонового задания)
Процедура ОтправитьСтатусНаКлиент(ТекущийШаг, ВсегоШагов, ИдентификаторРодительскогоСеанса)
Процент = Окр((ТекущийШаг / ВсегоШагов) * 100);
// Формируем пакет данных для отправки
ДанныеДляОтправки = Новый Структура;
ДанныеДляОтправки.Вставить("Процент", Процент);
ДанныеДляОтправки.Вставить("Сообщение", "Успешно обработано строк: " + ТекущийШаг);
// Определяем получателей. Нам нужно отправить сообщение именно в тот сеанс, откуда запустили процесс
Адресаты = Новый Массив;
Адресаты.Добавить(ИдентификаторРодительскогоСеанса);
// Отправляем уведомление. Платформа гарантирует его моментальную доставку на клиент!
УведомленияКлиента.ОтправитьУведомление("КаналПрогрессаОбработки", ДанныеДляОтправки, Адресаты);
КонецПроцедуры
Преимущества технологии: Отсутствие паразитной нагрузки на сервер постоянными HTTP-запросами от клиента «Ну что, готово?», мгновенный отклик интерфейса и гарантированная доставка сообщений даже при кратковременном разрыве сетевого соединения.
---
Если проект развернут на платформе версии ниже 8.3.26, но в системе настроена и активирована Система взаимодействия (встроенный мессенджер 1С), её также можно успешно использовать в качестве шины обмена мгновенными сообщениями между сервером и клиентом.
Принцип работы следующий:
Этот способ требует наличия развернутого сервера Системы взаимодействия (например, 1С:Диалог), но обеспечивает отличную скорость доставки сообщений без блокировки основного потока выполнения кода.
---
В процессе обсуждения на форуме прозвучала идея записывать лог выполнения в текстовый файл на сервере. Однако этот подход имеет существенные архитектурные недостатки: вам придется администрировать права доступа к сетевым папкам на сервере СУБД/приложений 1С, обрабатывать коллизии одновременного доступа к файлу и вручную писать логику его чтения.
Для целей отладки и оперативного наблюдения за работой длительного алгоритма гораздо эффективнее использовать стандартный Журнал регистрации 1С. Главная его особенность — запись в Журнал регистрации выполняется **асинхронно и вне транзакции**. Это значит, что если ваша обработка делает изменения в транзакции, которая в итоге завершится ошибкой и откатится, записи в Журнале регистрации все равно останутся сохраненными.
Реализуется это следующим образом:
// Пишем лог непосредственно в процессе выполнения серверного кода
Для Счетчик = 1 По 100000 Цикл
// Полезная логика...
Если Счетчик % 1000 = 0 Тогда
// Запись будет физически зафиксирована на диске мгновенно!
ЗаписьЖурналаРегистрации("МояТяжелаяОбработка",
УровеньЖурналаРегистрации.Информация,,,
"Успешно обработан блок. Текущий счетчик: " + Счетчик);
КонецЕсли;
КонецЦикла;
Как этим пользоваться разработчику во время отладки:
---
Мы проанализировали различные варианты решения задачи. Выбор конкретного инструмента зависит от ваших целей и версии платформы:
УведомленияКлиента. Это наиболее производительное и простое в реализации архитектурное решение.Длительные операции. Она берет на себя всю рутину по управлению фоновыми заданиями и опросу их состояния.ЗаписьЖурналаРегистрации и контролируйте ход выполнения через второй сеанс.