framework/skills/bsl-practices/background-jobs/SKILL.md
Use for проектирования, диагностики и исправления фоновых и регламентных заданий 1С. Helps обеспечить идемпотентность, retry-политику, checkpointing, mutex и разделение retryable/permanent ошибок.
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 через БлокировкаДанных или константу-флаг |
Контекст: Нужно создать регламентное задание, которое безопасно перезапускается и не дублирует работу.
Шаги:
// Канонический паттерн идемпотентного захвата задачи
Функция ЗахватитьЗадачуДляОбработки(ЗадачаСсылка) Экспорт
НачатьТранзакцию();
Попытка
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.СостоянияЗадач");
ЭлементБлокировки.УстановитьЗначение("Задача", ЗадачаСсылка);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
// Читаем актуальный статус после блокировки
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СостоянияЗадач.Статус КАК Статус,
| СостоянияЗадач.НачалоОбработки КАК НачалоОбработки
|ИЗ
| РегистрСведений.СостоянияЗадач КАК СостоянияЗадач
|ГДЕ
| СостоянияЗадач.Задача = &Задача";
Запрос.УстановитьПараметр("Задача", ЗадачаСсылка);
Результат = Запрос.Выполнить();
Если НЕ Результат.Пустой() Тогда
Выборка = Результат.Выбрать();
Выборка.Следующий();
// Уже обработано — пропускаем
Если Выборка.Статус = Перечисления.СтатусыЗадач.Обработано Тогда
ОтменитьТранзакцию();
Возврат Ложь;
КонецЕсли;
// Кто-то уже взял задачу (и не завис) — пропускаем
Если Выборка.Статус = Перечисления.СтатусыЗадач.ВОбработке
И (ТекущаяДата() - Выборка.НачалоОбработки) < 3600 Тогда
ОтменитьТранзакцию();
Возврат Ложь;
КонецЕсли;
КонецЕсли;
// Захватываем задачу
НаборЗаписей = РегистрыСведений.СостоянияЗадач.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Задача.Установить(ЗадачаСсылка);
Запись = НаборЗаписей.Добавить();
Запись.Задача = ЗадачаСсылка;
Запись.Статус = Перечисления.СтатусыЗадач.ВОбработке;
Запись.НачалоОбработки = ТекущаяДата();
НаборЗаписей.Записать();
ЗафиксироватьТранзакцию();
Возврат Истина;
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации(
НСтр("ru = 'ФоновоеЗадание.ЗахватЗадачи'"),
УровеньЖурналаРегистрации.Ошибка,,,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение;
КонецПопытки;
КонецФункции
Контекст: Регламентное задание не должно запускаться в двух экземплярах одновременно.
Шаги:
Процедура ВыполнитьРегламентноеЗадание() Экспорт
// Попытка получить эксклюзивный лок
НачатьТранзакцию();
Попытка
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Константа.ФлагЗапускаЗадания");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Попытка
Блокировка.Заблокировать();
Исключение
// Другой экземпляр уже работает — нормальная ситуация
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации(
НСтр("ru = 'РегламентноеЗадание.ИмяЗадания'"),
УровеньЖурналаРегистрации.Предупреждение,,,
НСтр("ru = 'Пропущен запуск: задание уже выполняется.'"));
Возврат;
КонецПопытки;
// Основная логика задания — выполняется только в одном экземпляре
ВыполнитьОсновнуюЛогику();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации(
НСтр("ru = 'РегламентноеЗадание.ИмяЗадания'"),
УровеньЖурналаРегистрации.Ошибка,,,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры
Контекст: Задание обрабатывает тысячи объектов. Нужно сохранять прогресс, чтобы при перезапуске не начинать с нуля.
Ключевые правила:
Процедура ОбработатьОбъектыСCheckpoint(РазмерБатча = 100) Экспорт
// Читаем checkpoint (откуда продолжать)
НачальнаяПозиция = ПолучитьCheckpoint();
МассивОбъектов = ПолучитьОбъектыДляОбработки(НачальнаяПозиция, РазмерБатча);
Пока МассивОбъектов.Количество() > 0 Цикл
НачатьТранзакцию();
Попытка
Для Каждого Объект Из МассивОбъектов Цикл
ОбработатьОдинОбъект(Объект);
КонецЦикла;
// Checkpoint и данные фиксируются атомарно
СохранитьCheckpoint(МассивОбъектов[МассивОбъектов.ВГраница()]);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации(
НСтр("ru = 'ФоновоеЗадание.ПакетнаяОбработка'"),
УровеньЖурналаРегистрации.Ошибка,,,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение;
КонецПопытки;
// Следующий батч
НачальнаяПозиция = Получить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:
testing
MUST use BEFORE making a judgment about the cause of a conflict, a test failure, or an artifact dispute. Defines the end-to-end verification method L1→L6 and the classification of the first broken link.
development
MUST use AFTER a work cycle with ≥2 iterations (wrote → error → fixed → success). Provides the retrospective procedure and the format for recording practice/anti-patterns in references/learned-patterns.md or {project}/.context/learned-patterns.md.
tools
MUST use WHEN you are writing reusable knowledge into RLM (pattern / architectural decision / stable domain fact) OR reading it before a non-trivial task/solution in the domain. Provides the breakdown of native-push vs RLM-pull, tools for writing and reading RLM, H-MEM levels, and hygiene.
testing
MUST use WHEN the task is classified as simple (< 20 lines, 1 file, no new metadata objects, no architectural decisions). Provides a short cycle of 3 steps with a guard on the self path and mandatory verify.