Обработчики событий ПослеЗаписи и ПослеЗаписиНаСервере реализованы только в форме клиентского приложения. Событие завершения транзакции легко отлавливается журналом регистрации, технологическим журналом и механизмом версионирования. Что мешает разработчикам платформы поместить этот механизм в подписки - загадка!
Такой обработчик помог бы в решении как минимум двух задач:
-
Мгновенная отправка сообщений во внешние базы, интеграционные шины, брокеры сообщений
-
Быстрая постобработка проведенных документов - формирование бухгалтерских проводок, различных трансляций в управленческий учет или МСФО
Да, эти механизмы можно реализовать и на регламентных заданиях. Цикл запуска РЗ можно выставить хоть в 5 секунд, и это будет работать. Но это как-то не спортивно:
- Лог РЗ засоряется холостыми пусками
- Сервер находится постоянно под нагрузкой, даже ночью (а вдруг бухгалтеру не спится!)
- Эффект 100%-ной загрузки процессора, когда в базе нет ни одного пользователя и происходит частый запуск регламентного задания (см. //infostart.ru/public/996126/)
Кроме того, уверен, есть бизнесы, для которых и 5 секунд непростительная задержка при передаче данных. В общем, обработчик "после завершения транзакции" явно востребован, надо искать решение!
Честно признаюсь, то что описано далее, работает на низко нагруженной базе (в пике до 200 пользователей, в среднем проводится 1-2 документа в секунду). Поэтому было бы интересно подружиться со смельчаком, который готов опробовать эту идею в более интенсивной среде.
Итак, сама идея:
У объекта Блокиро вкаДаных есть замечательное свойство - останавливать выполнение кода, пока не завершится транзакция в другом процессе, или пока не закончится время ожидания блокировки (по умолчанию это время равно 20 секундам). А что если использовать это свойство как пусковой механизм для старта постобработки сразу после завершения транзакции?
Проиллюстрирую идею на двух конкретных задачах:
Задача №1. Требуется запускать постобработку трансляции регистра бухгалтерии Хозрасчетный в управленческую подсистему сразу после событий:
-
Проведение/перепроведение документов
-
Снятие с проведения документов
-
Ручная корректировка проводок
Задача №2. Требуется запускать выгрузку во внешнюю систему сразу после записи определенных справочников и документов. Внешняя система способна обрабатывать многопоточную загрузку, единственное ограничение - нельзя параллельно загружать объект с одним идентификатором.
Для обеих задач должно выполняться важное условие - пользователи не должны почувствовать замедления в работе.
Для Задачи №1:
- Создадим общий модуль Трансляция_БУ_УУ
- Создадим подписку на событие ПриЗаписиРегБух, где источник РегистрБухгалтерииНаборЗаписей.Хозрасчетный и событие: ПриЗаписи.
- Создадим регистр сведений ОчередьТяжелыхДокументов, измерения: Док, Миллисекунды
- Создадим регламентное задание ОбработкаОчередиТяжелыхДокументов
Для Задачи №2:
- Создадим общий модуль ОбменСВнешнейСистемой
- Создадим подписку на событие ПриЗаписиДокСпр, где источниками являются справочники и документы, участвующие в выгрузке, событие: ПриЗаписи.
- Создадим регистр сведений ОчередьТяжелыхДокументов2, измерения: ДокСпр, Миллисекунды
- Создадим регламентное задание ОбработкаОчередиТяжелыхДокументов2
Далее описывается алгоритм, идентичный для обеих задач. Фразу "набор записей" можно заменить на "ссылочный объект", нюансы кода показаны в конце статьи.
В процедуре обработчика подписки установим управляемую блокировку по регистратору на набор записей регистра. Такая блокировка никому не будет мешать, кроме фонового задания, которое мы запустим следом. Подписка будет вызываться и при снятии с проведения документа, что так же важно для обнуления трансляции в УУ. Подписка будет вызываться дважды при проведении, если у документа стоит признак "Удалять движения автоматически", так конечно уже давно никто не делает, но надеюсь сервер 1С раздаёт разрешения на блокировки согласно очерёдности начала попыток блокировки, у меня проверить возможности не было.
В фоновое задание передадим ссылку Регистратора и Хеш-сумму содержимого набора записей регистра – будем считать её идентификатором версии. Для Задачи №2 тоже будем передавать хеш содержимого документа, т.к. атрибут Версия меняется только после завершения транзакции.
В процедуре фонового задания откроем программную транзакцию и в цикле будем пытаться установить блокировку на регистр по переданному регистратору. Количество циклов надо подобрать эмпирически исходя из самых плохих прогнозов времени проведения самого тяжёлого документа. Я выбрал 10 циклов, это 10 минут, исходя из 60 секунд времени ожидания блокировки данных, которое определил админ базы.
Блокировка выполняется в попытке, но ошибка типа «в этой транзакции уже происходили ошибки» исключена, так как внутри попытки нет вложенных транзакций.
Если блокировка преодолена, значит транзакция по записи набора в клиентском процессе завершилась. В этот момент пользователь побежал дальше, для него ожидание закончилось. Мы передали обработку результата завершенной транзакции в фоновое задание. В фоновом задании можно было и не открывать транзакцию и не накладывать блокировку на набор (или ссылочный объект), если мы не обрабатываем данные в совокупности с подчиненными объектами (самим документом и дочерними регистрами), и нам не нужна гарантия согласованности этой совокупности. В моём примере мы открываем транзакцию, чтобы сохранить блокировку на наборе и исключить покушения других процессов на наш набор (а для задачи №2 ещё и исключить параллельную выгрузку объекта с одинаковым идентификатором).
Сравниваем версии (Хеш-суммы) переданной и текущей:
-
Если версия та же, значит клиентская транзакция завершилась успешно - выполняем постобработку
-
Если версии различаются, то причин две:
-
был откат клиентской транзакции - значит ничего делать не надо
-
пока мы пытались установить блокировку набор записей захватил другой процесс и успешно изменил его - значит тоже ничего не делаем, им займётся фоновое задание того другого процесса
Если все циклы закончились, а мы так и не смогли установить блокировку, значит мы имеем дело с аномально тяжёлым документом, несовместимым с принципом быстрых сообщений. Помещаем ссылку регистратора в специально заготовленный регистр сведений, играющий роль очереди, которую обрабатывает регламентное задание по расписанию, скажем раз в час и только в рабочее время. Если уж пользователь был готов ждать более 10 минут проведения документа, значит это не самый срочный документ, подождёт ещё час или до утра.
Если фоновое задание получило битую ссылку регистратора - значит пользователь сразу после создания нового документа нажал кнопку «Провести», документ выдал ошибку, а транзакция откатилась – ничего страшного, управляемая блокировка легко переваривает битые ссылки! Так же не возникает исключительных ситуаций при определении Хеш-суммы: набор записей возвращает пустую коллекцию, а БитаяСсылка.ПолучитьОбъект() возвращает Неопределено, всё легко сериализуется и хешируется.
Вывод:
Я предложил вариант триггера, который запускает фоновое задание сразу после завершения транзакции. Пользователи не почувствуют увеличения времени реакции системы, так как внутри пользовательской транзакции нет никаких записей или долгих вычислений, разве что сериализация объекта для вычисления версии, которую платформа должна делать быстро, потому что делает это регулярно при передаче данных между клиентским и серверным контекстами. Транзакция и блокировка внутри фонового задания - необязательны, зависит от решаемой задачи. Варианты использования я предложил в начале статьи. Как вы распорядитесь этой возможностью - решать вам! :-) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Еще один вариант триггера.
Его идею подсказали в обсуждении под статьёй, мне она показалась интересной, но требующей предварительных испытаний под нагрузкой. Заключается в том, что если в транзакции записать новое регламентное задание с пустым расписанием, то оно выполниться сразу же, как закончится транзакция. Если транзакция откатилась, то РЗ не запишется и соответственно не выполнится. В параметры РЗ можно передать данные, которые требуется обработать сразу после завершения транзакции. А так же передать уникальный идентификатор регламентного задания, чтобы удалить его при выполнении фонового задания.
У этого варианта есть большой плюс - он не требует создания механизма ожидания окончания транзакции по таймауту и использования регистра очереди на случай превышения таймаута. Когда бы транзакция не закончилась - регламентное задание выполнится сразу же.
Основной минус такой - непонятно как поведёт себя сервер 1С в высоконагруженной базе. Если одновременно 1000 пользователей завершит транзакцию, как быстро будет запущена 1000 регламентных заданий? Боюсь что они попадут в некую очередь, и будут выполняться по мере освобождения ресурсов.
В любом случае идея интересная, и я точно буду её тестировать! :-)
|
|
|
|
|
|
|
Процедуру трансляции БУ-УУ ОбработатьДокумент( ) и процедуру выгрузки во внешнюю систему ВыгрузитьОбъект() я не привожу, это другая история, у каждого своя.
Для простоты восприятия привожу диаграмму происходящего в динамике:
//Подписка
Процедура ПриЗаписиРегБух(Источник, Отказ, РежимЗаписи) Экспорт
УстановитьПривилегированныйРежим(Истина);
ИмяПространстваБлокировки = "РегистрБухгалтерии.Хозрасчетный.НаборЗаписей";
//Устанавливаем управляемую блокировку на регистр по Регистратору
//Блокировка будет действовать до конца транзакции
//Сама по себе такая блокировка никому не мешает,
//но уводит фоновое задание ОбработатьПослеТранзакции в таймаут,
//который закончится сразу же как закончится эта транзакция
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить(ИмяПространстваБлокировки);
ЭлементБлокировки.УстановитьЗначение("Регистратор", Источник.Отбор.Регистратор.Значение);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
Парам = Новый Массив;
Парам.Добавить(ИмяПространстваБлокировки);
Парам.Добавить(Источник.Отбор.Регистратор.Значение);
Парам.Добавить(ВернутьХЕШОбъекта(Источник));
ФоновыеЗадания.Выполнить("Трансляция_БУ_УУ.ОбработатьПослеТранзакции", Парам, , "Трансляция_БУ_УУ");
УстановитьПривилегированныйРежим(Ложь);
КонецПроцедуры
Функция ВернутьХЕШОбъекта(Объект)
ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.УстановитьСтроку();
ЗаписатьXML(ЗаписьXML, Объект);
ХешированиеДанных = Новый ХешированиеДанных(ХешФункция.MD5);
ХешированиеДанных.Добавить(ЗаписьXML.Закрыть());
Возврат ХешированиеДанных.ХешСумма;
КонецФункции
//фоновое задание из подписки
Процедура ОбработатьПослеТранзакции(ИмяПространстваБлокировки, СсылкаДокумента, ХЕШНабораЗаписей) Экспорт
//Ждём освобождения управляемой блокировки в клиентском процессе.
//Каждый цикл по умолчанию 60 секунд, итого 600 секунд.
//Если ожидание более 600 секунд - отправляем документ в регистр обработки очереди по расписанию
НачатьТранзакцию();
Для НомерЦикла = 1 По 10 Цикл
Попытка
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить(ИмяПространстваБлокировки);
ЭлементБлокировки.УстановитьЗначение("Регистратор", СсылкаДокумента);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
Исключение
//Закончилось время ожидания блокировки данных, пробуем еще раз
ОтменитьТранзакцию();
НачатьТранзакцию();
Продолжить;
КонецПопытки;
//Проверяем версию записей регистра,
//- если версия та же, значит клиентская транзакция прошла успешна - выполняем постобработку
//- если версия другая, то:
//1. был откат клиентской транзакции - значит ничего делать не надо
//2. пока мы пытались установить блокировку набор записей захватил другой процесс и успешно изменил его
//значит тоже ничего не делаем, им займется фоновое задание того другого процесса
НаборЗаписей = РегистрыБухгалтерии.Хозрасчетный.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(СсылкаДокумента);
НаборЗаписей.Прочитать();
Если ХЕШНабораЗаписей = ВернутьХЕШОбъекта(НаборЗаписей) Тогда
//Останемся внутри транзакции с влюченной блокировкой, чтобы гарантированно обработать именно эту версию
ОбработатьДокумент(СсылкаДокумента);
ЗафиксироватьТранзакцию();
КонецЕсли;
Возврат;
КонецЦикла;
ОтменитьТранзакцию();
//Закончились все циклы ожидания блокировки.
//Это аномально длинная транзакция, отправляем ее в регистр обработки очереди по расписанию
Р = РегистрыСведений.ОчередьТяжелыхДокументов.СоздатьМенеджерЗаписи();
Р.Док = СсылкаДокумента;
Р.Миллисекунды = ТекущаяУниверсальнаяДатаВМиллисекундах();
Р.Записать();
КонецПроцедуры
//РеглЗадание
Процедура ОбработкаОчередиТяжелыхДокументов() Экспорт
ОбщегоНазначения.ПриНачалеВыполненияРегламентногоЗадания(Метаданные.РегламентныеЗадания.ОбработкиОчередиТяжелыхДокументов);
ОбработкаОчереди();
КонецПроцедуры
//обработка очереди документов с аномально длинной транзакцией
Процедура ОбработкаОчереди() Экспорт
//у нас версионник, записи будут видны только после завершения транзакции
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Пока Истина Цикл
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1
| Р.Док КАК Док,
| МИНИМУМ(Р.Миллисекунды) КАК МиллисекундыМин,
| МАКСИМУМ(Р.Миллисекунды) КАК МиллисекундыМакс
|ИЗ
| РегистрСведений.ОчередьТяжелыхДокументов КАК Р
|ГДЕ
| Р.Док В
| (ВЫБРАТЬ
| РР.Док
| ИЗ
| РегистрСведений.ОчередьТяжелыхДокументов КАК РР
| ГДЕ
| РР.Миллисекунды В
| (ВЫБРАТЬ
| МИНИМУМ(РРР.Миллисекунды)
| ИЗ
| РегистрСведений.ОчередьТяжелыхДокументов КАК РРР))
|
|СГРУППИРОВАТЬ ПО
| Р.Док";
Рез = Запрос.Выполнить();
Если Рез.Пустой() Тогда
Прервать;
Иначе
ВыборкаОсновная = Рез.Выбрать();
Если ВыборкаОсновная.Следующий() Тогда
Запрос.УстановитьПараметр("Док", ВыборкаОсновная.Док);
Запрос.УстановитьПараметр("МиллисекундыМин", ВыборкаОсновная.МиллисекундыМин);
Запрос.УстановитьПараметр("МиллисекундыМакс", ВыборкаОсновная.МиллисекундыМакс);
Запрос.Текст = "ВЫБРАТЬ
| Р.Док КАК Док,
| Р.Миллисекунды КАК Миллисекунды
|ИЗ
| РегистрСведений.ОчередьТяжелыхДокументов КАК Р
|ГДЕ
| Р.Док = &Док
| И Р.Миллисекунды МЕЖДУ &МиллисекундыМин И &МиллисекундыМакс
|";
ВыборкаЗаписей = Запрос.Выполнить().Выбрать();
Пока ВыборкаЗаписей.Следующий() Цикл
Р = РегистрыСведений.ОчередьТяжелыхДокументов.СоздатьМенеджерЗаписи();
Р.Док = ВыборкаЗаписей.Док;
Р.Миллисекунды = ВыборкаЗаписей.Миллисекунды;
Р.Удалить();
КонецЦикла;
ОбработатьДокумент(ВыборкаОсновная.Док);
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
//Подписка
Процедура ПриЗаписиДокСпр(Источник, Отказ) Экспорт
УстановитьПривилегированныйРежим(Истина);
ИмяПространстваБлокировки = Источник.Метаданные().ПолноеИмя();
Парам = Новый Массив;
Парам.Добавить(ИмяПространстваБлокировки);
Парам.Добавить(Источник.Ссылка);
Парам.Добавить(ВернутьХЕШОбъекта(Источник));
//Устанавливаем управляемую блокировку на объект по Ссылке
//Блокировка будет действовать до конца транзакции
//Сама по себе такая блокировка никому не мешает,
//но уводит фоновое задание ОбработатьПослеТранзакции в таймаут
//который закончится сразу же как закончится эта транзакция
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить(ИмяПространстваБлокировки);
ЭлементБлокировки.УстановитьЗначение("Ссылка", Источник.Ссылка);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
ФоновыеЗадания.Выполнить("ОбменСВнешнейСистемой.ОбработатьПослеТранзакции", Парам, , "ОбменСВнешнейСистемой");
УстановитьПривилегированныйРежим(Ложь);
КонецПроцедуры
Функция ВернутьХЕШОбъекта(Объект)
ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.УстановитьСтроку();
ЗаписатьXML(ЗаписьXML, Объект);
ХешированиеДанных = Новый ХешированиеДанных(ХешФункция.MD5);
ХешированиеДанных.Добавить(ЗаписьXML.Закрыть());
Возврат ХешированиеДанных.ХешСумма;
КонецФункции
//фоновое задание из подписки
Процедура ОбработатьПослеТранзакции(ИмяПространстваБлокировки, СсылкаОбъекта, Версия) Экспорт
//Ждём освобождения управляемой блокировки в клиентском процессе.
//Каждый цикл по умолчанию 60 секунд, итого 600 секунд.
//Если ожидание более 600 секунд - отправляем документ в регистр обработки очереди по расписанию
НачатьТранзакцию();
Для НомерЦикла = 1 По 10 Цикл
Попытка
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить(ИмяПространстваБлокировки);
ЭлементБлокировки.УстановитьЗначение("Ссылка", СсылкаОбъекта);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
Исключение
//закончилось время ожидания блокировки данных, пробуем еще раз
ОтменитьТранзакцию();
НачатьТранзакцию();
Продолжить;
КонецПопытки;
//Проверяем версию ссылочного объекта,
//- если версия та же, значит клиентская транзакция прошла успешна - выполняем постобработку
//- если версия другая, то:
//1. был откат клиентской транзакции - значит ничего делать не надо
//2. пока мы пытались установить блокировку набор записей захватил другой процесс и успешно изменил его
//значит тоже ничего не делаем, им займется фоновое задание того другого процесса
Если Версия = ВернутьХЕШОбъекта(СсылкаОбъекта.ПолучитьОбъект()) Тогда
//Останемся внутри транзакции с влюченной блокировкой, чтобы гарантированно обработать именно эту версию
ВыгрузитьОбъект(СсылкаОбъекта);
ЗафиксироватьТранзакцию();
КонецЕсли;
Возврат;
КонецЦикла;
ОтменитьТранзакцию();
//Закончились все циклы ожидания блокировки.
//Это аномально длинная транзакция, отправляем ее в регистр обработки очереди по расписанию
Р = РегистрыСведений.ОчередьТяжелыхДокументов2.СоздатьМенеджерЗаписи();
Р.ДокСпр = СсылкаОбъекта;
Р.Миллисекунды = ТекущаяУниверсальнаяДатаВМиллисекундах();
Р.Записать();
КонецПроцедуры
//РеглЗадание
Процедура ОбработкаОчередиТяжелыхДокументов2() Экспорт
ОбщегоНазначения.ПриНачалеВыполненияРегламентногоЗадания(Метаданные.РегламентныеЗадания.ОбработкаОчередиТяжелыхДокументов2);
ОбработкаОчереди();
КонецПроцедуры
Процедура ОбработкаОчереди() Экспорт
Запрос = Новый Запрос;
Пока Истина Цикл
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1
| Р.ДокСпр КАК ДокСпр,
| МИНИМУМ(Р.Миллисекунды) КАК МиллисекундыМин,
| МАКСИМУМ(Р.Миллисекунды) КАК МиллисекундыМакс
|ИЗ
| РегистрСведений.ОчередьТяжелыхДокументов2 КАК Р
|ГДЕ
| Р.ДокСпр В
| (ВЫБРАТЬ
| РР.ДокСпр
| ИЗ
| РегистрСведений.ОчередьТяжелыхДокументов2 КАК РР
| ГДЕ
| РР.Миллисекунды В
| (ВЫБРАТЬ
| МИНИМУМ(РРР.Миллисекунды)
| ИЗ
| РегистрСведений.ОчередьТяжелыхДокументов2 КАК РРР))
|
|СГРУППИРОВАТЬ ПО
| Р.ДокСпр";
Рез = Запрос.Выполнить();
Если Рез.Пустой() Тогда
Прервать;
Иначе
ВыборкаОсновная = Рез.Выбрать();
Если ВыборкаОсновная.Следующий() Тогда
Запрос.УстановитьПараметр("ДокСпр", ВыборкаОсновная.ДокСпр);
Запрос.УстановитьПараметр("МиллисекундыМин", ВыборкаОсновная.МиллисекундыМин);
Запрос.УстановитьПараметр("МиллисекундыМакс", ВыборкаОсновная.МиллисекундыМакс);
Запрос.Текст = "ВЫБРАТЬ
| Р.ДокСпр КАК ДокСпр,
| Р.Миллисекунды КАК Миллисекунды
|ИЗ
| РегистрСведений.ОчередьТяжелыхДокументов2 КАК Р
|ГДЕ
| Р.ДокСпр = &ДокСпр
| И Р.Миллисекунды МЕЖДУ &МиллисекундыМин И &МиллисекундыМакс";
ВыборкаЗаписей = Запрос.Выполнить().Выбрать();
Пока ВыборкаЗаписей.Следующий() Цикл
Р = РегистрыСведений.ОчередьТяжелыхДокументов2.СоздатьМенеджерЗаписи();
Р.ДокСпр = ВыборкаЗаписей.ДокСпр;
Р.Миллисекунды = ВыборкаЗаписей.Миллисекунды;
Р.Удалить();
КонецЦикла;
ВыгрузитьОбъект(ВыборкаОсновная.ДокСпр);
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецПроцедуры