В разработке на платформе 1С часто возникает задача автоматической обработки данных по расписанию. Для этого используются регламентные задания. Типичный сценарий: задание должно выбрать определенные документы или записи, выполнить над ними действие (например, отправить уведомление) и пометить их как обработанные, чтобы не захватывать их при следующем запуске. Однако иногда что-то идет не так, и задание начинает "зацикливаться" — обрабатывать одни и те же данные снова и снова, что может привести к массовым повторным рассылкам, дублированию записей и другим критическим ошибкам.
Рассмотрим такую ситуацию на конкретном примере. Автор столкнулся с проблемой, когда регламентное задание, предназначенное для отправки уведомлений по строкам документа, начало зацикливаться, несмотря на наличие флага для предотвращения повторной обработки. Проанализируем исходный код, выявим его слабые места и найдем несколько возможных причин такого поведения, а также предложим оптимальное решение.
Давайте посмотрим на первоначальный код, который выполняет рассылку.
&НаСервере
Процедура Отправить()
УстановитьПРивилегированныйРежим(Истина);
МассивКому = Новый Массив;
МассивВложений = Новый Массив;
// ...
Запрос = Новый Запрос;
Запрос.Текст = "
|ВЫБРАТЬ
| ХХХ_ПредварительныйЗаказПоставщикуОтметкиСклада.Ссылка КАК Ссылка,
| ХХХ_ПредварительныйЗаказПоставщикуОтметкиСклада.Ссылка.Номер КАК Номер,
| // ... другие поля
| ХХХ_ПредварительныйЗаказПоставщикуОтметкиСклада.НомерСтроки КАК НомерСтроки
|ИЗ
| Документ.ХХХ_ПредварительныйЗаказПоставщику.ОтметкиСклада КАК ХХХ_ПредварительныйЗаказПоставщикуОтметкиСклада
|ГДЕ
| // ... условия выборки
| И НЕ ХХХ_ПредварительныйЗаказПоставщикуОтметкиСклада.УведомлениеОтправлено = ИСТИНА
|
|УПОРЯДОЧИТЬ ПО
| РазностьДат";
// ... установка параметров и выполнение запроса
Рез = Запрос.Выполнить();
НомерПредыдущегоДокумента = Неопределено;
Если Не Рез.Пустой() Тогда
Выборка = Рез.Выбрать();
Пока Выборка.Следующий() Цикл
// Логика группировки писем по номеру документа
Если НомерПредыдущегоДокумента = Неопределено ИЛИ Не Выборка.Номер = НомерПредыдущегоДокумента Тогда
Сообщить("Отправляется " + Выборка.Номер);
// ... Формирование и отправка письма
РаботаСПочтовымиСообщениями.ОтправитьПочтовоеСообщение(УчетнаяЗаписьПочты, ПараметрыОтправки);
НомерПредыдущегоДокумента = Выборка.Номер;
КонецЕсли;
// Установка флага, что строка обработана
ДокПредварительныйЗаказПоставщику = Выборка.Ссылка.ПолучитьОбъект();
НайденнаяСтрока = ДокПредварительныйЗаказПоставщику.ОтметкиСклада.Найти(Выборка.НомерСтроки, "НомерСтроки");
НайденнаяСтрока.УведомлениеОтправлено = Истина;
ДокПредварительныйЗаказПоставщику.Записать();
КонецЦикла;
КонецЕсли;
КонецПроцедуры
В этом коде можно выделить несколько проблемных моментов.
Наиболее вероятной причиной "зацикливания", когда флаг `УведомлениеОтправлено` не устанавливается, являются конфликты блокировок. Разберем механизм их возникновения.
Регламентное задание выполняется в фоновом режиме, и платформа 1С, как правило, неявно открывает транзакцию на время выполнения значимых действий, особенно связанных с записью объектов.
Проследим за шагами выполнения кода в цикле:
ДокПредварительныйЗаказПоставщику = Выборка.Ссылка.ПолучитьОбъект(); — получаем объект документа.НайденнаяСтрока.УведомлениеОтправлено = Истина; — изменяем данные объекта в памяти.ДокПредварительныйЗаказПоставщику.Записать(); — пытаемся записать измененный объект в базу данных.В момент вызова метода Записать() система пытается установить эксклюзивную блокировку на изменяемые данные. Если в это же самое время другой пользователь (или другое фоновое задание) работает с этим же документом (например, проводит его или просто открыл для редактирования), то он уже удерживает блокировку на нем. В этом случае наше регламентное задание не сможет получить нужную блокировку и будет ждать ее освобождения. Если время ожидания превысит установленный таймаут, платформа сгенерирует исключение (ошибку "Конфликт блокировок" или "Тайм-аут на транзакции").
Ключевой момент: при возникновении такого исключения вся транзакция регламентного задания будет отменена. Это означает, что изменение флага `УведомлениеОтправлено = Истина` не сохранится в базе данных. Письмо при этом уже может быть отправлено. При следующем запуске регламентного задания запрос снова выберет ту же самую строку, так как флаг остался в значении `ЛОЖЬ`, и все повторится.
Как диагностировать? Первым делом нужно изучить Журнал регистрации. Ищите в нем сообщения об ошибках, содержащие слова "Конфликт блокировок", "тайм-аут", "deadlock" в момент времени, когда работало ваше регламентное задание.
Чтобы решить все перечисленные проблемы, код необходимо кардинально переработать, следуя двум основным принципам: минимизировать операции с базой данных и обрабатывать данные порциями (группировками).
Вместо того чтобы обрабатывать каждую строку отдельно, мы можем сгруппировать результат запроса по документу (`Ссылка`) и обработать все нужные строки одного документа за один раз.
Для этого изменим запрос и логику обхода результата.
// 1. Изменяем запрос, добавляя ИТОГИ по полю Ссылка
Запрос.Текст = "
|ВЫБРАТЬ
| ХХХ_ПредварительныйЗаказПоставщикуОтметкиСклада.Ссылка КАК Ссылка,
| ХХХ_ПредварительныйЗаказПоставщикуОтметкиСклада.НомерСтроки,
| ХХХ_ПредварительныйЗаказПоставщикуОтметкиСклада.Ссылка.Номер КАК Номер,
| ХХХ_ПредварительныйЗаказПоставщикуОтметкиСклада.Ссылка.Дата КАК Дата,
| ХХХ_ПредварительныйЗаказПоставщикуОтметкиСклада.Ссылка.Менеджер КАК Менеджер
|ИЗ
| Документ.ХХХ_ПредварительныйЗаказПоставщику.ОтметкиСклада КАК ХХХ_ПредварительныйЗаказПоставщикуОтметкиСклада
|ГДЕ
| НЕ ХХХ_ПредварительныйЗаказПоставщикуОтметкиСклада.УведомлениеОтправлено
| // ...прочие условия
|
|ИТОГИ ПО
| Ссылка";
Рез = Запрос.Выполнить();
// 2. Организуем обход по группировкам
ВыборкаДокументы = Рез.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаДокументы.Следующий() Цикл
// Внешний цикл - по документам
// Здесь мы работаем с одним документом
// Получаем объект документа ОДИН РАЗ
ДокОбъект = ВыборкаДокументы.Ссылка.ПолучитьОбъект();
// Готовим одно письмо для всех строк этого документа
ТекстПисьма = "Срочно предоставить документы по заказу №" + ДокОбъект.Номер + " от " + Формат(ДокОбъект.Дата, "ДЛФ=DD") + ":" + Символы.ПС;
// Внутренний цикл - по строкам текущего документа
ВыборкаСтроки = ВыборкаДокументы.Выбрать();
Пока ВыборкаСтроки.Следующий() Цикл
// Дополняем текст письма информацией из строки
ТекстПисьма = ТекстПисьма + " - позиция по строке №" + ВыборкаСтроки.НомерСтроки + Символы.ПС;
// Находим нужную строку в объекте и меняем флаг
НайденнаяСтрока = ДокОбъект.ОтметкиСклада.Найти(ВыборкаСтроки.НомерСтроки, "НомерСтроки");
Если НайденнаяСтрока <> Неопределено Тогда
НайденнаяСтрока.УведомлениеОтправлено = Истина;
КонецЕсли;
КонецЦикла;
// Формируем список получателей и отправляем ОДНО письмо
МассивКому.Очистить(); // Обязательно очищаем массив!
// ... код добавления адреса менеджера и контролера в МассивКому
// ... Отправка письма ...
РаботаСПочтовымиСообщениями.ОтправитьПочтовоеСообщение(...);
// Записываем измененный документ ОДИН РАЗ
Попытка
ДокОбъект.Записать(РежимЗаписиДокумента.Проведение); // или .Запись
Исключение
// Обязательно логируем ошибку, чтобы понять, почему запись не удалась
ЗаписьЖурналаРегистрации("Ошибка записи документа", УровеньЖурналаРегистрации.Ошибка, , , ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
Что мы изменили и почему это лучше:
ИТОГИ ПО Ссылка: Теперь запрос возвращает сгруппированные данные.ПолучитьОбъект() и Записать() вызываются только один раз для каждого документа, независимо от того, сколько его строк мы обрабатываем. Это кардинально снижает нагрузку и вероятность блокировок.Попытка-Исключение. Если произойдет ошибка блокировки, мы сможем зафиксировать ее в журнале регистрации и проанализировать причину, а работа задания не прервется аварийно.Применяя такой подход, вы не только исправляете ошибку зацикливания, но и создаете более надежный, производительный и масштабируемый механизм, который будет стабильно работать даже при больших объемах данных и высокой нагрузке на систему.