Как понять, что надо сделать перерыв в работе? Скажем так: если ваш скрипт внезапно начал вам подмигивать — точно пора проветриться.

Мои мысли о жизни, работе и разных интересных штуках вокруг меня.
Показано: 0 · Страница
Как понять, что надо сделать перерыв в работе? Скажем так: если ваш скрипт внезапно начал вам подмигивать — точно пора проветриться.

Итак, Конфигуратор выдает ошибку; нужно её исправить или обойти. Это потенциально неприятный расклад: у нас нет никакого доступа к коду приложения. Тем не менее, чтобы решить проблему — важно понять, что именно делал Конфигуратор до сбоя и почему он не справился.
Что может с этим помочь?
Первое — сам текст ошибки. Нередко его вполне достаточно, чтобы мысли двинулись в правильном направлении. Если причиной сбоя стал запрос, то в ошибке будет ещё и сообщение от СУБД.
Второе — технологический журнал по событиям EXCP, SDBL и DBMSSQL (DBPOSTGRS?) для t:applicationName=Designer. Из него мы получим информацию об исключениях внутри Конфигуратора и данные запросов, которые он выполняет (нередко они и есть причина ошибки).
Кроме того, может пригодиться трассировка запросов к базе данных. Если используется MS SQL, то трассировку можно получить через Extended Events — конкретно, нас интересуют события error_reported, rpc_completed и sql_batch_completed. Общий принцип тот же — ловим ошибки выполнения запросов и сами запросы.
Разберем пример — может, не самый показательный, зато свежий.
Контекст — конфигурация, использующая разделение данных. Её база данных «нарезана» на кусочки (области данных), каждый из которых ничего не знает о своих соседях — сколько их, какого они размера и так далее. При этом в БД есть и общие данные — например, справочники, к которым можно обратиться из любой области. Как правило, это разная техническая информация — например, перечень объектов метаданных.
Задача — исключить справочник FileStorageVolumes из основного разделителя. Сейчас этот справочник — разделенный: то есть, его данные от области к области будут различаться. Нам нужно сделать так, чтобы содержимое справочника стало одинаковым для всех областей.
Задача несложная, так как таблица справочника пуста — ни одна из областей ничего в нем не хранит. Что же, применяем настройку и:

В тексте исключения есть сообщения: одно от платформы, второе от СУБД. Первое озадачивает: то есть как это данные не уникальны? Справочник же пуст, никаких данных нет. Возможно, проблема в каких-то вспомогательных структурах, не связанных с содержимым справочника напрямую.
Сообщение СУБД более внятное: MS SQL Server хотела создать уникальный индекс для таблицы _DataHistorySettingsNG, но не смогла, так как сочетания индексируемых полей оказались неуникальны. Приводится даже конкретное значение, из-за которого не получилось создать индекс: это NULL.
Выводы?
Последнее объясняет, почему проблема уникальности возникла на пустом справочнике: настройки истории данных для объекта хранятся независимо от того, есть в объекте какие-то данные или нет. Если посмотреть на таблицу с настройками, там всего три поля: _MetadataId (ID объекта метаданных), _Content (значения настроек) и _Fld626 (разделитель области).
До реструктуризации данные имеют примерно такой вид:

Однако потом эта картина изменилась. Когда мы исключили справочник из состава общего реквизита, конфигуратор запустил реструктуризацию: создал таблицу _DataHistorySettingsNG, перенес в неё данные из _DataHistorySettings и установил значение поля _Fld626 в NULL всем записям, которые относятся к справочнику FileStorageVolumes.
К чему это привело? А вот к чему: для справочника FileStorageVolumes появился целый ворох настроек, которые не относятся к какой-либо области. Это само по себе звучит нездорово, но настоящие проблемы начались, когда Конфигуратор попытался создать для таблицы кластерный индекс: он строится по полям _MetadataId и _Fld626, является уникальным и, соответственно, не может быть создан — в таблице множество записей, у которых различается только поле _Content, а _MetadataId и _Fld626 — гарантированно идентичны.
Для очистки совести посмотрим техжурнал (я вычистил оттуда нерелевантные события и другую постороннюю информацию). Наши догадки подтверждается: видим, как Конфигуратор создает и заполняет таблицу _DataHistorySettingsNG, пытается проиндексировать её, но получает ошибку и удаляет. В трассировке СУБД примерно та же картина.
На этом интересное заканчивается, так как решение достаточно очевидно: удаляем настройки истории данных для справочника во всей областях данных и повторяем реструктуризацию. Ошибку это не исправит, конечно; по хорошему, Конфигуратор должен предсказывать такую ситуацию и выдавать адекватное сообщение — как, например, при удалении измерения регистра сведений, которое приводит к нарушению уникальности измерений для записей регистра. Однако задача будет решена.
В общем, готово — мы великолепны!
Ещё скрипт. Считает количество исключений в минуту и строит топ, по которому видно распределение. Можно быстро оценить периоды, когда программы сбоили особенно яростно.
По ходу дела столкнулся в двумя любопытными проблемами, которые меня порядком сбили с толку. Во-первых, я почему-то был уверен, что uniq -c группирует строки вне зависимости от того, где в потоке данных они встречаются. Рассмотрим пример:
банан
банан
груша
банан
Я думал, что если отдать эти данные uniq -c, то она сгруппирует одинаковые строки, посчитает количество повторений и выдаст примерно такое:
3 банан
1 груша
Но на деле получилось так:
2 банан
1 груша
1 банан
Вывод: утилита uniq ожидает, что повторяющиеся строки идут одна за другой. Если строка отличается от предыдущей — она начинает считать счетчик совпадений для неё с нуля. То есть, чтобы получить тот результат, на который я рассчитывал — нужно сначала отсортировать данные, и только потом передавать их в uniq.
Второй проблемой стала утилита sed. С помощью неё я пытался удалить из потока данных всё, кроме часов и минут (текст попытки на 12-й строке скрипта). Однако часть событий упорно не попадали под регулярку несмотря на то, что визуально никак не отличались. Я промаялся кучу времени и здорово разозлился, но потом вспомнил про существование BOM. Вычистил их и дальше все пошло как по маслу.
BOM используется во всех файлах ТЖ (в этом можно убедиться, например, с помощью скрипта). То есть каждый лог начинается с особых символов, которых невооруженным глазом не увидеть и которые могут помешать обработать первую строку файла (так как эти самые символы не будут попадать под условие регулярного выражения).
Вывод: проще всего удалять BOM по умолчанию, не оценивая рисков для каждой конкретной задачи. Да, иногда это будет лишним (например, решению задачи по тексту выше через grep BOM никак не мешает). Но я не люблю сюрпризы. Кроме того, несколько тактов процессора на простую замену — явно выгоднее, чем эквивалент в сгоревших нервных клетках и потерянном времени.

Дочка пишет о своих планах — мол, не теряй меня. А у меня профессиональная деформация: этот вполне нормальный чат мой мозг упорно воспринимает как код на Gherkin. Просто какой-то неправильный, что ли, хочется быстренько пофиксить :-)
И я выхожу
Тогда я в школе
И я выхожу
Тогда я покачаюсь
Мы на этом языке пишем автотесты нашей конфигурации для Vanessa Automation. Вроде не так уж много я их накатал (сравнивая с некоторыми коллегами — баловался, считай). Но, видимо, достаточно.
Набросал ещё два скрипта для анализа ТЖ: первый строит топ тяжелых запросов к MS SQL, второй — топ длительных ожиданий на блокировках.
Тяжелые запросы определяются по продолжительности событий DBMSSQL. То есть, чем дольше выполнялся запрос — тем вероятнее, что в процессе он слопал кучу ресурсов. Обычно это так и есть, хотя для нормальной диагностики нужно смотреть трассировку.
Ожидания на блокировках тоже считаются по продолжительности. При этом скрипт проверяет, что у события TLOCK заполнено свойство WaitConnections — то есть платформа действительно ждала возможности установить блокировку, а не просто потратила какое-то время на её установку.
Переписал скрипт на баше, строящий топ исключений по собранному ТЖ: хотел решить эту задачу как-то попроще.
В итоге выкинул из кода возню с заменой начала события на маркер gawk для разделения записей (его можно сразу задать регулярным выражением) и перенес больше логики в скрипт для gawk (так нагляднее, особенно если потом захочется её расширить).
Получилось явно лучше, чем было — во всяком случае, логика выглядит понятнее. Изначально, правда, я хотел скорее сократить скрипт в размере и получить что-то вроде:
grep -hoP ",EXCP,.*\KDescr=.*" */*.log | uniq -c | sort -rn
То есть фильтруем только строки с событием EXCP, отрезаем всё до описания ошибки и группируем с помощью uniq. По-моему, очень изящно.
Однако описание у EXCP может быть многострочным. То есть мы будем время от времени терять часть данных, нужных для расследования (всё описание после первого же перевода строки). Как решить эту проблему так, чтобы скрипт не разбарабанило втрое — я пока не придумал :-)
В прошлом месяце был на митапе «Инфостарта» по безопасности решений на платформе 1С. Узнал кучу интересного! Среди прочего, Олег Тымко обзорно рассказывал про подходы в разработке, которые можно считать потенциальными уязвимостями продуктов. Например, зашитых прямо в код IP-адресов, ссылок, e-mail'ов, паролей и так далее.
Накидал вчера простой скрипт на баше, который роется в выгрузке конфигурации в поисках таких косяков. По сути, эта штука просто пропускает конфигурацию через регулярки — я особо не заморачивался и чисто из спортивного интереса реализовал два-три варианта.
Вывод: для серьёзной итеративной разработки лучше сразу думать в сторону чего-то вроде SonarQube, а не колхозить вот это всё — подходы «в лоб» дают слишком много ложных срабатываний, нужно думать над фильтрами, исключениями, историей и другой обвязкой.
А вот для быстрой оценки такого подхода вполне достаточно — никакой инфраструктуры, никакой возни с настройкой и железом. Выгрузил конфигурацию, пнул баш и, собственно, всё — готовый результат за несколько секунд.
Этот эпизод схватки двух йокодзун конфликта между Apple и Epic Games, безотносительно всего прочего — отличное напоминание, что Single Sign-On в интернете использовать нельзя: нигде, никогда, ни на каких сервисах. Неизвестно, какие ещё гиганты внезапно сойдутся на кулачках или какой сайт забанит тебя без всякой внятной причины. Because screw you, dude, that's why.
В кейсе из твита выше пользователи, возможно, отделаются малой кровью, а вот если вашу учетку заблокирует что-то вроде Google — ситуация вполне может оказаться критической: куча сервисов, на которые вы ходили через гугл, резко превратятся в тыкву.
Решений, по удобству ничем не уступающих Single Sign-On, полным-полно. Менеджеры паролей, разнообразные расширения для браузеров, «железные» ключи — да что угодно будет лучше, чем проприетарные сервисы с закрытым кодом, на которые вы не имеете никакого влияния.
В конце августа сдал тест на профессионала 1С по техническим вопросам крупных внедрений. Это по сути такой входной билет на основной экзамен, призванный проредить поток претендентов и заставить их подучить теорию.
В тесте четырнадцать вопросов, случайно набранных из порядка пятиста возможных; нужно правильно ответить хотя бы на двенадцать. На это дается полчаса, однако по факту достаточно пяти-десяти минут: большинство ответов логически выводится или запоминается. Попадаются и неточности, и расплывчатые формулировки, но их мало. Норм вопросы, короче.
Подбор тем показался мне порядочным винегретом — по чуть-чуть из всего подряд. Впрочем, по факту такой охват скорее полезен: смотришь на очередной непривычный вопрос, отвечаешь, ошибаешься. Тушишь задницу, лезешь разбираться как так. Становишься немного умнее (но это не точно).
Приведу вариант решения проблемы неразрешимых ссылок, возникший после удаления функциональной опции. Вводная — при проверке конфигурации получаю десятка два сообщений вида:
CommonForm.PersonalSettings.Form Unresolved metadata object references (2)
Catalog.BankAccounts.Form.GLAccountsEditForm.Form Unresolved metadata object references (1)
Catalog.CashRegisters.Form.GLAccountsEditForm.Form Unresolved metadata object references (1)
Выгружаю конфигурацию в файлы, открываю Form.xml для одной из проблемных форм. Начинаю с той, где элементов немного — так проблему заметить будет проще.
Бегло просматриваю файл в поисках чего-то необычного. Искать долго не приходится:

Нормальная ссылка на функциональную опцию — её имя (как в случае DepreciationOfAssets). А вот если вместо имени указан GUID — этой функциональной опции в конфигурации нет. Ссылка неразрешима.
Делаю поиск этого GUID'а по остальной выгрузке и нахожу почти все проблемы, на которые ссылалась платформа при проверке. Решить их легко: удалить битую ссылку из XML, а потом загрузить файл обратно в конфигурацию. Можно ещё проще: открыть список функциональных опций для элемента формы и тут же сохранить его. В этом случая битая ссылка также будет удалена.
Почему искать таких потеряшек через сам Конфигуратор — занятие для клинических оптимистов? Да просто проблему почти невозможно заметить. В лучшем случае вы увидите что-то в духе:

Конфигуратор понимает, что в поле две опции, но получить название для второй не может (её нет). А ведь чаще всего функциональная опция только одна! И картина выглядит так:

Пару недель назад столкнулся с досадным багом. Контекст — примитивнее не придумаешь: нужно найти документ по номеру. Если сделать так — документ будет найден:
SELECT Ref FROM Document.Invoice WHERE Number = &Number
А вот так — фигушки:
Documents.Invoice.FindByNumber(Number)
Сначала я даже слегка завис, но потом полез в документацию и, конечно, нашел ответ. У метода FindByNumber() есть второй параметр, IntervalDate, нужный для поиска периодического документа. С помощью него можно сузить поиск до конкретного периода; например, если периодичность нумерации — год и мы присвоим параметру значение 01-05-2020, то поиск пойдет в периоде от 01-01-2020 до 31-12-2020. А нумерация у документа Invoice и правда периодическая — в пределах года.
Так в чем проблема? Дело в том, что за уклончивым «the parameter is used for documents with periodic numbering» на самом деле скрывается железное правило: параметр нельзя опускать для периодических документов.
Чтобы убедиться в этом, посмотрим, какой SQL выполняется на стороне СУБД. Делаем обычный запрос — никаких сюрпризов. P1 здесь — разделитель, P2 — номер документа:
SELECT
T1._IDRRef
FROM
dbo._Document283 T1
WHERE
((T1._Fld704 = @P1))
AND ((T1._Number = @P2))
А вот если выполнить FindByNumber(), то в запросе появится третий параметр со значением 2001-12-31 23:59:59 — ну да, «конец первого года с начала времён»:
SELECT
T1._IDRRef
FROM
dbo._Document283 T1
WHERE
((T1._Fld704 = @P1))
AND
(
T1._Number = @P2
AND T1._Date_Time <= @P3
)
Конечно, такой запрос не находит нужный документ. Он вообще ничего не найдет в любой реальной базе — никогда, ни при каких обстоятельствах.
Подведем итог. Как решить проблему — понятно: указываем IntervalDate или, если даты нет, подключаем ЗначениеРеквизитаОбъекта() БСП или его аналог. Но, честное слово, со стороны платформы, было бы куда адекватнее выбрасывать исключение, если FindByNumber() вызван без IntervalDate, а у документа включена периодичность — чем вот так, тихой сапой, делать гарантированно бессмысленные запросы.
Истории ради закинул на Github Gist один из неоптимальных запросов, с которыми возился несколько месяцев назад. Он использовался для динамического списка в форме элемента справочника и, когда пользователь открывал вкладку с этим списком, платформа погружалась в медитацию даже в относительно небольшой базе.
На первый взгляд структура запроса простая и четкая: пачка запросов к таблицам документов, соединяемых через ОБЪЕДИНИТЬ ВСЁ. Каждая из таблиц фильтруется по примерно одинаковым условиям — тип ссылки, дата и вхождение ссылки в результат подзапроса.
Однако потенциальных проблем тут сразу несколько. Во-первых, работа идет как минимум с 14-ю таблицами — по числу соединяемых запросов. Это само по себе повышает риск того, что оптимизатор не успеет подобрать хотя бы относительно приличный план выполнения. Скорее всего, он просто ткнет пальцем в небо, а дальше — как повезет.
Во-вторых, большая часть этой работы, вероятно, будет выполняться впустую. Каждый из запросов содержит примерно такую конструкцию:
WHERE VALUETYPE(AdditionalExpenses.Ref) IN (&DocumentsListSelectedTypes)
То есть в динамический список передается список типов документов, которые требуется вывести. Однако это условие будет наложено после выборки данных, и если пользователю нужны только инвойсы — СУБД все равно сначала выгребет все 14 таблиц, а потом отбросит 13 из них.
Но это всё не так критично. Если бы список проблем этим и ограничивался, мы, возможно, и не полезли бы разбираться. Главная проблема — во втором условии секций WHERE: каждый запрос проверяет вхождение ссылки на документ в результат подзапроса.
Использование вложенного запроса в условиях — само по себе почти табу, если речь идет не о временных таблицах: СУБД часто не в состоянии понять, сколько данных вернет подзапрос и, соответственно, какой способ работы с ними подойдет лучше. Однако тут это ещё и усугубляется тем, к какому источнику данных мы делаем запрос. Критерий отбора — это не таблица в базе данных, которую можно прочитать, пусть даже со сканом — это набор таблиц. В критерии отбора DocumentsByProject их тридцать!
В этом месте можно было бы сказать «занавес», но нужно добавить, что выборку из тридцати таблиц по определенному типу документа делает каждый из четырнадцати соединяемых запросов.

Вот теперь занавес :-)
Обожаю JavaScript! Каждый раз, когда у меня сгорает задница от какой-нибудь странной дичи в любимой платформе, я просто открываю любой тред про будни веб-девелоперов и быстро, очень быстро успокаиваюсь.
Впрочем, похожие трюки можно повторить в 1С. Например, JavaScript понимает window.window.window.location, а платформа — вот это :-)
Доработал логику запускатора служб. Теперь, если вызвать скрипт без параметров (т.е. не указав ни -start, ни -stop) — он сам решит, запускать службы или останавливать их.
Для этого скрипт определит статус первой службы в списке. Если она работает — все службы из списка будут остановлены, если выключена — скрипт попытается их запустить.
Зачем это нужно? Ну, если вы вызываете скрипт прямо с программируемой клавиатуры, как я — теперь вам нужна только одна кнопка. Раньше было нужно две: одна для запуска служб, вторая для остановки.
В середине прошлого года я загорелся идеей переписать этот сайт на чем-то посовременнее PHP, выбрал Vue.js в качестве фреймворка и принялся за дело. JavaScript я на тот момент почти не знал, поэтому набил прорву шишек на кейсах в духе «метод вывода даты падает в Safari» — однако в итоге получил вполне жизнеспособное приложение.
Ну, как жизнеспособное? С одной стороны, главное у меня получилось — я разработал клиентское веб-приложение, которое ходит на сервер только за данными, а весь интерфейс собирает само. С другой — пришлось накрутить на фреймворк целую армию костылей хуков даже для самых простых штук (вроде подсветки элементов меню или, скажем, выдачи правильных кодов HTTP). Кое-что я вообще с наскока реализовать не смог — например, выдачу HTML-версий страниц для Яндекса (их пауки в 2020-м году не умеют индексировать сайты на JavaScript).
В общем, опыт вышел полезным, но результат — настолько спорным, что в конце концов я бросил эту затею, а получившегося Франкенштейна выложил на GitHub. Возможно, он кому-то сэкономит время на решение задач в духе «как вывести через Vue.js произвольный HTML» или «как научить VueI18n работать с русским языком».
Документации там нет, правда. Я сначала хотел подробно расписать, как что работает, но быстро понял, что потрачу уйму времени без видимой пользы. Вероятно, буду возвращаться к этой теме под настроение — а пока, так сказать, ограничимся парадигмой MVP :-)
Какое-то время назад я писал про командлеты PowerShell, с помощью которых можно запускать и останавливать службы. В итоге я собрал из этих набросков нормальный скрипт: он читает список к запуску или остановке служб из отдельного файла, а ещё проверяет и (если нужно) запрашивает права администратора.
Результат можно посмотреть на GitHub'е. No big deal — хотел поупражняться в языке и упростить ежедневную рутину: в моей системе наберется десятка полтора прожорливых сервисов, которые нужны для работы, но бесполезны в другое время. Вручную останавливать, а потом запускать этот зоопарк неудобно, а вот одной командой — совсем другое дело!
На прошлой неделе листал комментарии к 8.3.15 и наткнулся на метод ПолучитьРазмерДанныхБазыДанных(). Стало любопытно, как эта штука работает и насколько её данные расходятся с теми, которые можно получить из, например, Management Studio.
В итоге накатал что-то вроде консоли, через которую методу можно передавать разные метаданные, и принялся следить за запросами платформы к БД.
В общем, размер данных платформа считает примерно таким выражением:
CAST(
SUM(
CAST(
DATALENGTH(T1._Fld40) AS NUMERIC(12, 0)
)
) AS NUMERIC(18, 0)
)
И так для каждого поля, которое есть у объекта, включая стандартные. Если есть табличные части — они тоже считаются. Результат суммируется.
Выводы?
Ну, во-первых, понятно, почему у метода такое дурацкое название. Он считает не размер таблиц, как я изначально подумал, а именно размер данных — то есть на оценку не влияют ни расходы на схему данных, ни расходы на индексы, ни механика экстентов. Учитывается только размер самих данных, которые хранятся непосредственно в объекте.
Таким образом, реальный объём места, которое слопал условный справочник номенклатуры, будет больше того, которое покажет метод. Возможно, значительно. Для точной аналитики такой подход не годится, но чтобы быстро оценить распределение данных в БД – вполне подходит.
Во-вторых, метод никак не считает расходы на историю данных для анализируемых объектов, что честно указано в документации. Теоретически их можно посчитать вручную, оттолкнувшись от _DataHistoryMetadata, но подождем релиз-другой — возможно, разработчики это добавят.
В-третьих, СУБД в ходе расчетов выгребает все содержимое нужных таблиц, а потом считает размер того, что выгребла. То есть вызов, скорее всего, приведет к куче сканирований и может быстро вымыть буферный кэш. На 1cFresh запросы будут делаться с учетом разделителей, но это слабое утешение, как по мне.
В общем, на работающем проде применять с осторожностью.
Недавно внедрили в нашу конфигурацию встроенный в платформу механизм истории данных вместо морально и функционально устаревшего велосипеда из SSLi. Сейчас как раз дописываю выгрузку историю данных в бэкап и загрузку его обратно: удивительно, но этого пока не умеет ни БСП, ни БТС, ни SSLi (впрочем, от последней я и не ждал).
Как закончу, расскажу подробнее. Пока хочу отметить любопытную опцию, которая пригодилась по ходу дела: встроенные в платформу обработки, которые доступны из меню «Все функции», можно выгрузить в виде обычных epf-файлов! Трюк очень подробно разобрали коллеги на Инфостарте (вот тут и вот тут). Вкратце магия выглядит вот так:
КопироватьФайл(
"v8res://mngbase/StandardDataChangeHistory.epf",
"Q:/StandardDataChangeHistory.epf"
);
Полный список стандартных форм и обработок, которые можно вытащить, способ получения этого списка, а также куча споров вокруг и около — по ссылкам выше.
Зачем это пригодится вам — честно, не знаю. Что касается нас, то мы делали интерфейсы для работы с историей данных и было любопытно, как они написаны у самой 1С (спойлер: довольно неряшливо).
На волне актуальных новостей наткнулся на трёхлетней давности ролик от SpaceX с подборкой неудачных приземлений на плавучую платформу. Как по мне — отличный пример того, как нужно относиться к своим ошибкам. С юмором!
Well, technically, it did land… Just not in one piece.
:D
В комментариях не отстают:
Space X : Launches astronauts for the first time.
Youtube: Let's recommend this video!