Skill v1.0.1
LLM-judged scan95/100+1 new
version: "1.0.1" name: background-jobs description: "Use for проектирования, диагностики и исправления фоновых и регламентных заданий 1С. Helps обеспечить идемпотентность, retry-политику, checkpointing, mutex и разделение retryable/permanent ошибок." skills:
- architect
- developer-code
Фоновые и регламентные задания
Ключевой принцип: Фоновое задание может быть прервано, перезапущено или запущено повторно в любой момент. Код задания обязан выдерживать это без потери данных и дублирования работы.
Когда применять
| Триггер | Действие | |
|---|---|---|
| Проектируется регламентное или фоновое задание | Определить контракт: параметры, пользователь, транзакция, идемпотентность, блокировка, тайм-аут | |
| Задание зависает, не завершается, дублирует работу | Диагностировать по ЖР: найти первый сбой, проверить активные фоновые задания и стейл-локи | |
| Ошибка в задании — нужна retry-логика | Разделить retryable и permanent ошибки, реализовать backoff | |
| Задание обрабатывает большой объём данных | Применить checkpointing и батч-обработку с промежуточными фиксациями | |
| Возможен параллельный запуск нескольких экземпляров | Реализовать mutex через БлокировкаДанных или константу-флаг |
Сценарий 1: Проектирование идемпотентного задания
Контекст: Нужно создать регламентное задание, которое безопасно перезапускается и не дублирует работу.
Шаги:
- Определить идемпотентный ключ: что уникально идентифицирует единицу работы (документ, период, хэш параметров).
- Хранить статус обработки в информационной базе (справочник, регистр сведений, реквизит объекта).
- Читать статус внутри транзакции с блокировкой перед началом работы.
- Обновлять статус на «В обработке» с меткой времени старта — защита от параллельного захвата.
- По завершении выставлять «Обработано».
// Канонический паттерн идемпотентного захвата задачиФункция ЗахватитьЗадачуДляОбработки(ЗадачаСсылка) ЭкспортНачатьТранзакцию();ПопыткаБлокировка = Новый БлокировкаДанных;ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.СостоянияЗадач");ЭлементБлокировки.УстановитьЗначение("Задача", ЗадачаСсылка);ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;Блокировка.Заблокировать();// Читаем актуальный статус после блокировкиЗапрос = Новый Запрос;Запрос.Текст ="ВЫБРАТЬ| СостоянияЗадач.Статус КАК Статус,| СостоянияЗадач.НачалоОбработки КАК НачалоОбработки|ИЗ| РегистрСведений.СостоянияЗадач КАК СостоянияЗадач|ГДЕ| СостоянияЗадач.Задача = &Задача";Запрос.УстановитьПараметр("Задача", ЗадачаСсылка);Результат = Запрос.Выполнить();Если НЕ Результат.Пустой() ТогдаВыборка = Результат.Выбрать();Выборка.Следующий();// Уже обработано — пропускаемЕсли Выборка.Статус = Перечисления.СтатусыЗадач.Обработано ТогдаОтменитьТранзакцию();Возврат Ложь;КонецЕсли;// Кто-то уже взял задачу (и не завис) — пропускаемЕсли Выборка.Статус = Перечисления.СтатусыЗадач.ВОбработкеИ (ТекущаяДата() - Выборка.НачалоОбработки) < 3600 ТогдаОтменитьТранзакцию();Возврат Ложь;КонецЕсли;КонецЕсли;// Захватываем задачуНаборЗаписей = РегистрыСведений.СостоянияЗадач.СоздатьНаборЗаписей();НаборЗаписей.Отбор.Задача.Установить(ЗадачаСсылка);Запись = НаборЗаписей.Добавить();Запись.Задача = ЗадачаСсылка;Запись.Статус = Перечисления.СтатусыЗадач.ВОбработке;Запись.НачалоОбработки = ТекущаяДата();НаборЗаписей.Записать();ЗафиксироватьТранзакцию();Возврат Истина;ИсключениеОтменитьТранзакцию();ЗаписьЖурналаРегистрации(НСтр("ru = 'ФоновоеЗадание.ЗахватЗадачи'"),УровеньЖурналаРегистрации.Ошибка,,,ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));ВызватьИсключение;КонецПопытки;КонецФункции
Сценарий 2: Защита от параллельного запуска (mutex)
Контекст: Регламентное задание не должно запускаться в двух экземплярах одновременно.
Шаги:
- В начале задания установить исключительную блокировку на специальный ключ (константа или запись регистра).
- Если блокировка не получена — завершить с предупреждением в ЖР (не ошибкой).
- Снять блокировку автоматически при завершении транзакции.
Процедура ВыполнитьРегламентноеЗадание() Экспорт// Попытка получить эксклюзивный локНачатьТранзакцию();ПопыткаБлокировка = Новый БлокировкаДанных;ЭлементБлокировки = Блокировка.Добавить("Константа.ФлагЗапускаЗадания");ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;ПопыткаБлокировка.Заблокировать();Исключение// Другой экземпляр уже работает — нормальная ситуацияОтменитьТранзакцию();ЗаписьЖурналаРегистрации(НСтр("ru = 'РегламентноеЗадание.ИмяЗадания'"),УровеньЖурналаРегистрации.Предупреждение,,,НСтр("ru = 'Пропущен запуск: задание уже выполняется.'"));Возврат;КонецПопытки;// Основная логика задания — выполняется только в одном экземпляреВыполнитьОсновнуюЛогику();ЗафиксироватьТранзакцию();ИсключениеОтменитьТранзакцию();ЗаписьЖурналаРегистрации(НСтр("ru = 'РегламентноеЗадание.ИмяЗадания'"),УровеньЖурналаРегистрации.Ошибка,,,ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));ВызватьИсключение;КонецПопытки;КонецПроцедуры
Сценарий 3: Checkpointing при обработке большого объёма
Контекст: Задание обрабатывает тысячи объектов. Нужно сохранять прогресс, чтобы при перезапуске не начинать с нуля.
Ключевые правила:
- Батч = одна транзакция. Не открывать транзакцию на весь объём.
- Сохранять checkpoint в той же транзакции, что и полезная работа батча.
- При перезапуске читать checkpoint и стартовать с него.
Процедура ОбработатьОбъектыСCheckpoint(РазмерБатча = 100) Экспорт// Читаем checkpoint (откуда продолжать)НачальнаяПозиция = ПолучитьCheckpoint();МассивОбъектов = ПолучитьОбъектыДляОбработки(НачальнаяПозиция, РазмерБатча);Пока МассивОбъектов.Количество() > 0 ЦиклНачатьТранзакцию();ПопыткаДля Каждого Объект Из МассивОбъектов ЦиклОбработатьОдинОбъект(Объект);КонецЦикла;// Checkpoint и данные фиксируются атомарноСохранитьCheckpoint(МассивОбъектов[МассивОбъектов.ВГраница()]);ЗафиксироватьТранзакцию();ИсключениеОтменитьТранзакцию();ЗаписьЖурналаРегистрации(НСтр("ru = 'ФоновоеЗадание.ПакетнаяОбработка'"),УровеньЖурналаРегистрации.Ошибка,,,ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));ВызватьИсключение;КонецПопытки;// Следующий батчНачальнаяПозиция = ПолучитьCheckpoint();МассивОбъектов = ПолучитьОбъектыДляОбработки(НачальнаяПозиция, РазмерБатча);КонецЦикла;КонецПроцедуры
Сценарий 4: Retry-политика — retryable vs permanent ошибки
Контекст: Задание вызывает внешний сервис или работает с ресурсами, которые могут быть временно недоступны.
Классификация ошибок:
| Тип | Примеры | Действие | |
|---|---|---|---|
| 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;КонецФункции
Сценарий 5: Запуск и диагностика через v8-runner
Запуск задания вручную (для отладки и тестирования):
# Запустить конкретное регламентное задание через v8-runnerv8 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 сек'"),КоличествоОбработано, КоличествоОшибок, Длительность));
Контрольный список (review checklist)
- [ ] Задание может быть безопасно перезапущено без дублирования работы (идемпотентно).
- [ ] Длинная работа разбита на батчи с промежуточными checkpoint и фиксациями транзакций.
- [ ] Лок защищает от параллельного запуска; лок имеет TTL (защита от зависания).
- [ ] ЖР содержит: старт, финиш, параметры задания, итог (сколько обработано/ошибок), длительность.
- [ ] В логах нет секретов (паролей, токенов, персональных данных).
- [ ] Ошибки разделены на retryable (повторять) и permanent (не повторять).
- [ ] Внешние вызовы (HTTP, COM, WS) выполняются за пределами транзакции.
- [ ] Транзакция минимальна по времени: подготовка данных — вне транзакции, только запись — внутри.
Связанные ресурсы
- error-handling — транзакции,
БлокировкаДанных, канонический паттернНачатьТранзакцию/ЗафиксироватьТранзакцию/ОтменитьТранзакцию - v8-runner references/testing — запуск заданий вручную и проверка результатов
- vanessa-diagnostics — диагностика по логам и ЖР
depends_on:
- bsl-practices/error-handling