Задача выглядела на день. Изменилась ставка налога: надо пройти по номенклатуре в трёх базах крупной розничной сети, заменить старое значение на новое и заодно вычистить места, где старое значение зашито в коде.
Прошли, заменили, вычистили. Через шесть дней в одной из трёх баз 1 122 карточки снова оказались на старой ставке. В двух других ноль.
Дальше разбор: что было в данных, где сидели четыре разных хардкода одной и той же ставки, почему правка правил обмена прямо в конфигурации не даёт ровным счётом ничего, и чем закончилась история с вернувшимися карточками. Про её конец скажу сразу, чтобы никто не ждал красивой развязки: причину возврата мы не доказали. Так и записано в журнале задачи.
Страну и обе ставки не называю. По паре "страна плюс ставка" заказчик вычисляется с первой попытки, а на механику это не влияет никак. Дальше просто "старая" и "новая".
Что было в данных на старте
Первый прогон только на чтение дал такую картину по карточкам номенклатуры (без групп):
| База | На новой ставке | На старой, чинить | Без налога |
|---|---|---|---|
| Учётная | 132 202 | 21 534 | 3 |
| Розничная первая | 132 792 | 4 839 | 3 |
| Розничная вторая | 135 564 | 17 079 | 3 |
Отдельно проверили справочник видов номенклатуры, потому что именно оттуда подставляется ставка по умолчанию для новых товаров. Там нашлось по три вида на старой ставке в каждой базе плюс по три-четыре с пустым значением.
Ещё одна строка отчёта важнее, чем кажется: никаких других ставок у карточек не было вообще. Ни промежуточных, ни расчётных, ни пустых. Это редкая удача: финальная проверка пишется одним условием, всё отличное от новой ставки считается отклонением, перечислять допустимые варианты руками не приходится.
Три позиции "без налога" одинаковы во всех трёх базах, это служебные карточки, и решение по ним висело на менеджере заказчика. Их не трогали ни разу. Скрипт правки их не задевал по построению: он переписывал только семейство старых ставок. Всё прочее, что отличалось от новой, он проходил мимо.
Четыре хардкода одной ставки в трёх разных слоях
Дальше начали искать, откуда в живой системе вообще берётся ставка помимо справочника. Нашли четыре места, и они лежат в совершенно разных слоях.
Первое, главное. Карта соответствия ставок внутри модуля веб-сервиса (HTTP-сервис в дереве метаданных), который принимает документы из розницы в учётную базу. Ключи карты - строки вида "NN%", как их отдаёт преобразование значения перечисления в строку. Ключа для новой ставки в карте не было вовсе: входящее значение не находилось, реквизит не заполнялся, сервис ругался на неизвестную ставку. Плюс подарок сверху: одному из старых ключей был сопоставлен вообще посторонний элемент перечисления. Про новую ставку карта не знала. Про старую врала.
Второе. Обработчик "ПослеЗагрузки" в правиле "Виды подарочных сертификатов -> Номенклатура": ставка присваивалась жёстко, значением, которого в этой юрисдикции нет совсем. Оно приехало вместе с исходной поставкой правил и с тех пор никого не беспокоило, потому что подарочные сертификаты создают редко.
Третье. Числовой расчёт ставки в правиле выгрузки документа перемещения товаров. Внутри запроса стоит конструкция ВЫБОР с ветками на старые ставки и внутренним ИНАЧЕ 0. Ветки на новую ставку там нет. Значит у товара на новой ставке числовая ставка уезжает нулём, а вместе с ней и сумма налога в выгруженном документе. Это худший тип бага из всех: он ничего не роняет, он тихо считает ноль, и заметить его можно только через месяц по отчётам.
Четвёртое. Общий модуль интеграции с внешним поставщиком чеков, функция заполнения табличной части товаров. Ставка строки присвоена константой старого значения. Тут страдают строки документов, карточки номенклатуры не затрагиваются вовсе. Этот хардкод нашёл сам заказчик, глазами, уже после того как мы закрыли первые три.
Момент, ради которого стоило перечислять. Найти один хардкод и успокоиться - самая дорогая ошибка в такой задаче. Значение, которое считается "настройкой", в живой системе расползается по слоям: модуль веб-сервиса, обработчик в правилах обмена, текст запроса внутри тех же правил, общий модуль интеграции. Слои писали разные люди в разные годы, друг про друга они не знают.
Почему поиск по конфигурации помогает наполовину
Первый приём тривиален, но его почему-то не делают: ищите по старому значению, а не по новому. Нового значения в конфигурации нет по определению, оно же новое. Ноль результатов люди читают как "чисто", хотя это просто неправильный запрос.
Второй приём труднее, и он про отделение находок от целей. Поиск по смыслу, по слову "ставка", выдаёт десятки мест. В аудите этой же задачи нашлись четыре функции получения ставки в фискальном блоке, которые новую ставку уже поддерживали и трогать их было не надо, и ссылка на старую ставку в типовом модуле обмена через универсальный формат, которую трогать нельзя вообще. Отделять "нашлось" от "надо чинить" пришлось руками, и это заняло больше времени, чем сами правки.
Главная ловушка: макет правил в конфигурации не работает
Вот тут я потерял бы день, если бы не полез проверять настройки плана обмена.
План был настроен так: типовые правила не используются, источник правил - файл. В переводе на практику это значит, что рабочие правила лежат в регистре сведений "Правила для обмена данными": текст XML (расширяемый язык разметки, в нём же правила отдаёт "Конвертация данных") в реквизитах правил и правил корреспондента плюс скомпилированное представление в отдельном реквизите. Макет внутри конфигурации при этом никуда не девается и продолжает лежать там, где лежал.
Что происходит дальше, если про это не знать.
Поиск по конфигурации находит макет правил обмена и показывает в нём ровно тот хардкод, который вы ищете. Номер строки есть, текст совпадает, всё сходится. Вы правите макет, обновляете конфигурацию базы данных, запускаете обмен - и не меняется ничего. Исполняется XML из регистра, а он к макету отношения не имеет с того дня, как правила туда загрузили.
Поиск не соврал. Он ответил на другой вопрос: что написано в поставке. А что исполняется - лежит в данных, и глобальный поиск данные не смотрит.
Чинили так. Выгрузили XML из регистра, сложили бэкап по каждой базе и по каждому направлению, внесли правку в XML и залили обратно штатным методом библиотеки стандартных подсистем, тем самым, которым правила загружаются из интерфейса. Метод хорош тем, что сам валидирует компиляцию: если правила не собираются, он ставит отказ и в регистр ничего не пишет. Откат - перезалить сохранённые XML тем же механизмом, никакой ручной хирургии.
Отдельно про репетицию на тесте. Раз правила лежат в данных, а не в конфигурации, тест месячной давности содержит другие правила, и прогон на нём не доказывает ничего: вы отрепетировали правку того, чего в бою уже нет. Значит тест нужен свежий, а поднимать его руками перед каждой такой задачей никто не станет. Скрипт ночного восстановления собирает Тестовая база из бэкапа рабочей: задание для SQL Agent или cron каждую ночь поднимает тест из последнего бэкапа прода поверх старой копии. К СУБД обработка не подключается, только печатает скрипт, который админ читает глазами перед запуском.
Проверка после правки свелась к трём счётчикам по каждой базе: дефолтов со старой ставкой ноль, ветка с новым числовым коэффициентом одна, признак загруженных правил стоит. И функционально: по контрольной сумме документа налог посчитался, до правки там был ноль.
И про копии. Правила лежат в каждой базе своим экземпляром: в учётной основные, в розничных корреспондента. Это зеркала одного правила, и правка, сделанная в одной базе, в остальных сама не появится. Три базы - три правки, три бэкапа, три проверки. Считать копии надо до начала работы: любая находка автоматически умножается на их число, и планировать окно по одной правке значит промахнуться втрое.
Про числовой расчёт ставки скажу честно, потому что защищать этот способ не буду. Правку внесли внутрь сгенерированных правил, в текст запроса. Правильный путь - перегенерировать правила в "Конвертации данных 2" с учётом новой ставки. Мы пошли коротким, потому что окно измерялось часами, и оставили это долгом.
Что показала диагностика вернувшихся карточек
Через шесть дней после массовой правки повторный отчёт дал: в учётной базе 154 627 карточек на новой ставке и 1 122 на старой. В обеих розничных ноль на старой, чисто.
Прежде чем переписывать второй раз, посмотрели, кто именно вернулся. Иначе непонятно, всё ли нашли.
Ни одна из 1 122 не помечена на удаление. Соблазн списать возврат на мусор, который просто не тронули при первой правке, был - и он не подтвердился.
Все 1 122 сидят на одном виде номенклатуры. Значит отбор шёл по признаку: случайная выборка так ровно не ложится, дата изменения тут тоже ни при чём. У самого вида ставка при этом правильная: справочник видов поправили, и он остался поправленным. Возвращались только карточки.
Диапазон кодов широкий, от карточек, созданных давно, до созданных на этой же неделе. Арифметика подтверждает: сумма трёх колонок стартового отчёта по учётной базе даёт 153 739, а после правки там 155 749, то есть за неделю прибавилось около двух тысяч новых карточек. Вернувшиеся оказались смесью старых и свежесозданных карточек; на одну откатившуюся пачку это не похоже.
Дальше честная часть, ради которой этот раздел и написан.
Из наблюдений следует, что признак у пострадавших общий и привязан к виду номенклатуры. Не следует, какой именно механизм их переписал. Хардкод в общем модуле интеграции сюда не подходит: он заполняет строки документов, карточек номенклатуры не касается вовсе, да и вид там другой. Правила обмена подходят по смыслу, версия про откат обменом выглядит правдоподобно, но прямого доказательства у нас нет: никто не поймал за руку конкретную загрузку, переписавшую конкретную карточку.
Заказчик выбрал применить фикс данных и не заказывать расследование причины. В журнале это зафиксировано прямым текстом: корень не устранён, разовая правка временная, при следующей проверке может накопиться снова. Так что раздел "нашли и победили" здесь отсутствует. Данные вернули в порядок, причина осталась гипотезой, и выдавать её за установленную я не буду.
Как переписывали и что дала арифметика
Штатный асинхронный режим массовой правки в этих базах молча не отрабатывал: команда уходит, ошибок нет, эффекта тоже. Разбираться с ним было дороже, чем обойти, поэтому сделали синхронный драйвер: сначала тест на пяти записях, потом батчи по 400.
Разбивка: 5 + 400 + 400 + 317 = 1 122. Ошибок ноль.
На этой строке стоит задержаться. В исходном журнале записано "батчи 400+400+317 = 1122". Сложите сами: 1 117. Пятёрки не хватает, и это ровно тестовый прогон. В базу он записался. В сумму батчей его никто не включил. По влиянию пять записей на тысячу с лишним - мелочь. По последствиям всё хуже: через полгода это несведение будет выглядеть как потерянные записи, и объяснять его будет некому. Тестовый прогон - такая же запись в бой, как остальные, и в сумму он входит.
Вторая арифметика сходится точно, и она нужна как приёмка. До правки в учётной базе было 154 627 карточек на новой ставке, вернувшихся 1 122, после правки стало 155 749. Сумма сходится копейка в копейку, значит переписали ровно вернувшихся и никого лишнего не задели. Такую проверку стоит делать всегда, когда правите массово: если сумма не сходится, вы зацепили что-то помимо цели, и узнать об этом лучше сразу.
Итог по трём базам после правки: 155 749, 137 636 и 154 425 карточек, все на новой ставке. Позиций с другой ставкой нет ни в одной базе. Через несколько часов вторая розничная подросла до 154 898, и новые карточки создавались уже на новой ставке.
Проверка формой опровержения
Финальная сверка была одна и простая: выбрать по каждой базе позиции, у которых ставка отличается от новой. Ответ - пусто.
Обратите внимание на форму запроса. Мы не считали, сколько карточек переписали. Такой счётчик - отчёт о собственной работе, он всегда красивый и на вопрос приёмки не отвечает. Мы искали тех, кого не переписали. Первый запрос говорит, что я сделал; второй говорит, что осталось сломанным. В приёмку идёт второй.
Та же логика применилась к проверке кода. Заказчик применил правку общего модуля у себя, в двух базах. Верить на слово в такой задаче нельзя, а смотреть в систему контроля версий бесполезно: папка выгрузки конфигураций там в игноре, статус ничего не покажет. Сняли свежий инкрементальный дамп конфигурации из базы в файлы и прочитали строку в выгруженном модуле. Строка новая - значит правка в базе. Доказательством работает дамп из живой базы. Сообщение "я обновил" доказательством не работает.
Отдельная мелочь, которая стоила времени и которую стоит запомнить. Дамп конфигурации в файлы прошёл под учётной записью куратора со стороны заказчика, а под той учёткой, которую все считали административной, конфигуратор отвечал "not authenticated". Право на выгрузку конфигурации и полнота прав, в которую вы верите, живут отдельно друг от друга, и выясняется это в самый неудачный момент.
Границы применимости и что мы не сделали
История с регистром работает только при источнике правил "файл". Если у вас типовые правила из конфигурации, макет и есть рабочее правило, и всё написанное выше про регистр к вам не относится. Зато относится обратное: у вас правила меняются вместе с обновлением конфигурации, у нас - нет, и это разные операционные риски.
Причина возврата 1 122 карточек не установлена. Признак сузили до вида номенклатуры, версию про обмен считаем правдоподобной, доказательства нет, расследование не заказывали.
История в проведённых документах не тронута. Строки чеков и отчётов о розничных продажах за прошлые периоды остались со старой ставкой. Правка общего модуля чинит новые документы, старые она не переписывает. Перепроведение фискальных документов - решение с совсем другой ценой ошибки, и принимать его походя мы не стали.
Одно расхождение в цифрах, которое мы не объяснили. Сложите строку первой розничной в стартовой таблице: 132 792 + 4 839 + 3 = 137 634, а через неделю там 137 636. Прирост две карточки за шесть дней. В учётной за тот же срок прибавилось 2 010, во второй розничной 1 779. Числа верные, все три сняты одним и тем же отчётом, но почему первая розничная практически не выросла, я не знаю: замера по созданию карточек в ней мы не делали. Пишу это здесь, чтобы никто не потратил время, пытаясь свести таблицу.
Чего в журнале нет. Сколько заходов на задачу было до первого найденного хардкода и какие версии причины отвергли по дороге. По хронологии видно, что заходов было несколько, но числа нигде нет, и придумывать я его не буду.
Открытый вопрос
Зашитые соответствия в правилах обмена - ставки, коды видов, единицы измерения, коэффициенты пересчёта - находятся всегда случайно и всегда после того, как выстрелили. Кто-нибудь делал у себя регулярную ревизию на такое? Мне видится единственный формализуемый вариант: выгрузить рабочие правила из регистра в текст и прогнать поиск по значениям, которые обязаны быть данными. Дальше всё равно упирается в ручной разбор находок. Если у кого-то получилось лучше, расскажите, как.
Вступайте в нашу телеграмм-группу Инфостарт