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