bsl-language-server icon indicating copy to clipboard operation
bsl-language-server copied to clipboard

[FP] Нарушение правил работы с транзакциями для метода 'ЗафиксироватьТранзакцию' при досрочном выходе из процедуры.

Open Ndochp opened this issue 2 years ago • 9 comments

Диагностика

Нарушение правил работы с транзакциями для метода 'ЗафиксироватьТранзакцию' Метод 'ЗафиксироватьТранзакцию' должен идти последним в блоке 'Попытка' перед оператором 'Исключение'

Версия

Сонар плагин 1.10.0

Описание ложного срабатывания диагностики

Для кода вида:

НачатьТранзакцию();
Попытка
    // нечто
    Если Истина Тогда
        ЗафиксироватьТранзакцию();
        Возврат;
    КонецЕсли;
    // Еще нечто
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    ВызватьИсключение;
КонецПопытки

диагностика срабатывает в блоке Если, хотя раз за фиксацией транзакции следует возврат, то это явно последний метод в блоке попытка и процедуре вообще.

Пример кода

см. выше

Скриншоты

изображение

Дополнительная информация

Обсуждение в телеграм возможных подводных камней:

Дмитрий, [23.03.2022 11:17] ЗафиксироватьТранзакцию(); Возврат Результат; Код выше ловится правилом Метод 'ЗафиксироватьТранзакцию' должен идти последним в блоке 'Попытка' перед оператором 'Исключение' Я конечно понимаю, что формально правило нарушено, но когда следующей строчкой идет возврат - то логически это все-таки последний оператор в блоке. Это достойно ИШШ или холивар, и вопрос лучше не поднимать? (естественно все это происходит внутри попытки, и таких досрочных выходов там пяток)

Artur, [23.03.2022 11:32] [In reply to Дмитрий] считаю, что такое срабатывание ложно. предлагаю сделать ишуз

Abramov Dmitry, [23.03.2022 11:32] [In reply to Artur] А если будет что-то типо Возврат Результат.НесуществующееПоле?

Artur, [23.03.2022 11:34] [In reply to Abramov Dmitry] обычное исключение с двойным падением ) к обсуждение правилу падение не относится.

Кирилл Черненко, [23.03.2022 11:34] [In reply to Abramov Dmitry] Скорее уместно спросить что делать с Возврат ПроцедураНаЕщеТыщуСтрокКода()

Artur, [23.03.2022 11:35] [In reply to Кирилл Черненко] ну да, для вызовов методов уже сложнее.

проще, конечно, следовать правилу из стандарта )

Дмитрий, [23.03.2022 11:35] [In reply to Abramov Dmitry] можно ограничить "Возврат;"

Кирилл Черненко, [23.03.2022 11:36] [In reply to Artur] Ну так правило по стандарту, и выше не фп а нарушение стандарта, то что людям нравится так писать не значит что правило должно подстраиваться

Дмитрий, [23.03.2022 11:37] [In reply to Кирилл Черненко] Для процедур вы согласны, что ЗафиксироватьТранзакцию(); Возврат; это четкое следование духу стандарта?

Кирилл Черненко, [23.03.2022 11:38] [In reply to Дмитрий] Нет, ведь и возврат в этой конструкции может упасть в исключение в случае если был параметр без знач в который сунули не сериализуемое значение и это было вызов сервера с клиента

Nikita Fedkin, [23.03.2022 11:43] и вот никто из вас не подумал, что упасть может фиксация транзакции

Дмитрий, [23.03.2022 11:44] И что? Любой код ниже в этой ветке исполнения недостижим, значит Попытка ... Зафиксировать(); Возврат; ... Исключение Отмена(); ВызватьИсключение; конецПопытки; КонецПроцедуры; и Попытка ... ЕслиИстина Зафиксировать(); Иначе ... КонецЕсли; Исключение Отмена(); ВызватьИсключение; конецПопытки; КонецПроцедуры; эквивалентны Все равно, даже если падать в момент возврата транзакция в обоих случаях будет зафиксирована, а потом исключение. Только если возврат внутри попытки он сначала свалится в блок исключения. Хотя при этом нарушится парность начала/отмены транзакции... Можно ненароком откатить внешнюю.

Artur, [23.03.2022 11:44] [In reply to Nikita Fedkin] это в стандарте, это неинтересно

Nikita Fedkin, [23.03.2022 11:44] поэтому фиксация и в попытке

Дмитрий, [23.03.2022 11:44] [In reply to Nikita Fedkin] Так фиксация в любом случае в попытке.

Кирилл Черненко, [23.03.2022 11:44] [In reply to Nikita Fedkin] Если фиксация упадет не будет сайд эффекта в бд

Ndochp avatar Mar 23 '22 10:03 Ndochp

Указанный код на раз-два рефакторится Представь это ОбработкеПроведения() и тебе нужно проверить наличие остатков

НачатьТранзакцию();
Попытка
    // нечто - ответственное чтение
    Если Условие Тогда
        ВызватьИсключение;
    КонецЕсли;
    // ЕшеНечто
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
КонецПопытки

или, если так надо пробрасывать исключение

НачатьТранзакцию();
Попытка
    // нечто
    Если ДействиеДоступно() Тогда
        // Еще нечто
    Иначе
        Отказ = Истина;
    КонецЕсли;
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    ВызватьИсключение;
КонецПопытки
Если Отказ Тогда
    Возврат;
КонецЕсли;

или так

НачатьТранзакцию();
Попытка
    НечтоИЕщёЧтото(Отказ);
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    ВызватьИсключение;
КонецПопытки
Если Отказ Тогда
    Возврат;
КонецЕсли;

Зависит от контекста. Какой у вас код?

kuzyara avatar Sep 29 '22 15:09 kuzyara

  1. У меня типовой код и я не хочу рефакторить за 1С.
  2. У меня может быть простыня на 5 экранов. И для некоторого условия я могу сразу сказать ок и свалить из функции. Почему-то во всех примерах ты крутишься вокруг перехода на отказ. Да, если все плохо я могу сразу выкинуть исключение и упасть в отмену транзакции. Но мне как раз наоборот стало хорошо раньше простыни обработки крайних случаев. В этот момент я фиксирую транзакцию и ухожу, вместо того, чтобы загнать весь код ниже в блок "иначе" и в 2 раза увеличить когнитивку обработки особых ситуаций.

Типичный пример ниже. И строчки ЗафиксироватьТранзакцию() и отменитьТранзакцию() с возвратом после них не должны учитываться при расчете парности совсем.

Функция ПодготовитьКОтправкеДокумент(Сообщение, КонтекстДиагностики, СообщенияДляОбработки, ОбработанныеСообщения, ИдентификаторПакета = Неопределено)
	
	Результат = Ложь;
	
	Действие = Перечисления.ДействияПоЭДО.ПодготовитьКОтправке;
	
	НачатьТранзакцию();
	Попытка
		
		СообщениеОбъект = ОбщегоНазначенияБЭД.ОбъектПоСсылкеДляИзменения(Сообщение);
		Если СообщениеОбъект.Состояние <> Перечисления.СостоянияСообщенийЭДО.ПодготовкаКОтправке Тогда
			ЗафиксироватьТранзакцию();
			Возврат Результат;
		КонецЕсли;
		
		ЭтоВходящийЭДО = ЭтоВходящийЭДО(СообщениеОбъект.ЭлектронныйДокумент);
		
		ТребуетсяПоискПакета = Не ЭтоВходящийЭДО
			И СообщениеОбъект.ТипЭлементаРегламента = Перечисления.ТипыЭлементовРегламентаЭДО.ИнформацияОтправителя
			И ИдентификаторПакета = Неопределено;
		
		Блокировка = Новый БлокировкаДанных;
	
		ЭлементБлокировки = Блокировка.Добавить("Документ.СообщениеЭДО");
		ЭлементБлокировки.УстановитьЗначение("ЭлектронныйДокумент", СообщениеОбъект.ЭлектронныйДокумент);
		
		ЭлементБлокировки = Блокировка.Добавить(ИмяТаблицыДокументаЭДО(ЭтоВходящийЭДО));
		ЭлементБлокировки.УстановитьЗначение("Ссылка", СообщениеОбъект.ЭлектронныйДокумент);
		
		Запрос = Новый Запрос;
		ТекстыЗапроса = Новый Массив;
		ТекстыЗапроса.Добавить(ТекстЗапросаДанныхДляПодготовкиКОтправке(ЭтоВходящийЭДО));
		Запрос.УстановитьПараметр("Сообщение", Сообщение);
		ТекстыЗапроса.Добавить(ТекстЗапросаПараметровСостоянияДокумента());
		Запрос.УстановитьПараметр("ЭлектронныйДокумент", СообщениеОбъект.ЭлектронныйДокумент);
		
		Если ТребуетсяПоискПакета Тогда
			ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.СоставПакетовДокументовЭДО");
			ЭлементБлокировки.УстановитьЗначение("ЭлектронныйДокумент", СообщениеОбъект.ЭлектронныйДокумент);
			ТекстыЗапроса.Добавить(ТекстЗапросаИдентификатораПакетаДокумента());
		КонецЕсли;
		
		Запрос.Текст = СтрСоединить(ТекстыЗапроса, ОбщегоНазначения.РазделительПакетаЗапросов());
		
		Блокировка.Заблокировать();
		РезультатыЗапроса = Запрос.ВыполнитьПакет();
		
		Если РезультатыЗапроса[1].Пустой()
			ИЛИ РезультатыЗапроса[2].Пустой()
			ИЛИ РезультатыЗапроса[3].Пустой()
			ИЛИ РезультатыЗапроса[4].Пустой() Тогда
			ОтменитьТранзакцию();
			Возврат Результат;
		КонецЕсли;
		
		Если ТребуетсяПоискПакета И Не РезультатыЗапроса[7].Пустой() Тогда
			ВыборкаПакетов = РезультатыЗапроса[7].Выбрать();
			ВыборкаПакетов.Следующий();
			ИдентификаторПакета = ВыборкаПакетов.ИдентификаторПакета;
			Подготовлены = ПодготовитьСообщенияПакетаКОтправке(ИдентификаторПакета, Сообщение,
				СообщенияДляОбработки, ОбработанныеСообщения, КонтекстДиагностики);
			Если Не Подготовлены Тогда
				ОтменитьТранзакцию();
				Возврат Результат;
			КонецЕсли;
		КонецЕсли;
		
		СвойстваДокумента = РезультатыЗапроса[1].Выбрать();
		СвойстваДокумента.Следующий();
		ТипыСообщений = РезультатыЗапроса[2].Выбрать();
		ТипыСообщений.Следующий();
		ФайлыСообщения = РезультатыЗапроса[3].Выгрузить();
		СостоянияСообщений = РезультатыЗапроса[4].Выгрузить();
		ИдентификаторыОснований = РезультатыЗапроса[5].Выгрузить();
		
		ДанныеОбъектов = СинхронизацияЭДО.НовыеДанныеОбъектов();
		ДанныеОбъекта = СинхронизацияЭДО.ДобавитьДанныеОбъекта(ДанныеОбъектов);
		
		ЗаполнитьДанныеОбъектаДляПодготовкиКОтправке(ДанныеОбъекта, СвойстваДокумента, СообщениеОбъект,
			ТипыСообщений.ТипСообщения, ФайлыСообщения, ИдентификаторыОснований, КонтекстДиагностики);
		
		Если ДанныеОбъекта.Отказ Тогда
			ОтменитьТранзакцию();
			Возврат Результат;
		КонецЕсли;
		
		РезультатПодготовки = СинхронизацияЭДО.ПодготовитьОбъектыКОтправке(ДанныеОбъектов, КонтекстДиагностики);
		
		ПричинаОстановки = Перечисления.ПричиныОстановкиЭДО.ПустаяСсылка();
		
		Если РезультатПодготовки.Отказ Тогда
			Если ДанныеОбъекта.ТипОшибкиОтправки = СинхронизацияЭДО.ТипыОшибокОтправки().ОжидаетсяОтветНаПриглашение Тогда
				ПричинаОстановки = Перечисления.ПричиныОстановкиЭДО.ОжидаетсяОтветНаПриглашение;
			Иначе
				ОтменитьТранзакцию();
				Возврат Результат;
			КонецЕсли;
		КонецЕсли;
		
		Комментарий = КомментарийКСостояниюДокумента(РезультатыЗапроса[6]);
		
		Если ЗначениеЗаполнено(ПричинаОстановки) Тогда
			ОстановитьДокументПриПодготовкиКОтправке(СообщениеОбъект, ПричинаОстановки, СостоянияСообщений,
				КонтекстДиагностики, Комментарий);
		Иначе
			ОтметитьПодготовкуСообщенияКОтправке(СообщениеОбъект, СвойстваДокумента, СостоянияСообщений,
				КонтекстДиагностики, Комментарий);
		КонецЕсли;
		
		ОбработанныеСообщения.Добавить(СообщениеОбъект);
		
		Результат = Истина;
		ЗафиксироватьТранзакцию();
	Исключение
		ОтменитьТранзакцию();
		ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
		ДобавитьОшибкуВыполненияДействияПоСообщению(Сообщение, Действие, ТекстОшибки, КонтекстДиагностики);
	КонецПопытки;
	
	Возврат Результат;
	
КонецФункции

Ndochp avatar Sep 30 '22 09:09 Ndochp

  1. У меня типовой код и я не хочу рефакторить за 1С.

В этом и суть ишуза, считаю.

// не хочу рефакторить за 1С
// BSLLS:PairingBrokenTransaction-off
Функция ПодготовитьКОтправкеДокумент(Сообщение, КонтекстДиагностики, СообщенияДляОбработки, ОбработанныеСообщения, ИдентификаторПакета = Неопределено)
КонецФункции
// BSLLS:PairingBrokenTransaction-on

kuzyara avatar Oct 19 '22 13:10 kuzyara

Нет суть ишшуза в том, что

начатьТранзакцию();
Попытка
//нечто
Если нечто Тогда
   ЗафиксироватьТранзацкцию();
   Возврат;
КонецЕсли
//Еще нечто
Если ЕщеНечто Тогда
  ОтменитьТранзакцию();
  Возврат;
КонецЕсли;
// продолжаем кодить
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;

это такой же допустимый код, как и просто

Процедура ЭтоНечто()
//нечто
Если нечто Тогда
   Возврат;
КонецЕсли
//Еще нечто
Если ЕщеНечто Тогда
  Возврат;
КонецЕсли;
// продолжаем кодить
КонецПроцедуры

Так как альтернатива не проходит когнитивку

Процедура ЭтоНечто()
   //нечто
   Если не нечто Тогда
      //Еще нечто
      Если не ЕщеНечто Тогда
          // продолжаем кодить
      КонецЕсли;
   КонецЕсли;
КонецПроцедуры

Ndochp avatar Oct 21 '22 19:10 Ndochp

Не воспринимайте это правило как однозначный закон ПДД или категоричное правило. Оно всего лишь подсвечивает code smells, и с этим успешно справляется.

Вы же хотите признания ваших допущений в коде - хотя вы сами и без правила видите что приведенный кусок кода сложно читать.

С другой стороны алерты на типовой код, который нет объективных (а не стилистических) причин рефакторить - это мозоль которую не надо трогать bslls дабы у пользователей не подгорало.

Но особенносить линтера такова, что при правке пары строк - анализируется вся процедура. Поэтому предлагаю сместить акцент.

Не подсвечивать участок кода как жестокая ошибка - если он был написан не вами и такие ошибки уже присутствовали. Перенеся ответственность с программиста на легаси код.

kuzyara avatar Oct 24 '22 04:10 kuzyara

Ну я вижу тут явную ошибку по сравнению с тем, что пишет проверка. Для каждой Зафиксировать/ОтменитьТранзакцию я могу ткнуть пальцем в парную НачатьТранзакцию, а правило этого не видит. И кстати, если я приведенный кусок кода перепишу так, что правило ругаться не будет - читать будет еще хуже. А ОтменитьТранзакцию неожиданно на текущем наборе правил использовать вне блока Исключение оказывается нельзя, а это очевидно не так.

Ndochp avatar Oct 24 '22 07:10 Ndochp

А ОтменитьТранзакцию неожиданно на текущем наборе правил использовать вне блока Исключение оказывается нельзя, а это очевидно не так.

Потому что может рухнуть буквально что угодно и до отмены транзакции не дойдёт.

nixel2007 avatar Oct 25 '22 08:10 nixel2007

Чиво? я же не предлагаю выкинуть отмену транзации из блока исключения. Я просто самостоятельно, без выбрасывания ошибки хочу отменить транзакцию и уйти из процедуры. Почему нельзя?

Ndochp avatar Oct 25 '22 10:10 Ndochp

@nixel2007 мне эти ограничения напоминают ограничения ФП языка типа хаскеля)

	Если КоличествоСтрокТЧ = 1 Тогда
		НачатьТранзакцию();
		Попытка
			БлокировкаДанных = Новый БлокировкаДанных;
			ЭлементБлокировкиДанных = БлокировкаДанных.Добавить("РегистрСведений.ПризнакиАВСБезЗаявки");
			ЭлементБлокировкиДанных.УстановитьЗначение("ИдентификаторЗаявки", idЗаявки);
			ЭлементБлокировкиДанных.Режим = РежимБлокировкиДанных.Исключительный;
			БлокировкаДанных.Заблокировать();

			Запрос = Новый Запрос;
			Запрос.Текст =
			"ВЫБРАТЬ
			|	ПризнакиАВСБезЗаявки.ИдентификаторЗаявки,
			|	ПризнакиАВСБезЗаявки.ПризнакАВС
			|ИЗ
			|	РегистрСведений.ПризнакиАВСБезЗаявки КАК ПризнакиАВСБезЗаявки
			|ГДЕ
			|	ПризнакиАВСБезЗаявки.ИдентификаторЗаявки = &idЗаявки";

			Запрос.УстановитьПараметр("idЗаявки", idЗаявки);

			РезультатЗапроса = Запрос.Выполнить();

			ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();

			Если ВыборкаДетальныеЗаписи.Следующий() Тогда
				МенеджерЗаписи = РегистрыСведений.ДополнительныеДанныеЗаявкиНаВозврат.СоздатьМенеджерЗаписи();
				МенеджерЗаписи.Заявка		= ЗаявкаНаВозврат.Ссылка;
				МенеджерЗаписи.Номенклатура = Номенклатура;
				МенеджерЗаписи.Бренд 		= Номенклатура.Бренд;
				МенеджерЗаписи.ПризнакАВС 	= ВыборкаДетальныеЗаписи.ПризнакАВС;
				МенеджерЗаписи.Записать();
				
				МенеджерЗаписи = РегистрыСведений.ПризнакиАВСБезЗаявки.СоздатьМенеджерЗаписи();
				МенеджерЗаписи.ИдентификаторЗаявки = idЗаявки;
				МенеджерЗаписи.Удалить();
				
				ЗафиксироватьТранзакцию();
			Иначе
				ОтменитьТранзакцию();
			КонецЕсли;
		Исключение
			ОтменитьТранзакцию();
			
			ЗаписьЖурналаРегистрации("ru = 'ДобавлениеПризнакаАВСПриСозданииЗаявки'", 
										УровеньЖурналаРегистрации.Ошибка, 
										, 
										ЗаявкаНаВозврат.Ссылка,
										ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
		КонецПопытки;
	КонецЕсли;
	
КонецПроцедуры

вот ещё пример, кто-то написал такой код

как аккуратно привести его в нормальному виду?

kuzyara avatar Nov 01 '22 01:11 kuzyara

Встретил в УТ11 вот такой интересный шаблон конкурентного доступа. image

kuzyara avatar May 31 '23 05:05 kuzyara