Как прервать длительную операцию в 1С:Предприятие 8.3, выполняемую на сервере, когда ОбработкаПрерыванияПользователя() не работает?

Программист 1С v8.3 (Управляемые формы) Промышленность, строительство и АПК
← На главную

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

Почему ОбработкаПрерыванияПользователя() не работает для серверных вызовов?

Функция ОбработкаПрерыванияПользователя() в 1С предназначена для реагирования на интерактивное действие пользователя – обычно нажатие комбинации клавиш Ctrl+Break. Однако ее функциональность имеет существенные ограничения, которые необходимо понимать:

  1. Работает только на клиенте: ОбработкаПрерыванияПользователя() функционирует исключительно на стороне клиента. Она не может прервать выполнение кода, который в данный момент выполняется на сервере, поскольку серверный код работает в другом контексте и не реагирует на клиентские интерактивные действия, такие как нажатия клавиш. Если ваша длительная операция фактически выполняется в серверной процедуре или функции, вставка ОбработкаПрерыванияПользователя() в клиентский код до или после вызова серверной части не даст никакого эффекта прерывания серверного процесса.
  2. Требуется интерактивный контекст вызова: Прерывание выполнения возможно только в тех обработчиках, которые инициированы определенными интерактивными действиями пользователя. К ним относятся:
    • Нажатие кнопки в форме.
    • Выбор пункта меню или кнопки командной панели в форме.
    • Выбор пункта меню или кнопки панели инструментов интерфейса.
    • Действия, инициируемые обработчиками событий "Выбор" элементов управления.
    Например, мы можем прервать обработчик Нажатие() кнопки, но не сможем прервать обработчик формы ПриОткрытии(), если в нем выполняется длительная операция, не связанная с прямым действием пользователя.
  3. Момент прерывания: Прерывание происходит только в тот момент, когда непосредственно вызывается сама процедура ОбработкаПрерыванияПользователя(). Если внутри длительного цикла выполняется одна большая операция, например, сложный запрос к базе данных, то функция не сможет прервать ее непосредственно во время выполнения запроса. Она сработает только после завершения этой операции и перехода к следующему вызову ОбработкаПрерыванияПользователя().
  4. Проблемы в терминальных сессиях: Некоторые пользователи отмечают, что ОбработкаПрерыванияПользователя() может работать нестабильно или вовсе не работать в терминальных сессиях. Это может быть связано с особенностями передачи нажатий клавиш или долгим "зависанием" сессии, что не позволяет корректно обработать сигнал прерывания.

Рассмотрим типовой пример кода, где разработчик пытается использовать ОбработкаПрерыванияПользователя() для прерывания формирования отчета:


Пока Не ТекДень = КонецВыборки Цикл
    ОбработкаПрерыванияПользователя; // Здесь пытаемся прервать
    текДата=ТекДень;
    СоздатьНаСервере(ТекДень,общкол); // <strong >Длительная операция на сервере!</strong>
    ТекДень = ТекДень+86400;//Добавим 1 День
    текДата=ТекДень;
    СостояниеОбработки = Формат(ТекДень, "ДЛФ=D");
    инд=инд+окр(пар);
    Индикатор = инд;
    ОбновитьОтображениеДанных;
    ОбработкаПрерыванияПользователя; // И здесь тоже
КонецЦикла;

В этом примере, несмотря на наличие вызовов ОбработкаПрерыванияПользователя(), если процедура СоздатьНаСервере() выполняется долго, пользователь не сможет ее прервать, так как выполнение серверного кода блокирует клиентский поток, и функция прерывания просто не сможет быть вызвана до завершения серверного вызова.

Надежное решение: Фоновые задания для длительных серверных операций

Для длительных операций, особенно тех, которые выполняются на сервере, самым эффективным и рекомендуемым подходом является использование фоновых заданий (асинхронное выполнение). Этот механизм позволяет перенести ресурсоемкую обработку на сервер, не блокируя при этом клиентский интерфейс, и предоставить пользователю возможность мониторить состояние операции и при необходимости ее отменить.

Мы рассмотрим по шагам, как переделать ваш код для использования фоновых заданий, на примере формирования отчета производства за месяц.

  1. Шаг 1: Подготовка серверного кода для выполнения в фоновом задании

    Для того чтобы процедура или функция могла быть выполнена в фоновом задании, она должна быть размещена в общем модуле, который отмечен как &НаСервереБезКонтекста (или просто &НаСервере, если она требует контекст, но для фоновых заданий чаще используется безконтекстный вызов), и быть экспортной. Мы перенесем основную логику формирования отчета в такую серверную процедуру.

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

    
    // В ОбщемМодуле "ДлительныеОперацииСервер"
    &НаСервереБезКонтекста
    Процедура СформироватьОтчетПроизводстваФоново(ПараметрыЗадания) Экспорт
        // ПараметрыЗадания - это структура или массив с необходимыми входными данными
        // Например: ПараметрыЗадания.НачалоПериода, ПараметрыЗадания.КонецПериода, ПараметрыЗадания.ОбщаяКоллекция
            
        НачалоПериода = ПараметрыЗадания.НачалоПериода;
        КонецПериода = ПараметрыЗадания.КонецПериода;
        ОбщаяКоллекция = ПараметрыЗадания.ОбщаяКоллекция; // Если это ссылка на объект, она будет недоступна напрямую
                                                      // Нужно передавать идентификаторы или полные данные
                
        ТекДень = НачалоПериода;
        Пока ТекДень <= КонецПериода Цикл
            // Здесь мы можем вызывать вашу существующую серверную функцию
            // Если она также находится в общем модуле или доступна
            СоздатьНаСервере(ТекДень, ОбщаяКоллекция); 
                    
            // ВАЖНО: В фоновом задании нет прямого доступа к клиентским переменным формы.
            // Состояние и индикатор нужно сохранять в хранилище значений,
            // или в реквизите объекта, который мониторится клиентом.
                    
            // Пример записи прогресса для мониторинга
            ФоновыеЗадания.УстановитьПрогресс(Цел((ТекДень - НачалоПериода) / (КонецПериода - НачалоПериода) * 100));
                    
            ТекДень = ТекДень + 86400; // Добавим 1 День (секунд в дне)
        КонецЦикла;
                
        // Если нужно сохранить результат работы, можно его поместить во временное хранилище
        // или записать в базу данных.
        // Например, СсылкаНаРезультат = ПоместитьВоВременноеХранилище(ПолученныйОтчет);
        // ВозвращаемоеЗначение = СсылкаНаРезультат;
        // ФоновоеЗадание.УстановитьВыполненноеЗадание(ВозвращаемоеЗначение); // Внутри фонового задания
    КонецПроцедуры
            
    // Предполагается, что ваша процедура СоздатьНаСервере() определена в этом же или доступном серверном модуле.
    // Если СоздатьНаСервере() — это функция, которая возвращает что-то для каждого дня,
    // то СформироватьОтчетПроизводстваФоново должна собирать эти результаты.
    // Если СоздатьНаСервере() непосредственно пишет данные в базу, то изменений не требуется.
    &НаСервереБезКонтекста
    Процедура СоздатьНаСервере(ТекДень, ОбщКол) Экспорт
        // Ваш текущий код обработки для одного дня
        // ...
    КонецПроцедуры
    
  2. Шаг 2: Запуск фонового задания с клиента

    Теперь мы можем запустить эту длительную операцию с клиентской стороны, не блокируя интерфейс. Для этого используем объект ФоновыеЗадания.

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

    Пользователю необходимо видеть прогресс выполнения операции и иметь возможность ее отменить. Для этого мы будем периодически проверять состояние фонового задания с клиента с помощью обработчика ожидания.

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

    Ключевое преимущество фоновых заданий — возможность их отмены. Мы добавим на форму кнопку "Отмена" и соответствующий обработчик.

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

Дополнительные рекомендации при работе с фоновыми заданиями

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

← На главную