При разработке механизмов проведения документов в 1С:Предприятие программисты часто сталкиваются с фундаментальной особенностью платформы: транзакционностью. Если в процессе выполнения программного кода в обработчике ОбработкаПроведения возникает ошибка или вызывается отказ, вся транзакция откатывается. Это означает, что любые данные, которые мы пытались записать в базу (включая технические логи в регистры сведений), бесследно исчезают вместе с отменой проведения документа.
Рассмотрим ситуацию: документ должен отправить данные во внешнюю систему (например, в Oracle или через API), и в случае неудачи мы хотим зафиксировать ошибку в специальном регистре «Логирование», но при этом само проведение документа должно прерваться. Разберем по шагам, как решить эту задачу, проанализируем различные подходы и выясним, какой из них наиболее надежен в промышленной эксплуатации.
Прежде всего, проанализируем, почему стандартный блок Попытка...Исключение внутри процедуры ОбработкаПроведения не помогает записать лог в регистр. Когда выполнение кода находится внутри транзакции (а обработка проведения всегда открывает неявную транзакцию), любая ошибка уровня базы данных (например, нарушение уникальности или блокировка) помечает транзакцию как «битую» или doomed. В этом состоянии любая последующая попытка записи в базу данных из этого же сеанса вызовет исключение: «В данной транзакции уже происходили ошибки».
Даже если мы перехватим ошибку, выполнение команды НаборЗаписей.Записать() внутри блока Исключение либо ничего не даст (так как в конце всё равно произойдет ОтменитьТранзакцию()), либо вызовет новую критическую ошибку.
Как было отмечено в ходе обсуждения, наиболее эффективным способом «выпрыгнуть» из текущей транзакции является запуск фонового задания. Рассмотрим, почему это работает: фоновое задание порождает новый сеанс на сервере 1С. У этого сеанса своя собственная транзакционная область, которая никак не связана с транзакцией документа, из которого оно было вызвано.
Посмотрим на пример реализации такого метода. В обработчике проведения при возникновении ошибки мы вызываем экспортную процедуру общего модуля через механизм фоновых заданий:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
Попытка
// Логика взаимодействия с внешней системой
Результат = ИнтеграцияСВнешнейСистемой.ОтправитьДанные(ЭтотОбъект);
Исключение
// Ошибка произошла, нам нужно ее залогировать и прервать проведение
ОписаниеОшибки = ИнформацияОбОшибке().Описание;
// Запускаем фоновое задание для записи лога
ПараметрыЛога = Новый Массив;
ПараметрыЛога.Добавить(ЭтотОбъект.Ссылка);
ПараметрыЛога.Добавить(ОписаниеОшибки);
ФоновыеЗадания.Выполнить("ЛогированиеСервер.ЗаписатьОшибкуВРегистр", ПараметрыЛога);
// Прерываем проведение
Отказ = Истина;
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Ошибка при обмене: " + ОписаниеОшибки;
Сообщение.Сообщить();
КонецПопытки;
КонецПроцедуры
Разберем важный нюанс: фоновое задание должно выполнять запись в базу данных максимально автономно. Поскольку основная транзакция еще не завершена (документ еще не записан с отказом), фоновое задание не должно пытаться прочитать данные самого документа из базы, если они еще не зафиксированы. Лучше передавать все необходимые данные для лога в параметрах задания.
Самый простой и встроенный в платформу способ логирования, который часто недооценивают. Метод ЗаписьЖурналаРегистрации() по своей природе является не транзакционным. Это означает, что если вы вызываете запись в ЖР внутри транзакции, и эта транзакция впоследствии откатывается, запись в журнале все равно остается.
Рассмотрим преимущества этого подхода:
Если вам обязательно нужно иметь данные в регистре сведений для удобства построения отчетов, вы можете использовать комбинированную схему: записывать ошибку в ЖР, а специальным регламентным заданием раз в час «перекладывать» новые записи из ЖР в ваш регистр логов.
Исключение
ОписаниеОшибки = ОписаниеОшибки();
ЗаписьЖурналаРегистрации("ОбменДанными", УровеньЖурналаРегистрации.Ошибка, , Ссылка, ОписаниеОшибки);
Отказ = Истина;
КонецПопытки;
Существует еще один продвинутый метод, который позволяет избежать создания фоновых заданий, но сохранить запись именно в регистре. Мы можем использовать ПараметрыСеанса. Параметры сеанса хранятся в памяти сервера и не очищаются при откате транзакции базы данных.
Проанализируем алгоритм:
Исключение обработчика проведения мы записываем структуру с данными ошибки в параметр сеанса (например, в заранее созданный параметр БуферОшибокПроведения типа ХранилищеЗначения).Для высоконагруженных систем (Enterprise-уровня) хорошим тоном считается использование внешнего или внутреннего HTTP-сервиса для сбора логов. Проанализируем, почему это выгодно:
Когда вы выполняете HTTP-запрос (через HTTPСоединение) к стороннему ресурсу или даже к этой же базе 1С, этот запрос выполняется вне контекста текущей транзакции. Вы передаете JSON с описанием ошибки, и удаленный сервис сохраняет его. Даже если ваша база данных «упадет» сразу после этого, информация об ошибке уже будет зафиксирована на другом сервере или в другом процессе.
Подводя итоги, выберем оптимальный путь в зависимости от задачи:
ЗаписьЖурналаРегистрации().Важное предостережение: Будьте осторожны с фоновыми заданиями при групповом перепроведении документов. Если вы попытаетесь перепровести 10 000 документов и в каждом возникнет ошибка, вызывающая фоновое задание, сервер может временно перестать отвечать из-за лавинообразного создания новых сеансов.