Описание диагностик из проектов BSL Language Server и SonarQube 1C (BSL) Plugin
Содержание:
- 0. Общепринятый паттерн
- 1. Метод "НачатьТранзакцию" должен располагаться непосредственно перед оператором "Попытка"
- 2. Метод "ЗафиксироватьТранзакцию" должен идти последним в блоке "Попытка"
- 3. Метод "ОтменитьТранзакцию" должен идти первым в блоке "Исключение"
- 4. Необоснованное использование метода ТранзакцияАктивна()
- 5. При обработке исключений необходимо использовать метод ЗаписьЖурналаРегистрации()
- 6. Необходимо обязательно указывать 1, 2 и 5 параметр метода ЗаписьЖурналаРегистрации()
- 7.Обращение к внешним ресурсам внутри транзакции вызывает проблемы производительности
- 8. Внутри транзакции недопустимо подавлять ошибки, вызывающие событие SDBL Func='setRollbackOnly'
- 9. При использовании вложенных транзакций в конце блока Исключение рекомендуется добавить оператор ВызватьИсключение
- 10. Дополнительные параграфы
Общепринятый паттерн
Описан в ИТС (п.1.3)
НачатьТранзакцию();
Попытка
... // чтение или запись данных
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
... // дополнительные действия по обработке исключения
КонецПопытки;
1. Метод "НачатьТранзакцию" должен располагаться непосредственно перед оператором "Попытка"
Описание диагностики
Метод НачатьТранзакцию должен быть за пределами блока Попытка-Исключение непосредственно перед оператором Попытка. (с) ИТС: Транзакции: правила использования, пункт 1.3
Начало транзакции и ее фиксация (отмена) должны происходить в контексте одного метода.
Код, который начинает транзакцию, обязан завершить или откатить ее. В случаях когда метод НачатьТранзакцию() находится внутри блока Попытка-Исключение есть риск нарушения парности вызовов НачатьТранзакцию()-ЗафиксироватьТранзакцию(), что может привести к трудно анализируемым ошибкам времени выполнения типа "В этой транзакции уже происходили ошибки."
Неправильно:
Процедура Пример2()
НачатьТранзакцию(); // <-- Ошибка: код перед попыткой
Метод();
Попытка
Метод2();
Исключение
ОтменитьТранзакцию();
Возврат;
КонецПопытки;
ЗафиксироватьТранзакцию();
КонецПроцедуры
Процедура Пример3()
Попытка
НачатьТранзакцию(); // <-- Ошибка: в попытке
Метод();
Исключение
Если ТранзакцияАктивна() Тогда
ЗафиксироватьТранзакцию();
Иначе
ОтменитьТранзакцию();
КонецЕсли;
Возврат;
КонецПопытки;
КонецПроцедуры
На 3 из 4 поддерживаемых СУБД, при вызове НачатьТранзакцию платформа идет в СУБД и открывает там транзакцию. В этот момент может произойти все что угодно: нехватка памяти для очередного соединения с СУБД, разрыв связи, ошибка дисковой подсистемы.
Давайте делать, как написано в документации: НачатьТранзакцию до начала Попытки, Зафиксировать — внутри, Откатить — в обработке исключения. Стандартная для всех языков практика, в общем-то.
Хотя науке и неизвестны случаи выброса перехватываемых исключений из НачатьТранзакцию().
Источник: Стандарт: Транзакции: правила использования
2. Метод "ЗафиксироватьТранзакцию" должен идти последним в блоке "Попытка"
Описание диагностики
Метод 'ЗафиксироватьТранзакцию' должен идти последним в блоке 'Попытка' перед оператором 'Исключение', чтобы гарантировать, что после ЗафиксироватьТранзакцию не возникнет исключение.
Неправильно:
Процедура Пример2()
НачатьТранзакцию();
Попытка
Метод();
Исключение
ОтменитьТранзакцию();
Возврат;
КонецПопытки;
ЗафиксироватьТранзакцию(); // <-- Ошибка: вне попытки
КонецПроцедуры
Процедура Пример3()
НачатьТранзакцию();
Попытка
Метод();
Исключение
Если ТранзакцияАктивна() Тогда
ЗафиксироватьТранзакцию(); // <-- Ошибка: в исключении
Иначе
ОтменитьТранзакцию();
КонецЕсли;
Возврат;
КонецПопытки;
КонецПроцедуры
Тем более! неправильно:
НачатьТранзакцию();
ДокументОбъект.Записать(РежимЗаписиДокумента.Запись);
Попытка
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
Инфо = ИнформацияОбОшибке();
ВызватьИсключение ПодробноеПредставлениеОшибки(Инфо);
КонецПопытки;
Источник: Стандарт: Транзакции: правила использования
3. Метод "ОтменитьТранзакцию" должен идти первым в блоке "Исключение"
Описание диагностики
В блоке Исключение нужно сначала вызвать метод ОтменитьТранзакцию, а затем выполнять другие действия, если они требуются.
Такое правило необходимо, чтобы убрать потенциальную возможность выброса исключения в блоке "Исключение", что может привести к тому, что метод "ОтменитьТранзакцию" не будет вызван.
Если в Метод2() будет обращение к БД (явное или неявное) - это вызовет ошибку "В данной транзакции уже происходили ошибки"
Примеры
Неправильно:
Процедура ЗаписатьЭлемент()
НачатьТранзакцию();
Попытка
Метод();
ЗафиксироватьТранзакцию();
Исключение
Метод2(); // <-- Ошибка: код перед отменой
ОтменитьТранзакцию();
КонецПопытки;
КонецПроцедуры
Источник: Стандарт: Транзакции: правила использования
4. Необоснованное использование метода ТранзакцияАктивна()
При жестком соблюдении правил работы с транзакциями использование метода ТранзакцияАктивна() в блоке Исключение становится лишним.
Требует обоснования:
НачатьТранзакцию();
Попытка
ДелаемЧтоТо();
ЗафиксироватьТранзакцию();
Исключение
Если ТранзакцияАктивна() Тогда
ОтменитьТранзакцию();
КонецЕсли;
ЗаписьЖурналаРегистрации();
КонецПопытки;
Использование данного паттерна нарушает инкапсуляцию и приводит к "размазыванию" логики управления транзакциями.
На нашем уровне абстракции мы обязаны заботиться только о нашей транзакции. Все прочие должны быть нам неинтересны. Они чужие, мы не должны нести за них ответственность. Именно НЕ ДОЛЖНЫ. Нельзя предпринимать попыток выяснения реального уровня счетчика транзакций. (с) Вы не умеете работать с транзакциями
Если считаем что это необходимо - просто игнорим срабатывание правила
Примечание: с помощью метода ТранзакцияАктивна() нельзя узнать что транзакция сломана
5. При обработке исключений необходимо использовать метод ЗаписьЖурналаРегистрации()
Недопустимо перехватывать любые исключения, бесследно для системного администратора.
Неправильно
Попытка
// код, приводящий к вызову исключения
....
Исключение // перехват любых исключений
КонецПопытки;
Как правило, подобная конструкция скрывает реальную проблему, которую впоследствии невозможно диагностировать.
Правильно
Попытка
// код, приводящий к вызову исключения
....
Исключение
// Пояснение причин перехвата всех исключений "незаметно" от пользователя.
// ....
// И запись события в журнал регистрации для системного администратора.
ЗаписьЖурналаРегистрации(НСтр("ru = 'Выполнение операции'"),
УровеньЖурналаРегистрации.Ошибка,,,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
Источник: Перехват исключений в коде
6. Необходимо обязательно указывать 1, 2 и 5 параметр метода ЗаписьЖурналаРегистрации()
Нельзя пропускать 1й параметр. Нельзя указывать его и переменной строкой - это вызывает раздувание словаря ЖР (1Cv8.lgf) и, как следствие, зависание при его открытии.
Нельзя пропускать 2й параметр - Уровень журнала регистрации. Если его не указать, по умолчанию 1С применит уровень ошибки Информация, и данная запись может потеряться в потоке записей.
Нельзя пропускать и 5й параметр - комментарий к событию записи в журнал регистрации. При обработке исключений обязательно нужно выполнять запись в журнал регистрации с полным представлением ошибки.
Неправильно:
ЗаписьЖурналаРегистрации("Событие");// ошибка
ЗаписьЖурналаРегистрации("Событие" + Ссылка); // ошибка
ЗаписьЖурналаРегистрации("Событие", УровеньЖурналаРегистрации.Ошибка);// ошибка
ЗаписьЖурналаРегистрации("Событие", , , , ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));//ошибка
ЗаписьЖурналаРегистрации("Событие", УровеньЖурналаРегистрации.Ошибка, , , ОписаниеОшибки());//ошибка
ОписаниеОшибки() и КраткоеПредставлениеОшибки() не содержат текста строки, вызвавшей ошибку. Код может измениться к моменту анализа ошибки, и придется дополнительно исследовать, где же именно возникла ошибка.
ТекстОшибки = ОписаниеОшибки(); // <-- Ошибка: отсутствует текст строки, вызвавшей ошибку
// {ОбщийМодуль.ОбщегоНазначения.Модуль(2)}: Деление на 0
ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
//{ОбщийМодуль.ОбщегоНазначения.Модуль(2)}: Деление на 0
// й=1/0; <-- присутствует текст строки, вызвавшей ошибку
// для 8.3.15+ <-- присутствует стек
ТекстОшибки =ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
//Деление на 0
//{ОбщийМодуль.ОбщегоНазначения.Модуль(2)}:й=1/0;
//{ВнешняяОбработка.ВнешняяОбработка1.Форма.Форма.Форма(61)}:А = ОбщегоНазначения.ЗначениеРеквизитаОбъекта();
7. Обращение к внешним ресурсам внутри транзакции вызывает проблемы производительности
Крайне не рекомендуется помещать вызов внешних ресурсов\сервисов внутри транзакции, явной или неявной.
Подобные вызовы могут значительно увеличить время выполнения транзакций, а это может повлечь за собой различные проблемы с производительностью, блокировкой, параллельной работой пользователей.
В качестве внешних ресурсов следует рассматривать любые ресурсы, которыми напрямую не управляет сервер 1С.
- Файловая система
- http-, web-сервисы
- ftp
- com-вызовы в Windows
- обращения к сторонним СУБД
- и т.п.
Нужно учитывать
- как явные транзакции - НачатьТранзакцию
- так и неявные - внутри системных событий 1С
- например, код внутри события ПередЗаписью, ОбработкаПроведения и т.п.
Неправильно:
// Подписка ПриЗаписи - транзакция открыта
Процедура усВыгрузитьДокументПриЗаписи(Источник, Отказ) Экспорт
// ...
// Подключение к внешнему веб-сервису в транзакции - ошибка!
// при недоступности которого транзакция зависнет
WSПрокси = Новый WSПрокси(WSОпределение, URIПространстваИмен, ИмяСервиса); // без таймаута - ошибка!
// ... или
Файл.Записать(); // обращение к файловой системе в транзакции - ошибка!
// ... или
Почта.Отправить(); // обращение к SMTP серверу в транзакции - ошибка!
КонецПроцедуры
Правильно:
Вынести обращение к внешним ресурсам за пределы транзакции
Чего только не находилось за последние несколько лет в транзакциях проведения документов или записи набора регистров:
- Предупреждение() или Вопрос() - это самое любимое
- вывод на печать на принтер (ага, прям в транзакции.. и потом появляются претензии "на бумаге написано что в документе одно, а в базу залезешь - там другое" - это при откате таких блокирующих транзакций)
- разнообразные файловые операции
- обращения к другим базам через com-объекты
- обращения к другим базам посредством Новый COMОбъект("ADODB.Connection") (ага, а на соседнем сервере уже запрос через то самое ADODB... тоже висит на блокировке! и такое было..)
- работа с ftp
- обращение к web-сервису
- запуск на исполнение сторонних программ
Источники:
- Транзакции: правила использования - Стандарт 1C
- Особенности использования транзакций при обмене данными - Методические рекомендации по конфигурированию от 1C
8. Внутри транзакции недопустимо подавлять ошибки, вызывающие событие SDBL Func='setRollbackOnly'
ИТС: Ошибки базы данных и транзакции
Если ошибка базы данных произошла в процессе выполнения транзакции, то транзакция уже не может быть продолжена или зафиксирована. Единственная операция с базой данных, которую можно произвести в такой ситуации - это отмена транзакции и проброс исключения.
Неправильно:
НачатьТранзакцию();
Попытка
// ...
Попытка
Объект.Записать(); // ПриЗаписи Отказ = Истина - вызовет setRollbackOnly
Исключение
ЗаписьЖурналаРегистрации();
КонецПопытки;
ЗафискироватьТранзакцию(); // <-- Ошибка: транзакция завершится откатом, исключение выдано не будет
Исключение
ОтменитьТранзакцию();
// ...
КонецПопытки;
Метод ЗафиксироватьТранзакцию() при глубине = 1 не всегда фиксирует фактическую транзакцию, а закрывает ее путем отката если она сломана и путем фиксации если она не сломана.
Без проброса исключения либо команда "ЗафискироватьТранзакцию();" отменит транзакцию, либо следующее обращение к базе данных вызывает ошибку «В данной транзакции уже происходили ошибки».
Неправильно:
Процедура ОбработкаПроведения()
Попытка
...запись в базу с ошибкой
Исключение
//по стандарту должно быть исключение, т.к. есть внешняя транзакция
//но его не было
//ВызватьИсключение;
КонецПопытки;
// <-- Ошибка: "В данной транзакции уже происходили ошибки!"
КонецПроцедуры
В каких случаях подавление ошибок делает транзакцию "сломанной":
- Вызов метода ОтменитьТранзакцию() внутри "вложенной" транзакции
- Подавление ошибки или отказа в методе Записать() в коде в транзакции
- Подавление ошибки при выполнении некорректного запроса, вида "ВЫБРАТЬ 1/0" в транзакции
Событие в технологическом журнале:
20:43.385010-1,
SDBL,5,process=1CV8,OSThread=12256,Usr=DefUser,DBMS=DBV8DBEng, DataBase=InfoBase75, Trans=1, Func=setRollbackOnly - установка флага наличия в транзакции ошибки (ее можно только откатить)
Дополнение от 12.05.2023:
Неправильно:
// неявная транзакция
Процедура ОбработкаПроведения() // или ПриЗаписи() или ПередЗаписью()
Для Каждого КорректировкаРеализации Из МассивКорректировок Цикл
Попытка
КорректировкаРеализации.Записать(РежимЗаписиДокумента.Проведение);
// неправильно, при ошибке записи внутри транзакции дальнейшая обработка
// документов в этой транзакции вызовет ошибку "В данной транзакции уже происходили ошибки!"
Исключение
Инфо = ИнформацияОбОшибке();
ОписаниеОшибки = ПодробноеПредставлениеОшибки(Инфо);
ЗаписьЖурналаРегистрации("СозданиеКорректировки", УровеньЖурналаРегистрации.Ошибка,
, ЗаявкаНаВозвратОтПокупателя, ОписаниеОшибки);
// неправильно, передача "ЗаявкаНаВозвратОтПокупателя" в параметр "Данные" делает
// неявный запрос к БД что вызовет ошибку "В данной транзакции уже происходили ошибки!"
КонецПопытки
КонецЦикла;
КонецПроцедуры
Неправильно:
// явная транзакция
Процедура ЗагрузкаИзВМС()
НачатьТранзакцию();
Попытка
Для Каждого КорректировкаРеализации Из МассивКорректировок Цикл
Попытка
КорректировкаРеализации.Записать(РежимЗаписиДокумента.Проведение);
Исключение
// ...
КонецПопытки
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
// ...
КонецПопытки;
КонецПроцедуры
Использование такого рода кода приводит к ошибке "В данной транзакции уже происходили ошибки!"
нельзя подавлять ошибки при работе внутри транзакции!
Ещё один вариант этого правила:
9. При использовании вложенных транзакций в конце блока Исключение рекомендуется добавить оператор ВызватьИсключение
Источник: ИТС: Транзакции: правила использования
Как известно, «1С:Предприятие 8» не поддерживает вложенных транзакций.
Это значит что, фактически, поддерживается только один уровень транзакции. То есть не существует возможности отменить действие транзакции некоторого уровня, не отменяя транзакции вышестоящего уровня.
Менеджер транзакции содержит признак “Отменена”. Если он установлен, то транзакция считается сломанной и фактическая транзакция подлежит отмене при ее любом завершении. Устанавливается он при возникновении ошибки базы данных и при вызове ОтменитьТранзакцию(). Явно получить значение признака “Отменена” менеджера транзакции во встроенном языке нельзя. (с) Безопасная работа с транзакциями во встроенном языке
Правильно:
НачатьТранзакцию();
Попытка
// блокировки, чтение, запись
// ..
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение; // есть внешняя транзакция
КонецПопытки
Общее правило состоит в следующем: если при выполнении транзакции имели место ошибки базы данных, то следует отменить всю транзакцию в целом и, в случае необходимости, повторить попытку ее выполнения с самого начала.
Если внутри транзакции произошла исключительная ситуация, она откатывается. Если внутри транзакции были вложенные транзакции или она сама являлась вложенной, откатываются все транзакции, независимо от того, на каком из уровней вложенности это произошло.
10. Дополнительные параграфы
10.1 При использовании оператора ВызватьИсключение необходимо сохранять стек ошибок
О вложенных попытках, исключениях и о представлении ошибок
10.2 Объектное чтение набора записей в транзакции устанавливает неявную управляемую разделяемую блокировку
Это может вызвать взаимоблокировку в параллельных транзакциях
10.3 Чтение данных в транзакции с их последующим изменением, необходимо производить после установки исключительной управляемой блокировки
ИТС: Ответственное чтение данных
10.4 Почему не рекомендуется передавать текст ошибки вместо непосредственной записи в ЖР?
10.5 Когда нужно использовать оператор ВызватьИсключение в блоке Исключение...КонецПопытки без параметра?
10.6 Почему нужно использовать блокировки при многопоточной загрузке данных?
10.7 Какие действия будут отменены в результате выполнения ОтменитьТранзакцию()
Код на встроенном языке содержит одну транзакцию, вложенную в другую. Какие действия будут отменены в результате выполнения ОтменитьТранзакцию() в коде вложенной транзакции?
Ответы:
- Будут отменены и вложенная и внешняя транзакция
- Только внешняя транзакция. ОтменитьТранзакцию() во вложенных транзакциях не работает, т.к. они не поддерживаются
- Т.к. вложенные транзакции технологическая платформа не поддерживает, то в начале вложенной транзакции возникнет ошибка
- Только вложенная транзакция. ОтменитьТранзакцию() во внешней транзакции нужно выполнять отдельно.
10.8 Использование конструкции Попытка...Исключение...КонецПопытки внутри транзакции:
- Не имеет смысла, транзакция при возникновении любой исключительной ситуации все равно откатывается
- Не всегда оправданно, транзакция откатывается, если исключительная ситуация определена как восстановимая
- Иногда оправданно, транзакция не откатывается, если исключительная ситуация не определена как восстановимая
- Мешает понять, что на самом деле происходит, если исключение не логируется
- Верны ответы 1 и 4
- Верны ответы 2, 3 и 4
10.9 При использовании конструкции Попытка...Исключение… КонецПопытки внутри вложенной транзакции, если внутри этой конструкции возникла восстановимая исключительная ситуация:
- при возврате в транзакцию верхнего уровня сведения об исключительной ситуации уже не передадутся.
- при возврате в транзакцию верхнего уровня исключительная ситуация будет представлена как откат вложенной транзакции, что будет расценено как невосстановимая ошибка.
- на уровне общей транзакции исключительная ситуация также будет расценена как восстановимая.
10.10 При использовании конструкции Попытка...Исключение… КонецПопытки снаружи вложенной транзакции, если внутри этой транзакции возникла восстановимая исключительная ситуация:
- на уровне общей транзакции исключительная ситуация также будет расценена как восстановимая.
- при возврате в транзакцию верхнего уровня сведения об исключительной ситуации уже не передадутся.
- при возврате в транзакцию верхнего уровня исключительная ситуация будет представлена как откат вложенной транзакции, что будет расценено как невосстановимая ошибка.
Ответ на вопрос разобран в примере в источнике: Филиппов Е.В., Настольная книга 1С:Эксперта по технологическим вопросам. 2-е издание, стр. 49-50:
"Пример 2
Сначала в процедуру ПриЗаписи модуля справочника Организации добавим строку:
ЭтотОбъект.ВыполнитьНесуществующийМетод(); // (такого метода у объекта не создавали)
Далее изменим код из примера 1:
НачатьТранзакцию();
Попытка
СпрСсылка = Справочники.Организации.НайтиПоКоду("000001");
СпрОбъект = СпрСсылка.ПолучитьОбъект();
СпрОбъект.НаименованиеПолное = ТекущаяДата();
//это чтобы отследить, зафиксирована транзакция или нет
СпрОбъект.Записать(); // неявная транзакция, там, как помним, ошибка
Исключение
КонецПопытки;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПлатежноеПоручение.Ссылка
|ИЗ
| Документ.ПлатежноеПоручение КАК ПлатежноеПоручение";
Результат = Запрос.Выполнить();
ЗафиксироватьТранзакцию();
Получим ошибку:
Ошибка при вызове метода контекста (Выполнить) Результат = Запрос.Выполнить(); По причине: Ошибка выполнения запроса. По причине: В данной транзакции уже происходили ошибки!
Исключительная ситуация, хотя она вызвана аналогичной строкой кода, потянула за собой дополнительные последствия (откат транзакции записи элемента справочника) и оказалась расценена как невосстановимая."
...
Диагностики взяты с сайтов:
- https://1c-syntax.github.io/bsl-language-server/diagnostics/
- https://docs.checkbsl.org/checks/ОписанияПравилПроверкиКода/
Для контроля ошибок рекомендую использовать:
- для vscode плагин Language 1C (BSL)
- для конфигуратора Phoenix BSL для 1С
- для EDT плагин SilverLint(платный) или bslls-connector-for-edt(бесплатный)
- для сонара SonarQube 1C (BSL) Community Plugin
Параграфы написаны в формате побудительных предложений, чтобы при код-ревью типичных ошибок работы с транзакциями делать однозначные подсказки.
Статья написана для облегчения онбординга новых сотрудников без опыта решения проблем с транзакциями, является частью соглашений по стайл-гайду (зачем нужен code style).