framework/skills/bsl-practices/background-jobs/SKILL.md
Для проектирования и отладки фоновых заданий 1С
npx skillsauth add steelmorgan/1c-agent-based-dev-framework background-jobsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Ключевой принцип: Фоновое задание может быть прервано, перезапущено или запущено повторно в любой момент. Код задания обязан выдерживать это без потери данных и дублирования работы.
| Триггер | Действие |
|---------|----------|
| Проектируется регламентное или фоновое задание | Определить контракт: параметры, пользователь, транзакция, идемпотентность, блокировка, тайм-аут |
| Задание зависает, не завершается, дублирует работу | Диагностировать по ЖР: найти первый сбой, проверить активные фоновые задания и стейл-локи |
| Ошибка в задании — нужна retry-логика | Разделить retryable и permanent ошибки, реализовать backoff |
| Задание обрабатывает большой объём данных | Применить checkpointing и батч-обработку с промежуточными фиксациями |
| Возможен параллельный запуск нескольких экземпляров | Реализовать mutex через БлокировкаДанных или константу-флаг |
Все примеры транзакций ниже — это канонический паттерн
error-handling(Правило 2:НачатьТранзакцию()передПопытка,ЗафиксироватьТранзакцию()последней операцией,ОтменитьТранзакцию()первой вИсключение, лог — после отката) с контекстной спецификой задания внутри него; сам паттерн здесь не переизлагается.
Контекст: Нужно создать регламентное задание, которое безопасно перезапускается и не дублирует работу.
Шаги:
// Канонический паттерн идемпотентного захвата задачи
Функция ЗахватитьЗадачуДляОбработки(ЗадачаСсылка) Экспорт
НачатьТранзакцию();
Попытка
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.СостоянияЗадач");
ЭлементБлокировки.УстановитьЗначение("Задача", ЗадачаСсылка);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
// Читаем актуальный статус после блокировки
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СостоянияЗадач.Статус КАК Статус,
| СостоянияЗадач.НачалоОбработки КАК НачалоОбработки
|ИЗ
| РегистрСведений.СостоянияЗадач КАК СостоянияЗадач
|ГДЕ
| СостоянияЗадач.Задача = &Задача";
Запрос.УстановитьПараметр("Задача", ЗадачаСсылка);
Результат = Запрос.Выполнить();
Если НЕ Результат.Пустой() Тогда
Выборка = Результат.Выбрать();
Выборка.Следующий();
// Уже обработано — пропускаем
Если Выборка.Статус = Перечисления.СтатусыЗадач.Обработано Тогда
ОтменитьТранзакцию();
Возврат Ложь;
КонецЕсли;
// Кто-то уже взял задачу (и не завис) — пропускаем
Если Выборка.Статус = Перечисления.СтатусыЗадач.ВОбработке
И (ТекущаяДата() - Выборка.НачалоОбработки) < 3600 Тогда
ОтменитьТранзакцию();
Возврат Ложь;
КонецЕсли;
КонецЕсли;
// Захватываем задачу
НаборЗаписей = РегистрыСведений.СостоянияЗадач.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Задача.Установить(ЗадачаСсылка);
Запись = НаборЗаписей.Добавить();
Запись.Задача = ЗадачаСсылка;
Запись.Статус = Перечисления.СтатусыЗадач.ВОбработке;
Запись.НачалоОбработки = ТекущаяДата();
НаборЗаписей.Записать();
ЗафиксироватьТранзакцию();
Возврат Истина;
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации(
НСтр("ru = 'ФоновоеЗадание.ЗахватЗадачи'"),
УровеньЖурналаРегистрации.Ошибка,,,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение;
КонецПопытки;
КонецФункции
Контекст: Регламентное задание не должно запускаться в двух экземплярах одновременно.
Шаги:
Процедура ВыполнитьРегламентноеЗадание() Экспорт
// Попытка получить эксклюзивный лок
НачатьТранзакцию();
Попытка
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Константа.ФлагЗапускаЗадания");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Попытка
Блокировка.Заблокировать();
Исключение
// Другой экземпляр уже работает — нормальная ситуация
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации(
НСтр("ru = 'РегламентноеЗадание.ИмяЗадания'"),
УровеньЖурналаРегистрации.Предупреждение,,,
НСтр("ru = 'Пропущен запуск: задание уже выполняется.'"));
Возврат;
КонецПопытки;
// Основная логика задания — выполняется только в одном экземпляре
ВыполнитьОсновнуюЛогику();
ЗафиксироватьТранзакцию();
Исключение
// канонический Исключение-блок (см. сценарий 1 / error-handling, Правило 2),
// имя события ЖР: "РегламентноеЗадание.ИмяЗадания"
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации(...);
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры
Контекст: Задание обрабатывает тысячи объектов. Нужно сохранять прогресс, чтобы при перезапуске не начинать с нуля.
Ключевые правила:
Процедура ОбработатьОбъектыСCheckpoint(РазмерБатча = 100) Экспорт
// Читаем checkpoint (откуда продолжать)
НачальнаяПозиция = ПолучитьCheckpoint();
МассивОбъектов = ПолучитьОбъектыДляОбработки(НачальнаяПозиция, РазмерБатча);
Пока МассивОбъектов.Количество() > 0 Цикл
НачатьТранзакцию();
Попытка
Для Каждого Объект Из МассивОбъектов Цикл
ОбработатьОдинОбъект(Объект);
КонецЦикла;
// Checkpoint и данные фиксируются атомарно
СохранитьCheckpoint(МассивОбъектов[МассивОбъектов.ВГраница()]);
ЗафиксироватьТранзакцию();
Исключение
// канонический Исключение-блок (см. сценарий 1 / error-handling, Правило 2),
// имя события ЖР: "ФоновоеЗадание.ПакетнаяОбработка"
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации(...);
ВызватьИсключение;
КонецПопытки;
// Следующий батч
НачальнаяПозиция = ПолучитьCheckpoint();
МассивОбъектов = ПолучитьОбъектыДляОбработки(НачальнаяПозиция, РазмерБатча);
КонецЦикла;
КонецПроцедуры
Контекст: Задание вызывает внешний сервис или работает с ресурсами, которые могут быть временно недоступны.
Классификация ошибок:
| Тип | Примеры | Действие |
|-----|---------|----------|
| Retryable (временные) | Таймаут сети, сервис недоступен (503), захват блокировки | Повторить с backoff, записать Предупреждение |
| Permanent (постоянные) | Неверные данные, бизнес-правило нарушено, 404/400 | Не повторять, записать Ошибка, перевести задачу в статус Отклонено |
Функция ВыполнитьСRetry(ПараметрыЗадачи) Экспорт
МаксПопыток = 3;
ЗадержкаСекунд = 30; // для ФоновогоЗадания — через повторный запуск планировщиком
Для НомерПопытки = 1 По МаксПопыток Цикл
Попытка
Результат = ВызватьВнешнийСервис(ПараметрыЗадачи);
ЗафиксироватьУспех(ПараметрыЗадачи, Результат);
Возврат Истина;
Исключение
ИнфОшибки = ИнформацияОбОшибке();
Если ЭтоPermanentОшибка(ИнфОшибки) Тогда
// Повтор бессмысленен
ЗаписьЖурналаРегистрации(
НСтр("ru = 'ФоновоеЗадание.ВнешнийСервис'"),
УровеньЖурналаРегистрации.Ошибка,,,
СтрШаблон(НСтр("ru = 'Постоянная ошибка (повтор не поможет). %1'"),
ПодробноеПредставлениеОшибки(ИнфОшибки)));
ЗафиксироватьОтклонение(ПараметрыЗадачи, КраткоеПредставлениеОшибки(ИнфОшибки));
Возврат Ложь;
КонецЕсли;
// Retryable — логируем как предупреждение и продолжаем
ЗаписьЖурналаРегистрации(
НСтр("ru = 'ФоновоеЗадание.ВнешнийСервис'"),
?(НомерПопытки < МаксПопыток,
УровеньЖурналаРегистрации.Предупреждение,
УровеньЖурналаРегистрации.Ошибка),,,
СтрШаблон(НСтр("ru = 'Попытка %1/%2. %3'"),
НомерПопытки, МаксПопыток,
ПодробноеПредставлениеОшибки(ИнфОшибки)));
Если НомерПопытки = МаксПопыток Тогда
ВызватьИсключение;
КонецЕсли;
КонецПопытки;
КонецЦикла;
Возврат Ложь;
КонецФункции
Функция ЭтоPermanentОшибка(ИнфОшибки)
ТекстОшибки = КраткоеПредставлениеОшибки(ИнфОшибки);
// Признак permanent: HTTP 4xx, бизнес-ошибки, невалидные данные
Возврат СтрНайти(ТекстОшибки, "400") > 0
ИЛИ СтрНайти(ТекстОшибки, "404") > 0
ИЛИ СтрНайти(ТекстОшибки, "422") > 0;
КонецФункции
Запуск задания вручную (для отладки и тестирования):
# Запустить конкретное регламентное задание через v8-runner
v8 run --ib <путь_к_ИБ> --execute "РегламентныеЗаданияСервер.ВыполнитьЗадание(<ИмяЗадания>)"
Диагностика зависших заданий через ЖР (event-log-analysis):
# Посмотреть ошибки фоновых заданий за последние 2 часа
v8 run --ib <путь_к_ИБ> --event-log --filter "ФоновоеЗадание" --level Error --hours 2
Проверка активных фоновых заданий в ЖР:
Искать события с именем Фоновое задание. Зависшее задание = событие «Запуск» без парного «Завершение» и без «Ошибка» — это кандидат на стейл-лок.
| Антипаттерн | Последствие | |-------------|-------------| | HTTP/внешний вызов внутри транзакции | Таймаут 30 сек = блокировка 30 сек на всю ИБ | | Одна транзакция на весь объём | Перезапуск = откат всей работы | | Отсутствие идемпотентности | Дублирование данных при повторном запуске | | Молчаливое поглощение ошибок | Данные потеряны, в ЖР нет следов | | Бесконечный retry без ограничения | Задание заблокирует очередь навсегда | | Стейл-лок без TTL | Задание не запускается после краша, лок не снят |
Каждое задание при старте и завершении должно писать в ЖР:
// Старт задания
ЗаписьЖурналаРегистрации(
НСтр("ru = 'РегламентноеЗадание.<ИмяЗадания>.Старт'"),
УровеньЖурналаРегистрации.Информация,,,
СтрШаблон(НСтр("ru = 'Задание запущено. Параметры: %1'"),
<КраткоеОписаниеПараметров>));
// Финиш задания
ЗаписьЖурналаРегистрации(
НСтр("ru = 'РегламентноеЗадание.<ИмяЗадания>.Финиш'"),
УровеньЖурналаРегистрации.Информация,,,
СтрШаблон(НСтр("ru = 'Задание завершено. Обработано: %1, Ошибок: %2, Время: %3 сек'"),
КоличествоОбработано, КоличествоОшибок, Длительность));
БлокировкаДанных, канонический паттерн НачатьТранзакцию/ЗафиксироватьТранзакцию/ОтменитьТранзакциюdepends_on:
development
1C server maintenance webhooks: container restart and external component cache cleanup
development
Interactive DAP debugging of a single BSL procedure
tools
Rules for using RLM tools for project search and navigation in 1C/BSL
development
Creates web applications and routes on Winow (a web server on OneScript and Autumn). Use when working with a web server on OneScript, routing, or Winow controllers.