Как гарантированно узнать об отмене транзакции после проведения документа и отправить уведомление?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

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

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

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

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

Принципы работы транзакций в 1С:Предприятие

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

  1. Атомарность (Atomicity): В 1С транзакции обеспечивают целостность данных. Это означает, что все изменения, произведенные в рамках одной транзакции, либо полностью применяются к базе данных (транзакция фиксируется), либо полностью отменяются (транзакция откатывается). Платформа 1С неявно начинает транзакцию при любой записи в базу данных, включая проведение документов.
  2. Отказ транзакции: Если в процессе записи или проведения документа, например, в событии ПриЗаписиНаСервере или ОбработкаПроведения, установить параметр Отказ в Истина, то вся текущая транзакция будет отменена. Все изменения, которые были сделаны в рамках этой транзакции, включая те, что были выполнены в подписках на события, будут аннулированы. Именно это и является причиной нашей проблемы.
  3. Отсутствие вложенных транзакций: В 1С нет настоящих вложенных транзакций в классическом понимании СУБД. Если вы используете методы НачатьТранзакцию(), ЗафиксироватьТранзакцию() и ОтменитьТранзакцию() внутри уже открытой транзакции (например, транзакции записи документа), то фактически вы работаете с транзакцией верхнего уровня. Отмена изменений во "вложенной" транзакции приведет к отмене всех изменений транзакции верхнего уровня.
  4. Обработка исключений: Возникновение исключения в коде само по себе не отменяет транзакцию немедленно, но устанавливает служебный флажок "Запрет успешного завершения". Реальная отмена транзакции произойдет, когда код дойдет до ОтменитьТранзакцию(), что обычно делается в блоке Исключение конструкции Попытка-Исключение.
  5. Запрет обращений к базе данных при активной транзакции с исключением: Важно помнить, что если в активной транзакции возникло исключение, то дальнейшие обращения к базе данных (даже неявные, например, для получения представления ссылки) могут привести к ошибке "В этой транзакции уже происходили ошибки". Это необходимо учитывать при обработке ошибок.

Исходя из этих принципов, становится очевидно, что любое действие, которое должно быть выполнено строго после успешной фиксации транзакции (т.е. после того, как мы убедились, что документ действительно записан и проведен), должно быть вынесено за пределы основной транзакции записи. Рассмотрим два основных подхода к решению этой задачи.

Решение 1: Использование фоновых заданий с проверкой актуальности объекта

Наиболее распространенный и гибкий способ гарантировать выполнение действий только после успешного завершения транзакции – это использование фоновых заданий. Этот подход позволяет отвязать длительные или необратимые операции (такие как отправка сообщений) от основной транзакции записи документа.

Принцип работы

  1. В подписке на событие ОбработкаПроведения (или любом другом месте, которое выполняется в рамках транзакции, но до потенциального Отказ = Истина) мы создаем и планируем фоновое задание.
  2. В фоновое задание мы передаем уникальные идентификаторы документа, а также его версию данных.
  3. Само фоновое задание не запускается немедленно. Оно попадает в очередь и будет запущено платформой только тогда, когда текущая сессия завершит свои транзакции (либо зафиксирует, либо откатит их).
  4. Когда фоновое задание начнет выполняться, оно должно сначала проверить, существует ли документ в базе данных и соответствует ли его текущая версия данных той, что была передана в фоновое задание. Это позволяет нам удостовериться, что транзакция записи документа была успешно зафиксирована, и документ не был откачен.
  5. Только после успешной проверки актуальности документа, фоновое задание выполняет целевое действие (например, отправляет сообщение в Telegram).

Шаги реализации

Рассмотрим, как это можно реализовать шаг за шагом:

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

Важные моменты при работе с фоновыми заданиями:

  1. Параметры: Передавайте в фоновое задание только примитивные типы данных (строки, числа, булево, УИД, даты) или ссылки. Не передавайте объекты целиком, так как они могут быть неактуальны к моменту запуска фонового задания. Использование УникальныйИдентификатор() и ВерсияДанных является правильной практикой.
  2. Блокировки: В сообщении 3 упоминается "выполняй блокировку записываемого объекта". Если ваше фоновое задание планирует не только читать данные, но и изменять их, тогда действительно стоит рассмотреть механизмы блокировок. Для простого чтения и отправки уведомления достаточно получить объект по ссылке и проверить его версию. Платформа сама позаботится о необходимых блокировках на чтение.
  3. Время выполнения: Учитывайте, что фоновое задание будет выполнено с некоторой задержкой, зависящей от загрузки сервера и настроек регламентных заданий. Если уведомление должно быть "мгновенным", то фоновые задания могут не подойти. Однако для большинства случаев "после проведения" такая задержка приемлема.
  4. Отладка: Отладка фоновых заданий требует специфических настроек отладчика 1С. Убедитесь, что вы умеете отлаживать код, выполняющийся на сервере в фоновом режиме.

Решение 2: Использование плана обмена с авторегистрацией

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

Принцип работы

  1. Мы создаем специальный план обмена (например, УведомленияОПроведении), который будет использоваться только для регистрации документов, требующих уведомления.
  2. Настраиваем авторегистрацию для необходимых видов документов в этом плане обмена. Это означает, что при каждом успешном изменении или записи документа (включая проведение) его ссылка будет автоматически регистрироваться в таблице изменений данного плана обмена для указанного узла.
  3. Создаем регламентное задание, которое периодически обрабатывает зарегистрированные изменения в этом плане обмена.
  4. В рамках регламентного задания мы получаем ссылки на зарегистрированные документы, выполняем необходимые действия (отправка уведомлений) и затем снимаем их с регистрации в плане обмена.

Шаги реализации

Рассмотрим, как это можно реализовать:

  1. Создание плана обмена: В конфигураторе создадим новый объект метаданных "План обмена", например, УведомленияОПроведенииДокументов.
    • Укажем состав плана обмена, включив в него наш документ (например, Документ.НашДокумент).
    • Настроим для этого документа авторегистрацию изменений. Это критически важно.
  2. Создание узла плана обмена: В режиме 1С:Предприятие нам потребуется создать хотя бы один узел для нашего плана обмена. Этот узел не будет использоваться для реального обмена данными, а скорее как "очередь" для регистрации уведомлений. Назовем его, например, "Уведомления".
    
    // Пример создания узла для плана обмена (можно выполнить один раз при старте системы)
    УзелУведомлений = ПланыОбмена.УведомленияОПроведенииДокументов.СоздатьЭлемент();
    УзелУведомлений.Наименование = "УведомленияДляРассылки";
    УзелУведомлений.Код = "00001"; // Уникальный код узла
    УзелУведомлений.Записать();
    
  3. Создание регламентного задания: Создадим регламентное задание, которое будет периодически считывать изменения из плана обмена.
    
    // Общий модуль, например, "МодульРегламентныхЗаданий"
    Процедура ОбработатьУведомленияОПроведении() Экспорт
        // Получаем узел, для которого регистрируются уведомления
        УзелУведомлений = ПланыОбмена.УведомленияОПроведенииДокументов.НайтиПоКоду("00001"); // Используем код созданного узла
    
        Если УзелУведомлений = Неопределено Тогда
            ЗаписьЖурналаРегистрации(
                "ОшибкаПланаОбмена",
                УровеньЖурналаРегистрации.Ошибка,
                ,
                ,
                "Не найден узел 'УведомленияДляРассылки' для плана обмена 'УведомленияОПроведенииДокументов'."
            );
            Возврат;
        КонецЕсли;
    
        // Начинаем выборку зарегистрированных объектов
        ВыборкаОбъектов = ПланыОбмена.ПолучитьИзменения(УзелУведомлений);
    
        Пока ВыборкаОбъектов.Следующий() Цикл
            ОбъектСсылка = ВыборкаОбъектов.Ссылка;
    
            Если ТипЗнч(ОбъектСсылка) = Тип("ДокументСсылка.НашДокумент") Тогда
                // Это наш документ, который был успешно проведен
                Попытка
                    ДокументОбъект = ОбъектСсылка.ПолучитьОбъект();
    
                    Если ДокументОбъект <> Неопределено И ДокументОбъект.Проведен Тогда
                        Сообщить("Документ " + ДокументОбъект + " успешно проведен. Отправляем уведомление в Telegram (через план обмена).");
                        // Здесь размещаем код для отправки сообщения в Telegram
                        // Например:
                        // ТелеграмБот.ОтправитьСообщение(ОтветственныйПользователь.ИдентификаторTelegram,
                        //                                 "Документ " + ДокументОбъект + " проведен!");
                    Иначе
                        // Документ не найден или не проведен (возможно, был отменен после регистрации,
                        // или удален). В этом случае уведомлять не нужно.
                        ЗаписьЖурналаРегистрации(
                            "ДокументНеАктуален",
                            УровеньЖурналаРегистрации.Информация,
                            ОбъектСсылка.Метаданные(),
                            ОбъектСсылка,
                            "Документ " + ОбъектСсылка + " не актуален или не проведен для отправки уведомления."
                        );
                    КонецЕсли;
    
                    // Снимаем с регистрации после успешной обработки или принятия решения не уведомлять
                    ПланыОбмена.УдалитьРегистрациюИзменений(УзелУведомлений, ОбъектСсылка);
    
                Исключение
                    // Если произошла ошибка при обработке конкретного документа,
                    // записываем в журнал, но не снимаем с регистрации,
                    // чтобы попытаться обработать его позже.
                    ЗаписьЖурналаРегистрации(
                        "ОшибкаОбработкиУведомления",
                        УровеньЖурналаРегистрации.Ошибка,
                        ОбъектСсылка.Метаданные(),
                        ОбъектСсылка,
                        "Ошибка при обработке уведомления для документа " + ОбъектСсылка + ": " + ОписаниеОшибки()
                    );
                КонецПопытки;
            Иначе
                // Если в плане обмена зарегистрировались другие объекты, которые мы не хотим обрабатывать
                // или для которых логика другая, мы можем их проигнорировать или обработать по-своему.
                // Важно: если мы их не удалим из регистрации, они будут попадать в выборку снова.
                // Например, можно удалить из регистрации без обработки:
                ПланыОбмена.УдалитьРегистрациюИзменений(УзелУведомлений, ОбъектСсылка);
            КонецЕсли;
        КонецЦикла;
    КонецПроцедуры
    
  4. Настройка регламентного задания: В конфигураторе создаем новое регламентное задание, которое будет вызывать процедуру ОбработатьУведомленияОПроведении из МодульРегламентныхЗаданий. Настраиваем расписание выполнения, например, каждые 5-10 минут.

Преимущества использования планов обмена для уведомлений:

  1. Надежность: Если регламентное задание завершится с ошибкой, документы останутся зарегистрированными и будут повторно обработаны при следующем запуске задания.
  2. Масштабируемость: Подходит для систем с высокой нагрузкой и большим объемом изменений.
  3. Отсутствие блокировок: Основная транзакция записи документа не задерживается на отправку уведомлений.
  4. Отложенная обработка: Уведомления обрабатываются асинхронно, что повышает общую производительность системы.
  5. Прозрачность: Через служебные механизмы платформы (регистрация изменений) легко отслеживать, какие документы ожидают обработки.

Дополнительные рекомендации и важные аспекты

При реализации любых отложенных действий, особенно связанных с транзакциями, важно учитывать несколько общих моментов:

  1. Версионирование данных: Механизм ВерсияДанных, который мы используем в фоновом задании, является фундаментальным для проверки актуальности объекта. Он позволяет нам удостовериться, что документ не был изменен или откачен после того, как мы инициировали отложенное действие. Всегда используйте его для проверки перед выполнением критически важных операций.
  2. Журнал регистрации: Активно используйте ЗаписьЖурналаРегистрации() для фиксации всех значимых событий: создания фоновых заданий, их успешного выполнения, ошибок, а также фактов, когда документ не прошел проверку актуальности. Это поможет вам отлаживать систему и оперативно реагировать на проблемы.
  3. Учет доступа: Фоновые задания и регламентные задания обычно выполняются от имени пользователя с полными правами (или специально настроенного технического пользователя). Убедитесь, что у этого пользователя есть все необходимые права для чтения документов и выполнения прочих действий, которые предполагаются в рамках задания.
  4. Асинхронные вызовы (для 1С 8.3.18+): В последних версиях платформы 1С появились асинхронные функции (Асинх и Ждать), которые позволяют выполнять код в многопоточном режиме, не блокируя основной поток выполнения. Хотя их семантика отличается от фоновых заданий (они скорее для выполнения длительных операций без зависания интерфейса), в некоторых случаях их можно рассматривать для частичной асинхронности. Однако, для гарантированного выполнения после фиксации транзакции, фоновые задания или планы обмена остаются предпочтительными.
  5. Встроенные системы уведомлений: В некоторых конфигурациях (например, "1С:Документооборот") уже есть развитые механизмы уведомлений. Если вы работаете с такой конфигурацией, изучите и используйте ее штатные возможности, так как они, скорее всего, уже учитывают все нюансы транзакций и откатов. Например, в "1С:Документообороте" есть регламентное задание "УведомлениеПользователейОСобытиях", которое обрабатывает очередь уведомлений.

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

← На главную