Ошибка при вызове метода контекста зафиксироватьтранзакцию

Исключительная ситуация — Транзакция не активна

При вызове ЗафиксироватьТранзакцию() или ОтменитьТранзакцию() возникает ошибка транзакция не активна.

Ошибка означает, что транзакция не была начата командой «НачатьТранзакцию()», либо уже зафиксирована/отменена.

Не зависит от платформы.

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

Скриншот ошибки

Транзакция не активна

Подробно

Ошибка при вызове метода контекста (ОтменитьТранзакцию)
Соединение.ОтменитьТранзакцию();
по причине:
Произошла исключительная ситуация (1C:Enterprise 8.3.8.1652): Транзакция не активна

Поведение системы

Система продолжает работать, но выполнение кода обработчика прерывается. Такие ошибки возникают обычно при обмене данными.

Решение

Добавить проверку ТранзакцияАктивна(), либо убрать дублирующие отмены транзакции в алгоритме программы.

Люди глупо доверчивы… Вся реклама мира основана на трех принципах: «Хорошо, много и даром». Поэтому можно давать скверно, мало и дорого.

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

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

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

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

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

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

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

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

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

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

    НачатьТранзакцию();

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

    ЗафиксироватьТранзакцию();

КонецПроцедуры

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Процедура ВажныйКод()

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

КонецПроцедуры

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

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

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

Например:

Процедура ВажныйКод()

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

     ЗафиксироватьТранзакцию();

     // Путевка в ад, серьезный разговор с автором о наших сложных трудовых отношениях.

КонецПроцедуры

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

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

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

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

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

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

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

    НачатьТранзакцию();

    Для Каждого Ссылка Из СписокСсылокСправочника Цикл
        ОбъектСправочника = Ссылка.ПолучитьОбъект();

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

    ЗафиксироватьТранзакцию();

КонецПроцедуры

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

**UPD: в комментариях предложен более безопасный вариант, когда ЗафиксироватьТранзакцию расположен внутри блока Попытка. Здесь приведен именно этот вариант, ранее Фиксация располагалась после блока Попытка-Исключение.

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

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

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

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

Паттерн:

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

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

Паттерн:

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

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

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

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

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

Паттерн:

НачатьТранзакцию()
Пока Выборка.Следующий() Цикл

    // чтение объекта по ссылке
    // запись объекта

КонецЦикла;
ЗафиксироватьТранзакцию();
  1. ввести управляемую блокировку во избежание deadlock
  2. ввести вызов метода Заблокировать
  3. обернуть в «попытку», как показано выше

Паттерн:

НачатьТранзакцию()
Пока Выборка.Следующий() Цикл

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

КонецЦикла;
ЗафиксироватьТранзакцию();

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

В заключение

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

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

Разбираемся с опасностями использования транзакций во встроенном языке 1С. Познаем ошибку «В данной транзакции уже происходили ошибки». Учимся защищаться от них.

Статья актуальна для версии платформы 8.3.14

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

Транзакции применяются для целостного изменения связанных данных, т.е. все действия с базой данных, выполняемые в рамках транзакции или выполняются целиком, или целиком откатываются.

  1. Менеджер транзакции
    1. Это условное название единого для сеанса базы 1С внутреннего объекта платформы, управляющего транзакцией. Единый для сеанса значит, что он синхронизируется между толстым клиентским и серверным контекстном сеанса.
    2. Свойства менеджера транзакции
      1. Глубина/Вложенность/Счетчик — целое число — сколько раз была открыта транзакция минус сколько раз была закрыта
      2. Отменена – булево — признак отмены транзакции
  2. Вложенные транзакции
    1. Менеджер транзакции содержит свойство “Глубина”. Если оно больше 1, то транзакция считается вложенной.
    2. Логические (1С) и фактические (СУБД) транзакции
      1. Логическая (1С) транзакция — все операции между началом транзакции и следующим завершением транзакции с тем же значением глубины/вложенности/счетчика транзакций
      2. Фактическая (СУБД) транзакция — совпадает с логической транзакцией с Глубина = 1
    3. Вложенными в 1С могут быть только логические транзакции
    4. При начале логической транзакции увеличивается на 1 свойство “Глубина” менеджера транзакции
    5. При завершении логической транзакции уменьшается на 1 свойство “Глубина” менеджера транзакции
    6. Определить во встроенном языке значение свойства Глубина или хотя бы наличие одной вложенности, не изменяя состояние менеджера транзакции, невозможно.
    7. Вложенность транзакций (ИТС)
    8. Правила использования транзакций (ИТС)
  3. Явные и неявные логические транзакции
    1. Явные — начинаются/завершаются методами встроенного языка
      1. НачатьТранзакцию
      2. ЗафиксироватьТранзакцию/ОтменитьТранзакцию
    2. Неявные — начинаются/завершаются платформой в начале/конце выполнения записи объектов данных. При этом программная запись влияет на свойство Глубина, а интерактивная нет.
    3. Определить во встроенном языке, является ли транзакция явной/неявной, невозможно.
  4. Сломанные транзакции
    1. Менеджер транзакции содержит признак “Отменена”. Сбрасывается он только при начале фактической транзакции. Если он установлен, то транзакция считается сломанной и фактическая транзакция подлежит отмене при ее любом завершении. Устанавливается он при возникновении ошибки базы данных и при вызове ОтменитьТранзакцию().
    2. Определить во встроенном языке, является ли транзакция сломанной, напрямую невозможно, но можно косвенно с достаточной долей уверенности. Пример будет рассмотрен далее.
    3. Примеры ошибок базы данных
      1. Ошибка выполнения запроса
      2. Необработанное исключение при записи объекта
      3. Отказ при записи объекта (“Не удалось записать <объект>”)
      4. Ошибка установки транзакционной блокировки
      5. Ошибка установки объектной блокировки
    4. Ошибки базы данных и транзакции (ИТС)
    5. Невосстановимые и восстановимые исключения (ИТС)
    6. При обращении к БД в сломанной транзакции платформа выбрасывает ошибку “В данной транзакции уже происходили ошибки”. Здесь возникает разрыв между первичной ошибкой, ломающей транзакцию, и этой вторичной ошибкой. Из-за этого разрыва разработчику обычно бывает очень тяжело добраться до причины первичной ошибки. Поэтому к обработке ошибок в сломанной транзакции нужно подходить очень аккуратно. Пример будет рассмотрен далее.
  5. Работа со ссылками в фактической (СУБД) транзакции
    1. Кэш представлений ссылок
      1. Транзакция имеет собственный кэш представлений ссылок.
      2. Обращение к представлению ссылки может вызывать неявное обращение к БД для обновления кэша представления по этой ссылке.
      3. Примеры обращений к представлению ссылки
        1. “” + Ссылка
        2. Таблица.Сортировать(“Ссылка”) — платформа считывает представления ссылок для сортировки по ним
        3. ЗаписьЖурналаРегистрации(,,, Ссылка) — всегда получает представление от ссылки
      4. При обработке исключений в транзакции часто возникает потребность вывода диагностической информации в лог/пользователю. Тут кроется самая коварная особенность сломанной транзакции. При получении представления ссылки с обновлением кэша в сломанной транзакции платформа выбрасывает необрабатываемое исключение без указания исходной строки, в которой выполнено это обращение. Причем если код выполняется внутри неявной транзакции, то исключение является восстановимым, а иначе не восстановимым. Об этом очень неудобном поведении я сообщал в 1С в 2013г и в 2023г, но невосстановимость этой ошибки до сих пор осталась. Далее будет приведен безопасный подход к решению задачи.
    2. Объектный кэш
      1. Транзакция имеет собственный объектный кэш.
      2. Обращение к полю ссылки может вызывать неявное обращение к БД для обновления кэша объекта по этой ссылке.
      3. Примеры обращений к объектному кэшу
        1. Ссылка.ПометкаУдаления;
        2. Ссылка.ПолучитьОбъект();
      4. Аналогично кэшу представлений ссылок при обращении к объектному кэшу в сломанной транзакции платформа может выбрасывать необрабатываемые и невосстановимые исключения, но в меньшем числе ситуаций.
  6. Взаимоблокировка (Deadlock)
    1. Чтобы снизить вероятность появления взаимоблокировок, нужно стараться устанавливать управляемые блокировки, нужные для всех вложенных транзакций, в самом начале фактической (СУБД) транзакции. Тогда выполнение кода установки управляемых блокировок во вложенных транзакциях не будет изменять состав заблокированных ресурсов и тем самым порядок захвата ресурсов в транзакции будет более стабильным и предсказуемым.

Таблица операций, воздействующих на менеджер транзакции

Операция

Выброс исключения

Признак отмены транзакции

Глубина

Фактическая транзакция (СУБД)

Начало записи объекта (неявное начало транзакции)

Если признак отмены был Истина, то “В данной транзакции уже происходили ошибки”

Ложь, если глубина была 0

+1 при программной записи;

не меняется при интерактивной записи
 

Открывается, если глубина была 0

Конец записи объекта (неявный конец транзакции)

  • Если признак отмены был Истина, то “В данной транзакции уже происходили ошибки” (кнопка «Сломанная неявная транзакция»)
  • Если признак отмены был Ложь и стал Истина, то “Не удалось записать <объект>”.
  • Если глубина была 0, то “Ошибка SDBL: Открытых транзакций нет(кнопка «Отмена транзакции в неявной транзакции»)

Истина, если Отказ = Истина или выброшено исключение

-1 при программной записи;

не меняется при интерактивной записи

Если глубина стала 0

  • Фиксируется, если признак отмены стал Ложь.
  • Откатывается, если признак отмены стал Истина.

НачатьТранзакцию

 

Ложь, если глубина была 0

+1

Открывается, если глубина была 0

ОтменитьТранзакцию

Если глубина была 0, то “Транзакция неактивна”

Истина

-1

Откатывается, если глубина стала 0.

ЗафиксироватьТранзакцию

Если глубина была 0, то “Транзакция неактивна”

не меняется

-1

Если глубина стала 0

  • Фиксируется, если признак отмены стал Ложь.
  • Откатывается, если признак отмены стал Истина.

Операция (явная или неявная) с БД в транзакции

Если признак отмены был Истина, то “В данной транзакции уже происходили ошибки”

  • восстановимое — для операций без обновления кэша объектного/представлений (кнопка «Ошибка обращения к БД в сломанной явной транзакции»)
  • невосстановимое — для операций с обновлением кэша объектного/представлений (кнопка «Невосстановимая ошибка обращения к ссылке в сломанной явной транзакции»)

Истина, если выброшено исключение

не меняется

не меняется

Завершение потока встроенного языка     0

Если глубина была > 0, Откатывается

Исключение в транзакции

Ошибки в транзакции могут быть

  1. Ломающие — связаны с БД, выставляют признак «Отменена» менеджера транзакции
  2. Неломающие — не связаны с БД, не воздействуют на менеджер транзакции

Проверка сломанной транзакции

Хотя явно получить значение признака “Отменена” менеджера транзакции во встроенном языке нельзя. Однако можно воспользоваться косвенным методом — попытаться выполнить простейшую операцию БД (общий модуль ОбщийМодуль1)

// Будет ли выброшено исключение "В данной транзакции уже происходили ошибки" (является ли транзакция сломанной)
Функция ВТранзакцииПроисходилиОшибки() Экспорт
Запрос = Новый Запрос("ВЫБРАТЬ 1");
Попытка
Запрос.Выполнить();
Результат = Ложь;
Исключение
Результат = Истина;
КонецПопытки;
Возврат Результат;
КонецФункции

При необходимости устранить ложные срабатывания, можно будет еще анализировать описание ошибки. Кстати обращения к серверу СУБД этот запрос не производит (только к модели БД).

Подготовка данных для обработки исключения в сломанной транзакции

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

Передача ссылки в журнал регистрации в сломанной транзакции

При обработке исключения в сломанной транзакции часто разумно писать диагностическую информацию в журнал регистрации. При этом метод ЗаписьЖурналаРегистрации() неявно берет представление от ссылки, используя кэш представлений ссылок, и помещает его в поле «Представление данных» события журнала. Обращение к этому кэшу в сломанной транзакции несет риск невосстановимой ошибки. Поэтому рекомендую передавать ссылку в журнал регистрации только через эту функцию (общий модуль ОбщийМодуль1)

// Помещение представления ссылки в кэш представлений в сломанной транзакции вызывает досрочное завершение фактической транзакции с невосстановимой ошибкой "В данной транзакции уже происходили ошибки"
// Передача в метод ЗаписьЖурналаРегистрации() ссылки на объект в сломанной транзакции приведет к такому исключению, если представление будет обновлено в кэше.
// Поэтому в таком случае функция возвращает строку идентификатор ссылки. Подразумевается что такие места в коде будут устраняться и потому возвращаться идентификатор будет достаточно редко.
Функция СсылкаДляПередачиВЖурналРегистрации(Ссылка) Экспорт
Если ВТранзакцииПроисходилиОшибки() Тогда
Результат = "" + Ссылка.УникальныйИдентификатор();
Иначе
Результат = Ссылка;
КонецЕсли;
Возврат Результат;
КонецФункции

Тогда безопасная запись в журнал регистрации при обработке исключения в транзакции может выглядеть так

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

Открывать и закрывать транзакцию в одном методе

Старайтесь открывать и закрывать логическую транзакцию в одном методе. Это значительно облегчает отладку и анализ кода. Следствие этой рекомендации — выполнять код транзакции в попытке, чтобы в исключении можно было отменить транзакцию. Иначе исключение поднимется по стеку (в вызывающий метод) без закрытия логической транзакции (восстановления свойства Глубина менеджера транзакции) в текущем методе. Часто и особенно во вложенных транзакциях при обработке такого исключения и после возможной регистрации диагностической информации исключение перевыбрасывают. Даже если на текущий момент ваша транзакция не вызывается вложенно потом это может измениться. Поэтому разумно сразу об этом позаботиться.

Операции с БД в сломанной транзакции

Иногда бывает нужно в сломанной транзакции (например при обработке исключения) выполнить очень важную операцию с БД.

В таком случае может быть оправдано пренебречь рекомендацией «Открывать и закрывать транзакцию в одном методе» и завершить фактическую транзакцию в текущем месте, чтобы сделать возможным выполнение этой важной операции с БД.

 Исключение
Пока ТранзакцияАктивна() Цикл
ОтменитьТранзакцию();
КонецЦикла;
ЗаписатьИнформациюОбОшибкеВРегистр();
...
КонецПопытки;

Следует иметь ввиду, что такой прием повышает вероятность возникновения ошибок во внешних транзакциях. Самый негативный сценарий — текущая транзакция была вложена в неявную фактическую транзакцию записи объекта. Тогда при завершении записи объекта будет невосстановимое исключение (кнопка «Невосстановимая ошибка после отмены транзакции в неявной транзакции»).

Также для выполнения очень важной операции с БД в сломанной транзакции можно использовать запуск фонового задания.

Рекомендуемая структура кода транзакции

  • метод НачатьТранзакцию рекомендуется располагать за пределами блока Попытка-Исключение непосредственно перед оператором Попытка;

  • все действия, выполняемые после вызова метода НачатьТранзакцию, должны находиться в одном блоке Попытка, в том числе чтение, блокировка и обработка данных;

  • метод ЗафиксироватьТранзакцию рекомендуется располагать последним в блоке Попытка перед оператором Исключение, чтобы  гарантировать, что после ЗафиксироватьТранзакцию не возникнет исключение;

  • необходимо предусмотреть обработку исключений – в блоке Исключение нужно сначала вызвать метод ОтменитьТранзакцию, а затем выполнять другие действия, если они требуются;

  • рекомендуется в блоке Исключение делать запись в журнал регистрации;

  • в конце блока Исключение рекомендуется добавить оператор ВызватьИсключение

Пример (кнопка «Обработка ошибки в явной транзакции»)

Процедура ОбработкаОшибкиВЯвнойТранзакции1() Экспорт
НачатьТранзакцию(); // Уровень = 1
Попытка
Ссылка = Справочники.БезОбработчиков.Тест1;
ПредставлениеСсылки = "" + Ссылка; // Подготовка данных для возможной обработки исключения. Это может быть неоправданное обращение к БД.
НачатьТранзакцию(); // Уровень = 2
Попытка
Запрос = Новый Запрос("ВЫБРАТЬ 1/0");
Запрос.Выполнить();
ЗафиксироватьТранзакцию(); // Уровень = 2
Исключение
ОтменитьТранзакцию(); // Уровень = 2
//ЗаписьЖурналаРегистрации("МойОшибка", УровеньЖурналаРегистрации.Ошибка, Ссылка.Метаданные(), Ссылка); // Так получим невосстановимую ошибку, если представления ссылки будет обновлено в кэше
ЗаписьЖурналаРегистрации("МойОшибка", УровеньЖурналаРегистрации.Ошибка, Ссылка.Метаданные(), СсылкаДляПередачиВЖурналРегистрации(Ссылка));
Сообщить("Обработана ошибка БД - пытались записать """ + ПредставлениеСсылки + """");
ВызватьИсключение;
КонецПопытки;
ЗафиксироватьТранзакцию(); // Уровень = 1
Исключение
ОтменитьТранзакцию(); // Уровень = 1
Сообщить("Обработана ошибка БД");
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры

Шаблон транзакции

Создайте себе шаблон текста транзакции (команда «Сервис»/»Шаблоны текста» конфигуратора)

НачатьТранзакцию();
Попытка
<?>
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
// Сюда писать код обработки ошибки
ВызватьИсключение;
КонецПопытки;

Скрытая отмена транзакции

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

 НачатьТранзакцию();
Попытка
Объект = Справочники.БезОбработчиков.тест1.ПолучитьОбъект();
Объект.Наименование = ТекущаяДата();
Объект.Записать();
Запрос = Новый Запрос("ВЫБРАТЬ 1/0");
Запрос.Выполнить(); // Ошибка возникла при выполнении операции БД, поэтому установлен признак "Отменена" менеджера транзакции
Исключение
Сообщить("Обработана ошибка БД");
КонецПопытки;
ЗафиксироватьТранзакцию(); // Отмена фактической транзакции, т.к. Глубина стала 0 и Отменена = Истина

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

Процедура ЗафиксироватьТранзакциюОбязательно() Экспорт
Если ВТранзакцииПроисходилиОшибки() Тогда
ВызватьИсключение "В данной транзакции уже происходили ошибки";
Иначе
ЗафиксироватьТранзакцию();
КонецЕсли;
КонецПроцедуры

Другим примером скрытой отмены транзакции является завершение потока встроенного языка, которое происходит при

  • завершение сеанса
  • завершение клиентским приложением обработки команды пользователя

Разрыв неявной транзакции

Этот пример демонстрирует довольно неочевидную возможность разорвать фактическую транзакцию программной записи объекта. Не рекомендую применять это, однако рекомендую изучить для лучшего понимая всех тонкостей работы транзакций нопка «Разрыв записи объекта», справочник РазрывЗаписиОбъекта модуль Объекта)

Процедура ПередЗаписью(Отказ)
ЭтотОбъект.Наименование = ТекущаяДата();
А = Справочники.БезОбработчиков.тест1.ПолучитьОбъект();
А.Реквизит1 = ТекущаяДата();
А.Записать();
Попытка
Запрос = Новый Запрос("ВЫБРАТЬ 1/0");
Запрос.Выполнить(); // Ошибка возникла при выполении операции БД, поэтому установлен признак "Отменена" менеджера транзакции
Исключение
КонецПопытки;
Если ОбщийМодуль1.ВТранзакцииПроисходилиОшибки() Тогда
ЗафиксироватьТранзакцию(); // Уменьшили внутреннее свойство "Глубина". Оно стало 0 и установлен признак "Отменена", поэтому фактическая транзакция отменилась.
// Завершены неявная логическая и фактическая транзакции. Транзакционные блокировки удалены. Изменения объекта ЭтотОбъект пока остались только в памяти.
КонецЕсли;
// Выполняется вне фактической транзакции
А = Справочники.БезОбработчиков.Тест1.ПолучитьОбъект();
А.Наименование = ТекущаяДата();
А.Записать();
// Изменения объекта А уже зафиксированы, т.к. сделаны вне фактической транзакции
Если Не ТранзакцияАктивна() Тогда
НачатьТранзакцию(); // Откроем явную логическую и фактическую транзакции, чтобы внутренная логика фиксации транзакции метода Записать() объекта данных не выбросила исключение
КонецЕсли;
КонецПроцедуры

Если же попробовать сделать тот же фокус с интерактивной записью объекта, то на вызове ЗафиксироватьТранзакцию() будет выброшено исключение «Транзакция не активна».

Полезные статьи по теме

  1. Правила использования транзакций (ИТС)
  2. Особенности работы объектов при отмене транзакции (ИТС)
  3. Ошибки базы данных и транзакции (ИТС)
  4. Невосстановимые и восстановимые исключения (ИТС)
  5. Вложенность транзакций (ИТС)
  6. Вы не умеете работать с транзакциями (habr)

Доброго дня!

Есть две базы (допустим): Ут 10-ка (на 8.2.19.83) и БП 2 (8.3.12.1616). Есть типовая УниверсальныйОбменДаннымиXML и некие правила, пусть передающие выбранную номенклатуру. Подключение прямое к ИБ, база приемник на сервере. Во вкладке «Загрузка данных» установлен флаг «Использовать транзакции».

При такой конфигурации обмен заканчивается ошибкой:

Произошла исключительная ситуация (1C:Enterprise 8.3.12.1616): {Обработка.УниверсальныйОбменДаннымиXML.МодульОбъекта(12482)}: Ошибка при вызове метода контекста (ЗафиксироватьТранзакцию)

Транзакция не активна

Исследования показали, что транзакция в случае установленного флага открывается в «ВыполнитьДействияПередЧтениемДанных()» и закрывается в «ВыполнитьДействияПослеЗавершенияЧтенияДанных()» и из второй функции как раз происходит падение в ошибку. Значит к этому моменту транзакция не открыта.

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

Чем объяснить такое поведение?

спасибо

Разбираемся с опасностями использования транзакций во встроенном языке 1С. Познаем ошибку «В данной транзакции уже происходили ошибки». Учимся защищаться от них.

Статья актуальна для версии платформы 8.3.14

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

Транзакции применяются для целостного изменения связанных данных, т.е. все действия с базой данных, выполняемые в рамках транзакции или выполняются целиком, или целиком откатываются.

  1. Менеджер транзакции
    1. Это условное название единого для сеанса базы 1С внутреннего объекта платформы, управляющего транзакцией. Единый для сеанса значит, что он синхронизируется между толстым клиентским и серверным контекстном сеанса.
    2. Свойства менеджера транзакции
      1. Глубина/Вложенность/Счетчик — целое число — сколько раз была открыта транзакция минус сколько раз была закрыта
      2. Отменена – булево — признак отмены транзакции
  2. Вложенные транзакции
    1. Менеджер транзакции содержит свойство “Глубина”. Если оно больше 1, то транзакция считается вложенной.
    2. Логические (1С) и фактические (СУБД) транзакции
      1. Логическая (1С) транзакция — все операции между началом транзакции и следующим завершением транзакции с тем же значением глубины/вложенности/счетчика транзакций
      2. Фактическая (СУБД) транзакция — совпадает с логической транзакцией с Глубина = 1
    3. Вложенными в 1С могут быть только логические транзакции
    4. При начале логической транзакции увеличивается на 1 свойство “Глубина” менеджера транзакции
    5. При завершении логической транзакции уменьшается на 1 свойство “Глубина” менеджера транзакции
    6. Определить во встроенном языке значение свойства Глубина или хотя бы наличие одной вложенности, не изменяя состояние менеджера транзакции, невозможно.
    7. Вложенность транзакций (ИТС)
    8. Правила использования транзакций (ИТС)
  3. Явные и неявные логические транзакции
    1. Явные — начинаются/завершаются методами встроенного языка
      1. НачатьТранзакцию
      2. ЗафиксироватьТранзакцию/ОтменитьТранзакцию
    2. Неявные — начинаются/завершаются платформой в начале/конце выполнения записи объектов данных. При этом программная запись влияет на свойство Глубина, а интерактивная нет.
    3. Определить во встроенном языке, является ли транзакция явной/неявной, невозможно.
  4. Сломанные транзакции
    1. Менеджер транзакции содержит признак “Отменена”. Сбрасывается он только при начале фактической транзакции. Если он установлен, то транзакция считается сломанной и фактическая транзакция подлежит отмене при ее любом завершении. Устанавливается он при возникновении ошибки базы данных и при вызове ОтменитьТранзакцию().
    2. Определить во встроенном языке, является ли транзакция сломанной, напрямую невозможно, но можно косвенно с достаточной долей уверенности. Пример будет рассмотрен далее.
    3. Примеры ошибок базы данных
      1. Ошибка выполнения запроса
      2. Необработанное исключение при записи объекта
      3. Отказ при записи объекта (“Не удалось записать <объект>”)
      4. Ошибка установки транзакционной блокировки
      5. Ошибка установки объектной блокировки
    4. Ошибки базы данных и транзакции (ИТС)
    5. Невосстановимые и восстановимые исключения (ИТС)
    6. При обращении к БД в сломанной транзакции платформа выбрасывает ошибку “В данной транзакции уже происходили ошибки”. Здесь возникает разрыв между первичной ошибкой, ломающей транзакцию, и этой вторичной ошибкой. Из-за этого разрыва разработчику обычно бывает очень тяжело добраться до причины первичной ошибки. Поэтому к обработке ошибок в сломанной транзакции нужно подходить очень аккуратно. Пример будет рассмотрен далее.
  5. Работа со ссылками в фактической (СУБД) транзакции
    1. Кэш представлений ссылок
      1. Транзакция имеет собственный кэш представлений ссылок.
      2. Обращение к представлению ссылки может вызывать неявное обращение к БД для обновления кэша представления по этой ссылке.
      3. Примеры обращений к представлению ссылки
        1. “” + Ссылка
        2. Таблица.Сортировать(“Ссылка”) — платформа считывает представления ссылок для сортировки по ним
        3. ЗаписьЖурналаРегистрации(,,, Ссылка) — всегда получает представление от ссылки
      4. При обработке исключений в транзакции часто возникает потребность вывода диагностической информации в лог/пользователю. Тут кроется самая коварная особенность сломанной транзакции. При получении представления ссылки с обновлением кэша в сломанной транзакции платформа выбрасывает необрабатываемое исключение без указания исходной строки, в которой выполнено это обращение. Причем если код выполняется внутри неявной транзакции, то исключение является восстановимым, а иначе не восстановимым. Об этом очень неудобном поведении я сообщал в 1С в 2013г и в 2023г, но невосстановимость этой ошибки до сих пор осталась. Далее будет приведен безопасный подход к решению задачи.
    2. Объектный кэш
      1. Транзакция имеет собственный объектный кэш.
      2. Обращение к полю ссылки может вызывать неявное обращение к БД для обновления кэша объекта по этой ссылке.
      3. Примеры обращений к объектному кэшу
        1. Ссылка.ПометкаУдаления;
        2. Ссылка.ПолучитьОбъект();
      4. Аналогично кэшу представлений ссылок при обращении к объектному кэшу в сломанной транзакции платформа может выбрасывать необрабатываемые и невосстановимые исключения, но в меньшем числе ситуаций.
  6. Взаимоблокировка (Deadlock)
    1. Чтобы снизить вероятность появления взаимоблокировок, нужно стараться устанавливать управляемые блокировки, нужные для всех вложенных транзакций, в самом начале фактической (СУБД) транзакции. Тогда выполнение кода установки управляемых блокировок во вложенных транзакциях не будет изменять состав заблокированных ресурсов и тем самым порядок захвата ресурсов в транзакции будет более стабильным и предсказуемым.

Таблица операций, воздействующих на менеджер транзакции

Операция

Выброс исключения

Признак отмены транзакции

Глубина

Фактическая транзакция (СУБД)

Начало записи объекта (неявное начало транзакции)

Если признак отмены был Истина, то “В данной транзакции уже происходили ошибки”

Ложь, если глубина была 0

+1 при программной записи;

не меняется при интерактивной записи
 

Открывается, если глубина была 0

Конец записи объекта (неявный конец транзакции)

  • Если признак отмены был Истина, то “В данной транзакции уже происходили ошибки” (кнопка «Сломанная неявная транзакция»)
  • Если признак отмены был Ложь и стал Истина, то “Не удалось записать <объект>”.
  • Если глубина была 0, то “Ошибка SDBL: Открытых транзакций нет(кнопка «Отмена транзакции в неявной транзакции»)

Истина, если Отказ = Истина или выброшено исключение

-1 при программной записи;

не меняется при интерактивной записи

Если глубина стала 0

  • Фиксируется, если признак отмены стал Ложь.
  • Откатывается, если признак отмены стал Истина.

НачатьТранзакцию

 

Ложь, если глубина была 0

+1

Открывается, если глубина была 0

ОтменитьТранзакцию

Если глубина была 0, то “Транзакция неактивна”

Истина

-1

Откатывается, если глубина стала 0.

ЗафиксироватьТранзакцию

Если глубина была 0, то “Транзакция неактивна”

не меняется

-1

Если глубина стала 0

  • Фиксируется, если признак отмены стал Ложь.
  • Откатывается, если признак отмены стал Истина.

Операция (явная или неявная) с БД в транзакции

Если признак отмены был Истина, то “В данной транзакции уже происходили ошибки”

  • восстановимое — для операций без обновления кэша объектного/представлений (кнопка «Ошибка обращения к БД в сломанной явной транзакции»)
  • невосстановимое — для операций с обновлением кэша объектного/представлений (кнопка «Невосстановимая ошибка обращения к ссылке в сломанной явной транзакции»)

Истина, если выброшено исключение

не меняется

не меняется

Завершение потока встроенного языка     0

Если глубина была > 0, Откатывается

Исключение в транзакции

Ошибки в транзакции могут быть

  1. Ломающие — связаны с БД, выставляют признак «Отменена» менеджера транзакции
  2. Неломающие — не связаны с БД, не воздействуют на менеджер транзакции

Проверка сломанной транзакции

Хотя явно получить значение признака “Отменена” менеджера транзакции во встроенном языке нельзя. Однако можно воспользоваться косвенным методом — попытаться выполнить простейшую операцию БД (общий модуль ОбщийМодуль1)

// Будет ли выброшено исключение "В данной транзакции уже происходили ошибки" (является ли транзакция сломанной)
Функция ВТранзакцииПроисходилиОшибки() Экспорт
Запрос = Новый Запрос("ВЫБРАТЬ 1");
Попытка
Запрос.Выполнить();
Результат = Ложь;
Исключение
Результат = Истина;
КонецПопытки;
Возврат Результат;
КонецФункции

При необходимости устранить ложные срабатывания, можно будет еще анализировать описание ошибки. Кстати обращения к серверу СУБД этот запрос не производит (только к модели БД).

Подготовка данных для обработки исключения в сломанной транзакции

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

Передача ссылки в журнал регистрации в сломанной транзакции

При обработке исключения в сломанной транзакции часто разумно писать диагностическую информацию в журнал регистрации. При этом метод ЗаписьЖурналаРегистрации() неявно берет представление от ссылки, используя кэш представлений ссылок, и помещает его в поле «Представление данных» события журнала. Обращение к этому кэшу в сломанной транзакции несет риск невосстановимой ошибки. Поэтому рекомендую передавать ссылку в журнал регистрации только через эту функцию (общий модуль ОбщийМодуль1)

// Помещение представления ссылки в кэш представлений в сломанной транзакции вызывает досрочное завершение фактической транзакции с невосстановимой ошибкой "В данной транзакции уже происходили ошибки"
// Передача в метод ЗаписьЖурналаРегистрации() ссылки на объект в сломанной транзакции приведет к такому исключению, если представление будет обновлено в кэше.
// Поэтому в таком случае функция возвращает строку идентификатор ссылки. Подразумевается что такие места в коде будут устраняться и потому возвращаться идентификатор будет достаточно редко.
Функция СсылкаДляПередачиВЖурналРегистрации(Ссылка) Экспорт
Если ВТранзакцииПроисходилиОшибки() Тогда
Результат = "" + Ссылка.УникальныйИдентификатор();
Иначе
Результат = Ссылка;
КонецЕсли;
Возврат Результат;
КонецФункции

Тогда безопасная запись в журнал регистрации при обработке исключения в транзакции может выглядеть так

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

Открывать и закрывать транзакцию в одном методе

Старайтесь открывать и закрывать логическую транзакцию в одном методе. Это значительно облегчает отладку и анализ кода. Следствие этой рекомендации — выполнять код транзакции в попытке, чтобы в исключении можно было отменить транзакцию. Иначе исключение поднимется по стеку (в вызывающий метод) без закрытия логической транзакции (восстановления свойства Глубина менеджера транзакции) в текущем методе. Часто и особенно во вложенных транзакциях при обработке такого исключения и после возможной регистрации диагностической информации исключение перевыбрасывают. Даже если на текущий момент ваша транзакция не вызывается вложенно потом это может измениться. Поэтому разумно сразу об этом позаботиться.

Операции с БД в сломанной транзакции

Иногда бывает нужно в сломанной транзакции (например при обработке исключения) выполнить очень важную операцию с БД.

В таком случае может быть оправдано пренебречь рекомендацией «Открывать и закрывать транзакцию в одном методе» и завершить фактическую транзакцию в текущем месте, чтобы сделать возможным выполнение этой важной операции с БД.

 Исключение
Пока ТранзакцияАктивна() Цикл
ОтменитьТранзакцию();
КонецЦикла;
ЗаписатьИнформациюОбОшибкеВРегистр();
...
КонецПопытки;

Следует иметь ввиду, что такой прием повышает вероятность возникновения ошибок во внешних транзакциях. Самый негативный сценарий — текущая транзакция была вложена в неявную фактическую транзакцию записи объекта. Тогда при завершении записи объекта будет невосстановимое исключение (кнопка «Невосстановимая ошибка после отмены транзакции в неявной транзакции»).

Также для выполнения очень важной операции с БД в сломанной транзакции можно использовать запуск фонового задания.

Рекомендуемая структура кода транзакции

  • метод НачатьТранзакцию рекомендуется располагать за пределами блока Попытка-Исключение непосредственно перед оператором Попытка;

  • все действия, выполняемые после вызова метода НачатьТранзакцию, должны находиться в одном блоке Попытка, в том числе чтение, блокировка и обработка данных;

  • метод ЗафиксироватьТранзакцию рекомендуется располагать последним в блоке Попытка перед оператором Исключение, чтобы  гарантировать, что после ЗафиксироватьТранзакцию не возникнет исключение;

  • необходимо предусмотреть обработку исключений – в блоке Исключение нужно сначала вызвать метод ОтменитьТранзакцию, а затем выполнять другие действия, если они требуются;

  • рекомендуется в блоке Исключение делать запись в журнал регистрации;

  • в конце блока Исключение рекомендуется добавить оператор ВызватьИсключение

Пример (кнопка «Обработка ошибки в явной транзакции»)

Процедура ОбработкаОшибкиВЯвнойТранзакции1() Экспорт
НачатьТранзакцию(); // Уровень = 1
Попытка
Ссылка = Справочники.БезОбработчиков.Тест1;
ПредставлениеСсылки = "" + Ссылка; // Подготовка данных для возможной обработки исключения. Это может быть неоправданное обращение к БД.
НачатьТранзакцию(); // Уровень = 2
Попытка
Запрос = Новый Запрос("ВЫБРАТЬ 1/0");
Запрос.Выполнить();
ЗафиксироватьТранзакцию(); // Уровень = 2
Исключение
ОтменитьТранзакцию(); // Уровень = 2
//ЗаписьЖурналаРегистрации("МойОшибка", УровеньЖурналаРегистрации.Ошибка, Ссылка.Метаданные(), Ссылка); // Так получим невосстановимую ошибку, если представления ссылки будет обновлено в кэше
ЗаписьЖурналаРегистрации("МойОшибка", УровеньЖурналаРегистрации.Ошибка, Ссылка.Метаданные(), СсылкаДляПередачиВЖурналРегистрации(Ссылка));
Сообщить("Обработана ошибка БД - пытались записать """ + ПредставлениеСсылки + """");
ВызватьИсключение;
КонецПопытки;
ЗафиксироватьТранзакцию(); // Уровень = 1
Исключение
ОтменитьТранзакцию(); // Уровень = 1
Сообщить("Обработана ошибка БД");
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры

Шаблон транзакции

Создайте себе шаблон текста транзакции (команда «Сервис»/»Шаблоны текста» конфигуратора)

НачатьТранзакцию();
Попытка
<?>
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
// Сюда писать код обработки ошибки
ВызватьИсключение;
КонецПопытки;

Скрытая отмена транзакции

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

 НачатьТранзакцию();
Попытка
Объект = Справочники.БезОбработчиков.тест1.ПолучитьОбъект();
Объект.Наименование = ТекущаяДата();
Объект.Записать();
Запрос = Новый Запрос("ВЫБРАТЬ 1/0");
Запрос.Выполнить(); // Ошибка возникла при выполнении операции БД, поэтому установлен признак "Отменена" менеджера транзакции
Исключение
Сообщить("Обработана ошибка БД");
КонецПопытки;
ЗафиксироватьТранзакцию(); // Отмена фактической транзакции, т.к. Глубина стала 0 и Отменена = Истина

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

Процедура ЗафиксироватьТранзакциюОбязательно() Экспорт
Если ВТранзакцииПроисходилиОшибки() Тогда
ВызватьИсключение "В данной транзакции уже происходили ошибки";
Иначе
ЗафиксироватьТранзакцию();
КонецЕсли;
КонецПроцедуры

Другим примером скрытой отмены транзакции является завершение потока встроенного языка, которое происходит при

  • завершение сеанса
  • завершение клиентским приложением обработки команды пользователя

Разрыв неявной транзакции

Этот пример демонстрирует довольно неочевидную возможность разорвать фактическую транзакцию программной записи объекта. Не рекомендую применять это, однако рекомендую изучить для лучшего понимая всех тонкостей работы транзакций нопка «Разрыв записи объекта», справочник РазрывЗаписиОбъекта модуль Объекта)

Процедура ПередЗаписью(Отказ)
ЭтотОбъект.Наименование = ТекущаяДата();
А = Справочники.БезОбработчиков.тест1.ПолучитьОбъект();
А.Реквизит1 = ТекущаяДата();
А.Записать();
Попытка
Запрос = Новый Запрос("ВЫБРАТЬ 1/0");
Запрос.Выполнить(); // Ошибка возникла при выполении операции БД, поэтому установлен признак "Отменена" менеджера транзакции
Исключение
КонецПопытки;
Если ОбщийМодуль1.ВТранзакцииПроисходилиОшибки() Тогда
ЗафиксироватьТранзакцию(); // Уменьшили внутреннее свойство "Глубина". Оно стало 0 и установлен признак "Отменена", поэтому фактическая транзакция отменилась.
// Завершены неявная логическая и фактическая транзакции. Транзакционные блокировки удалены. Изменения объекта ЭтотОбъект пока остались только в памяти.
КонецЕсли;
// Выполняется вне фактической транзакции
А = Справочники.БезОбработчиков.Тест1.ПолучитьОбъект();
А.Наименование = ТекущаяДата();
А.Записать();
// Изменения объекта А уже зафиксированы, т.к. сделаны вне фактической транзакции
Если Не ТранзакцияАктивна() Тогда
НачатьТранзакцию(); // Откроем явную логическую и фактическую транзакции, чтобы внутренная логика фиксации транзакции метода Записать() объекта данных не выбросила исключение
КонецЕсли;
КонецПроцедуры

Если же попробовать сделать тот же фокус с интерактивной записью объекта, то на вызове ЗафиксироватьТранзакцию() будет выброшено исключение «Транзакция не активна».

Полезные статьи по теме

  1. Правила использования транзакций (ИТС)
  2. Особенности работы объектов при отмене транзакции (ИТС)
  3. Ошибки базы данных и транзакции (ИТС)
  4. Невосстановимые и восстановимые исключения (ИТС)
  5. Вложенность транзакций (ИТС)
  6. Вы не умеете работать с транзакциями (habr)

1

2

Показывать по
10
20
40
сообщений

Новая тема

Ответить

Klyacksa

Дата регистрации: 04.05.2009
Сообщений: 20

Доброго времени суток!<br><br>Вопрос в следующем:<br>При выполнении регламентного задания (в клиент-серверной версии базы, выполнение на сервере) возникла ошибка «В данной транзакции уже происходили ошибки!», и, естественно, при каждом последующем запуске оно вылетает на эту же ошибку, даже не пытаясь ничего реально запускать. <br><br>Я сталкивалась с подобной ошибкой при выполнении кода на клиенте, и в этом случае спасало просто перезайти в базу — дальше все работало. А если на стороне сервера возникает эта ошибка? Каждый раз перезагружать сервер никто не даст. <br><br>Как сбросить эту ошибку, чтобы продолжить функционирование регламентного задания, почему ошибка могла возникнуть и как избежать ее повторения?

BelikovS

Дата регистрации: 05.03.2007
Сообщений: 1701

В основном такая штука случается, когда внутри одной транзакции оказывается другая, в которой собственно и присходит ошибка. Самый простой способ её увидеть:<br> Открываем транзакцию, начинаем проводить документы, причем проведение стоит в Попытка-Исключение, чтобы программа не вываливалась. Какой-то докумен не может быть проведен и выкидывает ошибку, после этого Зафиксировать транзакцию уже нельзя.<br><br>Вывод — либо отказаться от транзакции, либо от обработки ошибок. Я выбрал отказ от транзакции в таких случаях или закрываю транзакцию до записи объектов (особенно если они типовые).<br><br>> продолжить функционирование регламентного задания<br>Продолжить никак. Лучше всего его полностью остановить и запустить снова.<br>

Klyacksa

Дата регистрации: 04.05.2009
Сообщений: 20

«Продолжить — я имела ввиду не текущее выполнение регламентного задания, а работу рег.задания при последующих запусках. Кого остановить? Рег.задание?<br><br>Поясните, пожалуйста, подробней.<br>Почему нельзя Зафиксировать транзакцию, если, к примеру, используется конструкция:<br><br>НачатьТранзакцию();<br> Попытка<br>      // проводим докумены<br> Исключение<br>      // обработка исключения<br> КонецПопытки;<br>ЗафиксироватьТранзакцию();<br><br>И что значит «отказаться от отработки ошибок»? То есть, убрать из этой конструкции Попытку-Исключение?»

BelikovS

Дата регистрации: 05.03.2007
Сообщений: 1701

«> Поясните, пожалуйста, подробней.<br>> Почему нельзя Зафиксировать транзакцию, если, к примеру, используется конструкция:<br>> <br>> НачатьТранзакцию();<br>> Попытка<br>>       // проводим докумены<br>> Исключение<br>>       // обработка исключения<br>> КонецПопытки;<br>> ЗафиксироватьТранзакцию();<br>> <br>> И что значит «отказаться от отработки ошибок»? То есть, убрать из этой конструкции Попытку-Исключение?<br><br>Представть, что ваша конструкция теперь выглядит так (просто есть кусок которого вы не видите — не вы его писали):<br><br>НачатьТранзакцию();<br> Попытка<br>       //Вот этот код где-то там — далеко в каком-нибудь модуле, да и 1С, что-то такое ведет в случае проведения<br>       НачатьТранзакцию();<br>       Попытка<br>            //Здесь возникает ошибка<br>       Исключение<br>       ОтменитьТранзакцию();<br>       КонецПопытки;<br>       ЗафиксироватьТранзакцию();<br> Исключение<br>       // обработка исключения<br> КонецПопытки;<br>ЗафиксироватьТранзакцию();<br><br>Так вот, если я правильно понимаю, то если вложенная транзакция закрывается, то транзакция верхнего уровня это видит и отказывается закрываться (возможно она уже тоже закрытая). В общем — кроме отмены транзакции уже ничего не сделать.<br><br>Вот и получается, что несмотря на обработку ошибки (исключения) — ошибка уже была и ничего с этим не сделать. Значит надо от чего-то отказаться. Или увидеть ошибку или убрать транзакцию.<br>»

BelikovS

Дата регистрации: 05.03.2007
Сообщений: 1701

> Кого остановить? Рег.задание?<br>А что у вас показывает «Консоль Заданий » по поводу вашего зависшего задания?

Klyacksa

Дата регистрации: 04.05.2009
Сообщений: 20

«1> НачатьТранзакцию();<br>2>      Попытка<br>3>       //Вот этот код где-то там — далеко в каком-нибудь модуле, да и 1С, что-то такое ведет в случае проведения<br>4>       НачатьТранзакцию();<br>5>       Попытка<br>6>             //Здесь возникает ошибка<br>7>       Исключение<br>8>       ОтменитьТранзакцию();<br>9>       КонецПопытки;<br>10>       ЗафиксироватьТранзакцию();<br>11>      Исключение<br>12>       // обработка исключения<br>13>      КонецПопытки;<br>14> ЗафиксироватьТранзакцию();<br><br>В этой последовательности получается, что если мы попадаем на отмену транзакции в строке 8, то потом в строке 10 закрывается не внутренняя транзакция, а внешняя, и при закрытии ее повторно в 14 появляется ошибка. В данном случае, я бы предложила фиксацию внутренней транзакции перенести во внутреннюю попытку (в самый конец). Но это уже так, отступление. <br><br>Ок, в этой последовательности понятно, спасибо. Только будет очень трудно найти, где же оно в реальном коде ломается. <br><br>>> Кого остановить? Рег.задание?<br>>А что у вас показывает «Консоль Заданий » по поводу вашего зависшего задания? <br><br>Вот «КонсольЗаданий» как раз и показывала в графе Ошибка «В данной транзакции уже происходили ошибки!». Правда, потом убрали Использование Рег.задания, а сегодня опять его запустили — ошибка прошла. Но есть вероятность, что сервер перезагружали, узнать пока точно не могу — это сервер нашего филиала в другом городе. «

Klyacksa

Дата регистрации: 04.05.2009
Сообщений: 20

Попробовала на тестовой клиент-серверной базе реализовать описанный выше механизм — возникает ошибка «Ошибка при вызове метода контекста (ЗафиксироватьТранзакцию): Транзакция не активна»<br><br>Может, все-таки ошибка «В данной транзакции уже происходили ошибки» возникает наоборот от незакрытой (и не отмененной) транзакции, когда она остается висеть в базе?

BelikovS

Дата регистрации: 05.03.2007
Сообщений: 1701

> Попробовала на тестовой клиент-серверной базе реализовать описанный выше механизм — возникает ошибка «Ошибка при вызове метода контекста (ЗафиксироватьТранзакцию): Транзакция не активна»<br><br>Правильно, я вспомнил, она действительно так пишет.<br><br>> Может, все-таки ошибка «В данной транзакции уже происходили ошибки» возникает наоборот от незакрытой (и не отмененной) транзакции, когда она остается висеть в базе?<br><br>Так транзакция уже закрыта (что вам и было сообщено выще). А это сообщение в таком случае наоборот от того, что вы пытаетесь Отменить уже Закрытую транзакцию.<br><br><br>

Klyacksa

Дата регистрации: 04.05.2009
Сообщений: 20

Неа, при отмене закрытой (или отмененной) транзакции тоже пишется сообщение про то, что транзакция не активна. То же самое, что и при попытке закрыть закрытую (или отмененную) транзакцию :)<br><br>И еще сопутствующий вопрос: просмотрела весь код регламентного задания, все транзакции закрыты грамотно. В чем может быть проблема в этом случае? Может, могут быть грабли с встроенными транзакциями (которые мы сами не вызываем, которые сопутствуют записи объектов, к примеру)?

BelikovS

Дата регистрации: 05.03.2007
Сообщений: 1701

«Погонял я транзакции.<br>Результат такой: Это сообщение возникает в случае, когда:<br>1. Была ошибка<br>2. Транзакция еще не закрыта<br>3. Происходит операция с базой.<br>Например:<br>НачатьТранзакцию ();<br>Попытка<br> Объект = Ссылка.ПолучитьОбъект();<br> Объект.Код = «00000008476»; //Заведомо существующий<br> Объект.Записать();<br> ЗафиксироватьТранзакцию();<br>Исключение<br> Объект = Ссылка.ПолучитьОбъект();<br> Сообщить («Не Изменил» + Строка(Объект.Код));<br> ОтменитьТранзакцию();<br>КонецПопытки;»

Читают тему:

Понравилась статья? Поделить с друзьями:
  • Ошибка при вызове метода контекста данныеформывзначение
  • Ошибка при вызове метода контекста зарегистрироватьизменения
  • Ошибка получения обработки обслуживания при закрытии смены 1с
  • Ошибка при вызове метода контекста заполнить
  • Ошибка при вызове метода контекста выполнитьзадачуинтерактивно