Регламентное задание 1С обрабатывает не все записи и не выдаёт ошибок: проверка и исправление цикла
Иван Недомолков · Опубликовано:
Симптом выглядит так. Регламентное задание каждую ночь берёт записи с признаком “не обработано”, что-то с ними делает и снимает признак. Отрабатывает без ошибок, в журнале регистрации чисто, в логе стоит “обработано 5 000”. Утром в наборе опять висят тысячи записей, и новых среди них почти нет.
Если задание берёт записи порциями и сдвигает смещение на размер порции, почти наверняка дело в самом цикле. Ниже по порядку: как подтвердить это на своей базе, откуда берётся потеря, как переписать обход и что при этом сломается, если не подумать заранее.
Узнайте себя: признаки дефекта
Проверьте свой код по четырём вопросам. Если на все ответ “да”, вероятность высокая.
| Вопрос | Где смотреть |
|---|---|
| Набор задаётся отбором по признаку, который обработка меняет? | условие запроса или параметры вызова сервиса |
| Порции берутся со сдвигом: вторая страница, третья, смещение 100, 200? | счётчик в цикле |
| Набор пересобирается на каждой порции, а не выбирается один раз на старте? | где стоит вызов запроса или сервиса: внутри цикла или до него |
| Выход из цикла по пустой порции? | условие Прервать |
Оговорка про смещение в самой 1С. В языке запросов есть ПЕРВЫЕ, а OFFSET нет, поэтому в чистом запросе такой цикл написать трудно. Смещение появляется, когда порции отдаёт кто-то ещё: внешний сервис со страницами, свой HTTP-метод, прослойка над выборкой. Если вы один раз выбрали ключи в таблицу значений и идёте по ней, набор неподвижен, и описанного дефекта нет.
Холостой прогон: проверка за минуту вместо ночи
Ждать следующей ночи не нужно. Запустите обход своим же кодом, но вместо полной обработки только снимайте признак и собирайте ключи. Снимать признак обязательно: без этого набор не сжимается, прогон пройдёт по всем записям, и дефект не проявится. Поэтому запускайте на копии базы или на тестовом наборе.
// Холостой прогон на копии базы: считаем набор до, обходим, считаем после
Процедура ПроверитьОбход() Экспорт
До = КоличествоНеобработанных();
Собрано = Новый Соответствие;
Смещение = 0;
Размер = 100;
Пока Истина Цикл
Порция = ПолучитьНеобработанные(Смещение, Размер);
Если Порция.Количество() = 0 Тогда
Прервать;
КонецЕсли;
Для Каждого Запись Из Порция Цикл
Собрано.Вставить(Запись.Ссылка, Истина);
СнятьПризнак(Запись); // то же действие, что делает задание
КонецЦикла;
Смещение = Смещение + Размер;
КонецЦикла;
После = КоличествоНеобработанных();
Сообщить("До: " + До + ", обработано: " + Собрано.Количество()
+ ", осталось: " + После);
КонецПроцедуры
Как читать результат:
- осталось ноль: обход честный, ищите причину в другом месте;
- обработано плюс осталось равно “до”, а осталось около половины: это он, сжимающийся набор под сдвигающимся смещением;
- обработано плюс осталось больше “до”: набор рос во время прогона, смотрите раздел про дубли ниже.
Если ошибка в цикле, сверка обработанного с отправленным сойдётся идеально. Она проверяет, что каждая взятая запись обработана, а взяли не все. Ловит дефект только остаток.
Откуда берётся потеря
Набор пересобирается на каждой порции, и каждая порция вынимает из него обработанные записи. Смещение при этом растёт. Получается, что набор уезжает вперёд навстречу смещению, и смещение перешагивает записи, которые сдвинулись на освободившиеся места.
Пошагово на двадцати записях и порции в пять:
| Шаг | В наборе до шага | Смещение | Взяли | Перешагнули |
|---|---|---|---|---|
| 1 | 1-20 | 0 | 1-5 | - |
| 2 | 6-20 | 5 | 11-15 | 6-10 |
| 3 | 6-10, 16-20 | 10 | ничего | 6-10, 16-20 |
На третьем шаге в наборе десять записей, смещение тоже десять, порция пустая, и цикл честно выходит. Выход по пустой порции здесь означает “смещение догнало конец набора”, а вовсе не “набор кончился”. Обработано десять из двадцати.
Сколько теряется при разных размерах
Доля считается моделью ниже, её можно прогнать самому.
| Записей в наборе | Порция | Обработано |
|---|---|---|
| 10 000 | 100 | 50,0 % |
| 1 000 | 10 | 50,0 % |
| 1 000 | 100 | 50,0 % |
| 1 000 | 150 | 55,0 % |
| 1 000 | 200 | 60,0 % |
| 1 000 | 333 | 66,6 % |
Практический вывод из таблицы: у ночного задания порция почти всегда в десятки и сотни раз меньше набора, и тогда теряется ровно половина. На наборах от ста до пяти тысяч записей при любом размере порции модель ни разу не дала меньше пятидесяти процентов. Ближе к краю, когда порция сравнима с набором, доля растёт, потому что смещение выбегает за конец раньше.
Модель на встроенном языке, подставьте свои размеры:
Функция ОбработаноЗаПрогон(Всего, Страница)
Набор = Новый Массив;
Для Н = 1 По Всего Цикл
Набор.Добавить(Н);
КонецЦикла;
Смещение = 0;
Пока Смещение < Набор.Количество() Цикл
Конец = Мин(Смещение + Страница, Набор.Количество());
Если Конец <= Смещение Тогда
Прервать;
КонецЕсли;
// обработанные уходят из набора; удаляем с конца, чтобы индексы не съехали
Инд = Конец - 1;
Пока Инд >= Смещение Цикл
Набор.Удалить(Инд);
Инд = Инд - 1;
КонецЦикла;
Смещение = Смещение + Страница;
КонецЦикла;
Возврат Всего - Набор.Количество();
КонецФункции
Для набора 20 и порции 4 функция вернёт 12, то есть шестьдесят процентов: пример того, что половина это нижняя граница, а не константа.
Как переписать цикл
Есть три варианта, и выбирать надо по тому, можете ли вы менять код.
| Вариант | Когда подходит | Цена |
|---|---|---|
| Увеличить порцию | никогда как исправление | маскирует дефект до следующего роста набора |
| Повторять прогон, пока остаток не станет нулём | код чужой, править нельзя | лишние прогоны: примерно 4 для 1 000 записей при порции 100 и 7 для 10 000 |
| Всегда брать порцию с начала набора | код ваш | нужна защита от записей, которые не обрабатываются |
Правильный вариант третий: смещения нет вообще.
Размер = 100;
Пока Истина Цикл
// обработанные уже выпали из отбора, поэтому начало набора всегда свежее
Порция = ПолучитьНеобработанные(0, Размер);
Если Порция.Количество() = 0 Тогда
Прервать;
КонецЕсли;
Для Каждого Запись Из Порция Цикл
Обработать(Запись);
КонецЦикла;
КонецЦикла;
Пустая порция теперь значит ровно то, что написано: в наборе ничего не осталось.
Что сломается, если на этом остановиться. Запись, которая не смогла обработаться (сервис ответил отказом, данные битые), признак не теряет и приходит в следующей порции снова. Цикл становится вечным, и одна плохая запись держит задание до таймаута. Нужен учёт попыток: регистр сведений с ключом записи и числом попыток, отбор “попыток меньше трёх” в том же запросе и отдельный отчёт по тем, кто уткнулся в лимит. Страховка попроще: предел итераций цикла с записью в журнал, если он сработал.
В старом варианте со смещением эта беда была не видна как раз потому, что плохая запись оставалась позади вместе с половиной хороших.
Сортировка в порции обязательна
Порция в запросе обычно выглядит так:
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 100
| Ном.Ссылка КАК Ссылка
|ИЗ
| Справочник.Номенклатура КАК Ном
|ГДЕ
| НЕ Ном.ДанныеПолучены
|
|УПОРЯДОЧИТЬ ПО
| Ссылка";
Без УПОРЯДОЧИТЬ ПО запрос законен, но порядок строк тогда выбирает СУБД, и между вызовами он может отличаться. Мы видели такое на 8.3.27 с MS SQL, причину на своей связке не разбирали. Для обхода со смещением это означает, что пропуск становится случайным: сегодня пропущена одна запись, завтра другая, и дефект перестаёт воспроизводиться. Сортируйте по полю, которое не меняется при обработке. Ссылка годится, дата изменения и статус нет.
Обратный случай: набор растёт, и уходят дубли
Бывает зеркально: пока задание идёт по очереди, в неё поступают новые записи. Смещение тогда отстаёт от содержимого, следующая страница частично повторяет предыдущую, и часть записей отправляется дважды. Ошибок нет и здесь, получатель может принять оба раза.
Этот вариант замечают раньше, потому что дубль виден на той стороне и возвращается претензией. Лечение то же: убрать смещение и брать с начала. Плюс ключ идемпотентности в отправке, по которому получатель узнаёт повтор. Как повтор запроса к внешнему сервису превращается в дубль и какие операции можно повторять без опаски, разобрано в статье про таймаут обмена 1С с внешним сервисом.
Правило для обоих случаев одно: смещение работает только на наборе, который не меняется между страницами.
Что добавить в лог задания
Одна строка решает проблему на будущее. Рядом с “обработано” пишите “осталось по отбору после прогона”. У задания, которое должно опустошать набор, второе число хотя бы иногда обязано быть нулём. Если за неделю оно ни разу не обнулилось при небольшом притоке, у вас либо этот дефект, либо задание не успевает за потоком, и оба ответа стоит знать.
Если задание не стартует вовсе, это другая история: порядок проверки собран в статье про регламентное задание, которое не выполняется. А если ночная загрузка отработала, но привезла ноль строк, смотрите разбор загрузки, упавшей молча.
Чем искать такие циклы в своей базе
- Анализ кода внешних обработок пройдётся по обработкам, которые давно никто не открывал, и покажет, что они делают, включая обходы с внешними вызовами.
- Журнал регистрации для нейросети сворачивает журнал так, чтобы было видно, по каким записям задание на самом деле прошло.
- Сводная таблица в Excel из 1С удобна, чтобы выгрузить остаток набора и показать его владельцу процесса.
Если цикл сидит в обмене, который писали не вы, и править его страшно, это работа для доработки обменов 1С: найдём обход, перепишем с учётом попыток и добавим остаток в лог.