При разработке прикладных решений часто возникает необходимость выполнить определенное действие не сразу, а спустя некоторый промежуток времени. Классический пример — автоматическая смена статуса документа через фиксированный интервал (например, перевод рейса в статус РейсЗавершен ровно через 1 час после статуса СовершилПосадку).
Попытка решить эту задачу «в лоб» через клиентский метод ПодключитьОбработчикОжидания часто приводит разработчиков к двум проблемам: непониманию, как передать контекст (ссылку на документ) в вызываемую процедуру, и архитектурным сбоям, когда сеанс пользователя закрывается раньше времени. Рассмотрим подробно ошибки исходного подхода, правильный способ передачи параметров в обработчик ожидания, а также надежные серверные альтернативы.
Проанализируем исходную попытку передачи ссылки через временное хранилище с фиксированным GUID:
Функция ЗапускОбработчикаОжидания(ДокументСсылка) Экспорт
СтрокаGUID = "a763cfbb-f94f-4c67-8e13-0e96a3a7f353";
НовыйGUID = Новый УникальныйИдентификатор(СтрокаGUID);
Адрес = ПоместитьВоВременноеХранилище("212121115", НовыйGUID);
ПодключитьОбработчикОжидания("ВыполнениеОбработчикаОжидания", 5, Истина);
КонецФункции
&НаКлиенте
Процедура ВыполнениеОбработчикаОжидания() Экспорт
СтрокаGUID = "e1cib/tempstorage/fe5a376e-4e52-41b0-a3d5-5637380d3f5a";
Ссылка = ПолучитьИзВременногоХранилища(СтрокаGUID);
КонецПроцедуры
В этом коде допущено сразу несколько критических ошибок:
ПоместитьВоВременноеХранилище возвращает сформированный системой внутренний URL (например, e1cib/tempstorage/...). Передача второго параметра НовыйGUID не делает адрес хранилища равным этому GUID, а лишь определяет область жизни данных.ПолучитьИзВременногоХранилища возвращает Неопределено.Если задача действительно требует выполнения логики на клиенте (например, визуальное оповещение пользователя), передавать данные через временное хранилище или глобальные переменные не требуется. В платформе 1С:Предприятие 8.3 метод ПодключитьОбработчикОжидания поддерживает работу с объектом ОписаниеОповещения.
Разберем, как передать структуру параметров прямо в процедуру-обработчик:
&НаКлиенте
Процедура ЗапуститьТаймер(ДокументСсылка) Экспорт
ПараметрыВызова = Новый Структура;
ПараметрыВызова.Вставить("Документ", ДокументСсылка);
ПараметрыВызова.Вставить("ВремяСтарта", ТекущаяДата());
Оповещение = Новый ОписаниеОповещения("ОбработатьТаймер", ЭтотОбъект, ПараметрыВызова);
// Подключаем однократный обработчик на 5 секунд
ПодключитьОбработчикОжидания(Оповещение, 5, Истина);
КонецПроцедуры
&НаКлиенте
Процедура ОбработатьТаймер(ДополнительныеПараметры) Экспорт
Если ДополнительныеПараметры = Неопределено Тогда
Возврат;
КонецЕсли;
ДокументСсылка = ДополнительныеПараметры.Документ;
// Выполняем требуемые действия на сервере
ВыполнитьСменуСтатусаНаСервере(ДокументСсылка);
КонецПроцедуры
Ограничение подхода: клиентский обработчик ожидания подходит только для коротких интервалов (секунды, несколько минут). Если интервал составляет 1 час, этот подход категорически не подходит. Если пользователь закроет приложение, заблокирует компьютер или произойдет обрыв сети, клиентский таймер сбросится, и статус документа никогда не изменится.
Когда действие должно гарантированно произойти на сервере через длительное время (30 минут, 1 час, сутки), применяют механизм очереди задач на базе непериодического регистра сведений и регламентного задания.
Разберем реализацию по шагам:
ОчередьСменыСтатусов:
Измерения: Документ (Тип: ДокументСсылка.Рейс).
Реквизиты: ПлановоеВремя (Тип: Дата), ЦелевойСтатус (Тип: ПеречислениеСсылка.СтатусыРейсов).
В момент перевода документа в промежуточный статус записываем в регистр задание на выполнение через 3600 секунд:
Процедура ЗапланироватьСменуСтатуса(ДокументСсылка) Экспорт
МенеджерЗаписи = РегистрыСведений.ОчередьСменыСтатусов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Документ = ДокументСсылка;
МенеджерЗаписи.ПлановоеВремя = ТекущаяДатаСеанса() + 3600; // Ровно через 1 час
МенеджерЗаписи.ЦелевойСтатус = Перечисления.СтатусыРейсов.РейсЗавершен;
МенеджерЗаписи.Записать(Истина);
КонецПроцедуры
Создаем регламентное задание с расписанием (выполнять раз в 1 минуту), вызывающее процедуру общего модуля:
Процедура ОбработатьОчередьСтатусов() Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Очередь.Документ КАК Документ,
| Очередь.ЦелевойСтатус КАК ЦелевойСтатус
|ИЗ
| РегистрСведений.ОчередьСменыСтатусов КАК Очередь
|ГДЕ
| Очередь.ПлановоеВремя <= &ТекущееВремя";
Запрос.УстановитьПараметр("ТекущееВремя", ТекущаяДатаСеанса());
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.ОчередьСменыСтатусов");
ЭлементБлокировки.УстановитьЗначение("Документ", Выборка.Документ);
НачатьТранзакцию();
Попытка
Блокировка.Заблокировать();
// Проверяем актуальность состояния перед изменением
ДокументОбъект = Выборка.Документ.ПолучитьОбъект();
Если ДокументОбъект <> Неопределено И ДокументОбъект.Статус = Перечисления.СтатусыРейсов.СовершилПосадку Тогда
ДокументОбъект.Статус = Выборка.ЦелевойСтатус;
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
КонецЕсли;
// Удаляем запись из очереди
МенеджерЗаписи = РегистрыСведений.ОчередьСменыСтатусов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Документ = Выборка.Документ;
МенеджерЗаписи.Удалить();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("ФоноваяСменаСтатуса", УровеньЖурналаРегистрации.Ошибка, , Выборка.Документ, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
КонецЦикла;
КонецПроцедуры
Если бизнес-логика не требует повторного перепроведения документа и генерации проводок через 1 час, а статус используется исключительно для отображения в формах и отчетах, можно обойтись без регламентных заданий.
Для этого используется периодический регистр сведений (периодичность по позиции регистратора или по секундам). В момент совершения посадки в регистр сразу вносятся две записи:
Процедура ЗафиксироватьСтатусыВРегистре(ДокументСсылка) Экспорт
НаборЗаписей = РегистрыСведений.СтатусыДокументов.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Документ.Установить(ДокументСсылка);
// Запись 1: текущее состояние
Запись1 = НаборЗаписей.Добавить();
Запись1.Период = ТекущаяДатаСеанса();
Запись1.Документ = ДокументСсылка;
Запись1.Статус = Перечисления.СтатусыРейсов.СовершилПосадку;
// Запись 2: будущее состояние через 1 час
Запись2 = НаборЗаписей.Добавить();
Запись2.Период = ТекущаяДатаСеанса() + 3600;
Запись2.Документ = ДокументСсылка;
Запись2.Статус = Перечисления.СтатусыРейсов.РейсЗавершен;
НаборЗаписей.Записать(Истина);
КонецПроцедуры
При таком подходе статус документа в любой форме, списке или отчете определяется виртуальной таблицей СрезПоследних(&Период). До наступления назначенного часа срез будет возвращать статус СовершилПосадку, а как только пройдет ровно 60 минут, срез автоматически начнет отдавать РейсЗавершен без участия фоновых заданий или клиентских сеансов.