Хабрахабр

Вы не умеете работать с транзакциями

Сразу скажу, что речь пойдет об 1С. Заголовок вышел броским, но накипело. К такому выводу я пришел, просматривая большое количество кода на 1С, рождаемого в дебрях отечественного энтерпрайза. Дорогие 1С-ники, вы не умеете работать с транзакциями и не понимаете что такое исключения. Вы когда-нибудь видели у себя ошибку "В данной транзакции уже происходили ошибки"? В типовых конфигурациях с этим все достаточно хорошо, но ужасающее количество заказного кода написано некомпетентно с точки зрения работы с базой данных. Давайте под катом разберемся, наконец, что такое транзакции и как правильно с ними обращаться, работая с 1С. Если да — то заголовок статьи относится и к вам.

Почему надо бить тревогу

Это, на самом деле, предельно простая штука: вы пытаетесь работать с базой данных внутри уже откаченной (отмененной) транзакции. Для начала, давайте разберемся, что же такое представляет собой ошибка "В данной транзакции уже происходили ошибки". Например, где-то был вызван метод ОтменитьТранзакцию, а вы пытаетесь ее зафиксировать.

Потому что данная ошибка ничего не говорит вам о том, где на самом деле случилась проблема. Почему это плохо? Это задница. Когда в саппорт от пользователя приходит скриншот с таким текстом, а в особенности для серверного кода, с которым интерактивно не работает человек — это… Хотел написать "критичная ошибка", но подумал, что это buzzword, на который уже никто не обращает внимания…. Это не случайный сбой. Это ошибка программирования. Потому что, когда у вас фоновые процессы сервера встанут ночью и компания начнет стремительно терять деньги, то "В данной транзакции уже происходили ошибки" это последнее, что вы захотите увидеть в диагностических логах. Это косяк, который надо немедленно переделывать.

А реальная причина одна — программист Вася получил исключение внутри транзакции и решил, что один раз — не карабас "подумаешь, ошибка, пойдем дальше". Есть, конечно, вероятность, что технологический журнал сервера (он ведь у вас включен в продакшене, да?) как-то поможет диагностировать проблему, но я сейчас навскидку не могу придумать вариант — как именно в нем найти реальную причину указанной ошибки.

Что такое транзакции в 1С

Транзакции в 1С — это то же самое, что транзакции в СУБД. Неловко писать про азбучные истины, но, видимо, немножго придется. Согласно общей идее транзакций, они могут либо выполниться целиком, либо не выполниться совсем. Это не какие-то особенные "1С-ные" транзакции, это и есть транзакции в СУБД. Все изменения в таблицах базы данных, выполненные внутри транзакции, могут быть разом отменены, как будто ничего не было.

Собственно говоря, они не поддерживаются не "в 1С", а вообще не поддерживаются. Далее, нужно понимать, что в 1С не поддерживаются вложенные транзакции. Вложенных транзакций, например, нет в MS SQL и Postgres. По-крайней мере, теми СУБД, с которыми умеет работать 1С. Данное поведение описано в множестве книжек и статей, но выводы из этого поведения, видимо, разобраны недостаточно. Каждый "вложенный" вызов НачатьТранзакцию просто увеличивает счетчик транзакций, а каждый вызов "ЗафиксироватьТранзакцию" — уменьшает этот счетчик. SAVEPOINT, но 1С их не использует, да и вещь это достаточно специфичная. Строго говоря, в SQL есть т.н.

Здесь и далее, специально для Воинов Истинной Веры, считающих, что код должен писаться только на английском, под спойлерами будет приведен аналог кода в англоязычном синтаксисе 1С.

Процедура ОченьПолезныйИВажныйКод(СписокСсылокСправочника) НачатьТранзакцию(); Для Каждого Ссылка Из СписокСсылокСправочника Цикл ОбъектСправочника = Ссылка.ПолучитьОбъект(); ОбъектСправочника.КакоеТоПоле = "Я изменен из программного кода"; ОбъектСправочника.Записать(); КонецЦикла; ЗафиксироватьТранзакцию(); КонецПроцедуры

Код на английском

Мне совершенно не хочется дублировать примеры на английском только ради того, чтобы потешить любителей холиваров и священных войн. На самом деле, нет.

Приведенный пример кода содержит ошибки. Вы же наверняка пишете такой код, да? Знаете какие? Как минимум, три. Про вторую — чуть позже. Про первую я скажу сразу, она связана с объектными блокировками и не имеет отношения непосредственно к транзакциям. Ключевое слово для гугления: deadlock управляемые блокировки. Третья ошибка — это deadlock, который возникнет при параллельном исполнении этого кода, но это тема для отдельной статьи, ее рассматривать сейчас не будем, дабы не усложнять код.

Такого в ваших 1С-системах просто вагон. Обратите внимание, простой ведь код. Задумайтесь на досуге, сколько ошибок есть в более сложных сценариях работы с транзакциями, написанных вашими программистами 1С 🙂 И он содержит сразу, как минимум, 3 ошибки.

Объектные блокировки

В 1С существуют объектные блокировки, так называемые "оптимистические" и "пессимистические". Итак, первая ошибка. Совершенно невозможно запомнить, какая из них за что отвечает. Кто придумал термин, не знаю, убил бы :). Подробно про них написано здесь и здесь, а также в прочей IT-литературе общего назначения.

Здесь один из вас может получить ошибку "запись была изменена или удалена". Суть проблемы в том, что в указанном примере кода изменяется объект базы данных, но в другом сеансе может сидеть интерактивный пользователь (или соседний фоновый поток), который тоже будет менять этот объект. Если это произойдет в фоновом потоке, то вам придется искать это в логах. Если это произойдет в интерактивном сеансе, то пользователь почешет репу, ругнется и попробует переоткрыть форму. А журнал регистрации, как вы знаете, медленный, а ELK-стек для журналов 1С у нас в отрасли настраивают единицы… (мы, к слову, входим в число тех, кто настраивает и другим помогает настраивать :))

Поэтому, в стандартах разработки четко написано, что перед изменением объектов необходимо ставить на них объектную блокировку методом "ОбъектСправочника.Заблокировать()". Короче говоря, это досадная ошибка и лучше, чтобы ее не было. Тогда параллельный сеанс (который тоже должен так поступить) не сможет начать операцию изменения и получит ожидаемый, управляемый отказ.

А теперь про транзакции

С первой ошибкой разобрались, давайте перейдем ко второй.

Исключение из метода "Записать" может быть выброшено по самым разным причинам, например, сработают какие-то прикладные проверки в бизнес-логике, или возникнет упомянутая выше объектная блокировка. Если не предусмотреть проверку исключения в этом методе, то исключение (например, весьма вероятное на методе "Записать()") выбросит вас из данного метода без завершения транзакции. Так или иначе, вторая ошибка гласит: код, начавший транзакцию, не несет ответственность за ее завершение.

В нашем статическом анализаторе кода 1С на базе SonarQube мы даже отдельно встроили такую диагностику. Именно так я бы назвал эту проблему. Сейчас я работаю над ее развитием, и фантазия программистов 1С, чей код попадает ко мне на анализ, порой приводит меня в шок и трепет…

Потому что выброшенное наверх исключение внутри транзакции в 90% случаев не даст эту транзакцию зафиксировать и приведет к ошибке. Почему? Следует понимать, что 1С автоматически откатывает незавершенную транзакцию только после возвращения из скриптового кода на уровень кода платформы. До тех пор, пока вы находитесь на уровне кода 1С, транзакция остается активной.

Поднимемся на уровень выше по стеку вызовов:

Процедура ВажныйКод() СписокСсылок = ПолучитьГдеТоСписокСсылок(); ОченьПолезныйИВажныйКод(СписокСсылок); КонецПроцедуры

Наш проблемный метод вызывается откуда-то извне, выше по стеку. Смотрите, что получается. А если будут — то будут ли они все завершены… Мы же все тут за мир и инкапсуляцию, верно? На уровне этого метода разработчик понятия не имеет — будут ли какие-то транзакции внутри метода ОченьПолезныйИВажныйКод или их не будет. Того самого, в котором некорректно обрабатывается транзакция. Автор метода "ВажныйКод" не должен думать про то, что именно происходит внутри вызываемого им метода. В итоге, попытка поработать с базой данных после выброса исключения изнутри транзакции, с высокой вероятностью приведет к тому, что "В данной транзакции бла-бла…"

Размазывание транзакций по методам

Нельзя начинать транзакцию в одном методе и завершать ее в другом. Второе правило "транзакционно-безопасного" кода: счетчик ссылок транзакций в начале метода и в его конце должен иметь одно и то же значение. В общем случае так писать нельзя. Из этого правила, наверное, можно найти исключения, но это будет какой-то низкоуровневый код, который пишут более компетентные люди.

Например:

Процедура ВажныйКод() СписокСсылок = ПолучитьГдеТоСписокСсылок(); ОченьПолезныйИВажныйКод(СписокСсылок); ЗафиксироватьТранзакцию(); // Путевка в ад, серьезный разговор с автором о наших сложных трудовых отношениях. КонецПроцедуры

Нельзя писать методы так, чтобы вызывающая сторона помнила и следила за возможными (или вероятными — как знать) транзакциями внутри других методов, которые она вызывает. Выше — неприемлемый говнокод. Это нарушение инкапсуляции и разрастание спагетти-кода, который невозможно трассировать, сохраняя рассудок.

Выискивать начинающиеся и завершающиеся транзакции по шести уровням вложенности — это прям мотивирует на задушевные беседы с авторами. Особенно весело вспомнить, что реальный код намного больше синтетических примеров из 3-х строчек.

Пытаемся исправить код

Сразу скажу, что объектную блокировку мы чинить пока не будем, просто, чтобы не усложнять код примера. Вернемся к исходному методу и попытаемся его починить.

Первый подход типичного 1С-ника

А еще они боятся исключений, поэтому стараются их все перехватывать. Обычно программисты 1С знают, что при записи может быть выдано исключение. Например, вот так:

Процедура ОченьПолезныйИВажныйКод(СписокСсылокСправочника) НачатьТранзакцию(); Для Каждого Ссылка Из СписокСсылокСправочника Цикл ОбъектСправочника = Ссылка.ПолучитьОбъект(); ОбъектСправочника.КакоеТоПоле = "Я изменен из программного кода"; Попытка ОбъектСправочника.Записать(); Исключение Лог.Ошибка("Не удалось записать элемент %1", Ссылка); Продолжить; КонецПопытки; КонецЦикла; ЗафиксироватьТранзакцию(); КонецПроцедуры

Ведь теперь, возможные ошибки записи обрабатываются и даже логируются. Ну как, стало лучше, да? И в логе даже видно — на каком объекте, не поленился, вывел в сообщение ссылку вместо лаконичного "Ошибка записи справочника", как это часто любят писать вечно торопящиеся разработчики. Исключения больше не возникнут при записи объекта. Иными словами, налицо забота о пользователе и рост компетенций.

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

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

Каждая ветка кода должна быть исследована на предмет выхода из метода без фиксации или отмены транзакции. Код, который начинает транзакцию, обязан завершить или откатить ее. Не взирая ни на какие исключения.

Методы работы с транзакциями в 1С

Это всем известные методы: Не будет лишним напомнить, что вообще 1С предоставляет нам для работы с транзакциями.

  • НачатьТранзакцию()
  • ЗафиксироватьТранзакцию()
  • ОтменитьТранзакцию()
  • ТранзакцияАктивна()

Последний метод — возвращает Истину, если счетчик транзакций больше нуля. Первые 3 метода очевидны и делают то, что написано в их названии.

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

Очень просто: надо прочитать сформулированное выше правило: код, начавший транзакцию, должен нести ответственность за ее завершение. Как правильно пользоваться этими методами?

Давайте попробуем: Как же соблюсти это правило?

НачатьТранзакцию();
ДелаемЧтоТо();
ЗафиксироватьТранзакцию();

Он может выдать какое-то исключение, и транзакция "вылезет" наружу из нашего метода. Выше мы уже поняли, что метод ДелаемЧтоТо — потенциально опасен. Окей, добавим обработчик возможного исключения:

НачатьТранзакцию();
Попытка ДелаемЧтоТо();
Исключение // а что же написать тут?
КонецПопытки;
ЗафиксироватьТранзакцию();

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

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

А вот зачем: правило заставляет нас обеспечить завершение начатой нами транзакции. Так, стоп… Если мы просто прокидываем исключение дальше, то зачем тут вообще нужна Попытка?

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

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

Финальный вариант

Вот он: Наконец, мы можем написать правильный, "транзакционно-безопасный" вариант кода.

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

Почему же тогда "ЗафиксироватьТранзакцию" не обернут в такое же условие с "ТранзакцияАктивна"? Постойте, но ведь не только "ОтменитьТранзакцию" может выдавать ошибки. На нашем уровне абстракции мы обязаны заботиться только о нашей транзакции. Опять же, по тому же самому правилу: код, начавший транзакцию, должен нести ответственность за ее завершение. Наша транзакция необязательно самая первая, она может быть вложенной. Они чужие, мы не должны нести за них ответственность. Все прочие должны быть нам неинтересны. Нельзя предпринимать попыток выяснения реального уровня счетчика транзакций. Именно НЕ ДОЛЖНЫ. Мы проверили активность только в обработчике исключения и только для того, чтобы убедиться, что наш обработчик не породит нового исключения, "прячущего" старое. Это опять нарушит инкапсуляцию и приведет к "размазыванию" логики управления транзакциями.

Чек-лист рефакторинга

Давайте рассмотрим несколько наиболее распространенных ситуаций, требующих вмешательства в код.

Паттерн:

НачатьТранзакцию();
ДелаемЧтоТо();
ЗафиксироватьТранзакцию();

Обернуть в "безопасную" конструкцию с Попыткой, Проверкой активности и пробросом исключения.

Паттерн:

Если Не ТранзакцияАктивна() Тогда НачатьТранзакцию()
КонецЕсли

Автор не понимал, что делает. Анализ и Рефакторинг. Не нужно проверять условие, нужно просто начать вложенную транзакцию. Начинать вложенные транзакции можно безопасно. Это гарантированный геморрой. Ниже по модулю он наверняка еще там извращается с их фиксацией.

Примерно похожий вариант:

Если ТранзакцияАктивна() Тогда ЗафиксироватьТранзакцию()
КонецЕсли

Почему тут условие? аналогично: фиксация транзакции по условию — это странно. Повод для разбирательства. Что, кто-то иной мог уже зафиксировать эту транзакцию?

Паттерн:

НачатьТранзакцию()
Пока Выборка.Следующий() Цикл // чтение объекта по ссылке // запись объекта КонецЦикла;
ЗафиксироватьТранзакцию();

  1. ввести управляемую блокировку во избежание deadlock
  2. ввести вызов метода Заблокировать
  3. обернуть в "попытку", как показано выше

Паттерн:

НачатьТранзакцию()
Пока Выборка.Следующий() Цикл Попытка Объект.Записать(); Исключение Сообщить("Не получилось записать"); КонецПопытки; КонецЦикла;
ЗафиксироватьТранзакцию();

Нет смысла продолжать цикл. Данная транзакция уже не завершится в случае исключения. Дополнительно обеспечить более информативное сообщение об ошибке. Код требуется переписать, сверяясь с исходной задачей.

В заключение

К платформе, разумеется, есть претензии, особенно в среде Highload, но в общем и целом, она позволяет недорого и быстро разрабатывать очень качественные корпоративные приложения. Я, как вы уже, наверное, догадались, отношусь к людям, любящим платформу 1С и разработку на ней. В комментариях на Хабре обычно пишут всякое высокомерное, так вот, ребята — основная проблема 1С, как экосистемы — это не платформа и не вендор. Давая из коробки и ORM, и GUI, и веб-интерфейс, и Reporting, и много чего еще. 1С сделала разработку корпоративных приложений слишком легкой. Это слишком низкий порог вхождения, который позволяет попадать в отрасль людям, не понимающим, что такое компьютер, база данных, клиент-сервер, сеть и всякое такое. После этого, мне несложно подумать о себе, что и на больших масштабах можно писать примерно так же. Я за 20 минут могу написать на ней учетную систему для закупок/продаж с гибкими отчетами и веб-клиентом. Напишу-ка я "НачатьТранзакцию()".... Как-то там 1С сама все внутри сделает, не знаю как, но наверное сделает.

Простота разработки в 1С позволяет моментально реализовывать бизнес-идеи и встраивать их в процессы компании. И знаете — самое главное, что это прекрасно. И если вдруг вам нужна помощь в аудите вашей "медленной 1С" — обращайтесь к специалистам по оптимизации. Потом всегда можно отрефакторить, главное понимать как. Она совсем не медленная.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть