В процессе разработки в среде 1С:Предприятие часто возникает задача обеспечения уникальности выполнения регламентного задания. Представим ситуацию: у вас настроен импорт данных из внешней системы (например, загрузка заказов с сайта). Если процесс загрузки затянулся и пересекся со следующим запуском по расписанию, или если администратор запустил задание вручную, не дождавшись окончания автоматического процесса, может возникнуть неприятная ситуация — документы в базе начнут дублироваться, а нагрузка на сервер вырастет вдвое. В этой статье мы разберем несколько проверенных способов, как гарантированно избежать одновременного выполнения одного и того же задания.
Прежде всего, разберем штатный механизм платформы 1С. У объекта РегламентноеЗадание существует свойство Ключ (Key). Согласно документации, если у задания задан Ключ, то платформа при попытке старта нового экземпляра проверяет, нет ли уже работающего задания с таким же ключом в рамках данного регламентного задания.
Важный нюанс: если вы создали регламентное задание в конфигураторе и настроили ему расписание, 1С по умолчанию использует уникальный идентификатор (GUID) самого задания в качестве ключа. Это означает, что при штатной работе по расписанию платформа сама контролирует уникальность. Однако этот механизм может не сработать в следующих случаях:
ФоновыеЗадания.Выполнить(), не передавая туда уникальный ключ.rphost, при котором информация о состоянии задания в памяти менеджера кластера может временно рассинхронизироваться.Рассмотрим популярный способ, который часто применяют разработчики для самоконтроля внутри кода самого задания. Суть метода заключается в том, чтобы при старте процедуры опросить менеджер фоновых заданий и выяснить, нет ли уже активных процессов с таким же именем или ключом. Проанализируем пример кода:
Процедура ВыполнитьЗагрузкуДанных() Экспорт
// Определяем параметры для поиска активных заданий
ПараметрыЗадания = Новый Структура;
ПараметрыЗадания.Вставить("Состояние", СостояниеФоновогоЗадания.Активно);
// В ключе обычно указываем имя метода или уникальную строку
ПараметрыЗадания.Вставить("Ключ", "ИмпортДанныхИзСайта");
Если ФоновоеЗаданиеУжеЗапущено(ПараметрыЗадания) Тогда
ЗаписьЖурналаРегистрации("Регламентное задание",
УровеньЖурналаРегистрации.Предупреждение, , ,
"Попытка повторного запуска задания при активном первом экземпляре.");
Возврат;
КонецЕсли;
// Основная логика задания
// ...
КонецПроцедуры
Функция ФоновоеЗаданиеУжеЗапущено(ПараметрыЗадания)
// Получаем список заданий, соответствующих параметрам
МассивЗаданий = ФоновыеЗадания.ПолучитьФоновыеЗадания(ПараметрыЗадания);
// Если найдено более одного активного задания (одно из них — текущее),
// значит, другое уже выполняется.
Если МассивЗаданий.Количество() > 1 Тогда
Возврат Истина;
КонецЕсли;
Возврат Ложь;
КонецФункции
Этот метод кажется логичным, но у него есть два существенных недостатка. Во-первых, при огромном количестве выполненных заданий в истории базы метод ПолучитьФоновыеЗадания может работать медленно. Во-вторых, существует риск "состояния гонки" (Race Condition): если две копии задания стартуют в одну и ту же миллисекунду, обе функции ФоновоеЗаданиеУжеЗапущено могут вернуть Ложь, так как информация о новом задании еще не успела обновиться в реестре менеджера кластера.
Чтобы исключить влияние "состояния гонки" и особенностей работы кластера, мы рекомендуем использовать механизм управляемых блокировок данных. Этот способ гарантирует, что только один поток сможет войти в "критическую секцию" кода.
Разберем алгоритм по шагам:
СостоянияРегламентныхЗаданий), где в качестве измерения будет имя задания.Пример реализации:
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.СостоянияРегламентныхЗаданий");
ЭлементБлокировки.УстановитьЗначение("ИмяЗадания", "ЗагрузкаДанных");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
НачатьТранзакцию();
Попытка
Блокировка.Заблокировать();
// Если мы здесь, значит мы — единственный владелец блокировки.
// Выполняем полезную нагрузку...
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
// Блокировка не удалась, значит задание уже выполняется в другом сеансе
Возврат;
КонецПопытки;
Иногда разработчики используют обычную константу или флаг в регистре (например, булево Выполняется = Истина). Посмотрим на ситуацию критически: что произойдет, если в момент выполнения задания сервер "упадет" или процесс rphost будет принудительно завершен? Флаг останется в состоянии Истина навсегда, и задание больше никогда не запустится автоматически.
Чтобы этого избежать, проанализируем более сложную структуру флага. Вместо простого "да/нет" записывайте в регистр Дату и время начала, а также Номер сеанса (НомерСеанса). При старте нового задания проверяйте:
ПолучитьСеансыИнформационнойБазы()).Выясним причину, по которой нас так пугает одновременный запуск. Обычно это появление дублей данных. Проанализируем ситуацию: даже если мы запретим одновременный запуск, задание может упасть на середине, а при следующем запуске начать загружать те же данные снова.
Поэтому самый правильный подход — сделать код идемпотентным. Это означает, что повторный запуск с теми же входными данными не должен изменять состояние системы более одного раза. При загрузке документов всегда проверяйте, нет ли в базе документа с таким же идентификатором из внешней системы. Если он уже есть — обновляйте его, а не создавайте новый. В этом случае даже случайный одновременный запуск не приведет к порче данных в информационной базе.
Подведем итог: для простых задач достаточно использования параметра Ключ. Для высоконагруженных систем или критически важных процессов импорта лучше комбинировать программную проверку списка фоновых заданий с использованием управляемых блокировок на уровне базы данных.