Показаны сообщения с ярлыком Библиотека OTL. Показать все сообщения
Показаны сообщения с ярлыком Библиотека OTL. Показать все сообщения

воскресенье, 23 марта 2014 г.

[prog.thoughts] Вспомнил про старую идею о вынесении текстов SQL-запросов в конфиги

Есть такая штука в разработке ПО, как technical debt. Т.е. вещи, которые должны были быть сделаны, но по каким-то причинам не были. Иногда вы утешаете себя тем, что когда-нибудь в будущем, когда руки дойдут, вы таки расплатитесь по своим старым долгам, вернетесь к коряво написанному коду и, не смотря ни на что, все-таки перепишете его как надо. Излишне говорить, что это напрасные надежды, в большинстве случаев. Нет ничего более долговременного, чем временное. Чудеса, т.е. исключения, конечно, случаются. Но, на моей дырявой памяти это именно чудесные исключения, а не устоявшаяся практика...

Впрочем, от лирики пора переходить к физике. Заговорить о техническом долге заставила необходимость изучить некоторый кусок кода, дабы разобраться с возможностью минимальными усилиями адаптировать его под сильно изменившиеся условия задачи. В процессе изучения пришлось восстанавливать тексты SQL-запросов, которые были зашиты в код. И мне вспомнилась идея двух- или трехлетней давности о том, чтобы выносить тексты SQL-запросов в конфигурационные файлы. Идея эта не нашла своего воплощения, в очередной раз увеличив мой собственный технический долг. А, возможно, сейчас бы она сократила мне время на изучение чужого кода.

Суть идеи вот в чем. Мы для работы с РСУБД из C++ приложений (написанных с использованием SObjectizer, понятное дело) применяем отличную header-only библиотеку OTL. Ее особенностью является наличие т.н. потоков (реализуемых классом otl_stream). Поток связан с SQL-выражением. Если в SQL-выражение нужно подставлять параметры, то эти параметры специальным образом обозначаются в тексте SQL-запроса:

otl_stream s(
   1,
   "INSERT INTO TMP_IDS( id ) "
   "SELECT "
      "message_id "
   "FROM DRS "
   "WHERE "
      "received_at < :1<TIMESTAMP> AND "
      "rownum <= :2<UNSIGNED> ",
   db_connect );

Выделенные жирным фрагменты ":1<TIMESTAMP>" и ":2<UNSIGNED>" -- это и есть параметры для SQL-запроса. Для них перед выполнением запроса нужно задать значения. Значения задаются в стиле C++ных потоков вывода: через operator<<():

s << make_otl_datetime( time_border ) << max_count;

Вот такой примитивный, но вполне себе успешно работающий подход мы уже очень давно применяем в своих C++ проектах. Про различные ORM-инструменты, скрывающие от разработчика детали работы с БД, мы в курсе. Но, во-первых, C++ не Java, не Ruby и не C#. Как-то в C++ ORM-ов не густо. И, во-вторых, у многих ORM-ов, насколько я знаю, не все так гладко, когда нужно выжимать максимальную производительность из БД. Тут быстро выясняется, что SQL -- это не столько стандарт, сколько соглашение. А у каждой серьезной РСУБД есть свой вариант SQL-я и, если тебе за секунду нужно обработать тысячи строк в разных таблицах, то без задействования специфических для СУБД SQL-выражений далеко не уедешь. Вот и оказывается, что чем примитивнее инструмент, тем проще с его помощью выжимать нужную производительность.

При разработке и отладке приложений мы, естественно, профилируем свои SQL-запросы. Добавляем в них различные хинты для СУБД. Иногда существенно перерабатываем SQL-запросы, переходя от простых запросов над целевой таблицей, к более сложным комбинациям с временными таблицами, накапливающими промежуточные значения...

Однако, практика показывает, что как бы мы не старались на этапе разработки, в эксплуатации, на production-железе под реальной нагрузкой, когда разработчики не могут просто так взять и дотянуться до СУБД, возникают различные странные ситуации. Скажем, несколько дней программный компонент может работать с одной скоростью, а затем на несколько часов его производительность вдруг упадет в разы и проблема будет где-то в БД. В некоторых случаях служба эксплуатации обнаруживала проблемы и выдавала свои рекомендации. Иногда это были дополнительные хинты для SQL-запроса, актуальные для конкретной инсталляции СУБД на конкретном железе.

Отсюда и появилась идея о том, чтобы тексты SQL-запросов сохранялись в конфигурационных файлах приложений. Чтобы эксплуатация, решившая исправить проблемы производительности дополнительным хинтом, могла сделать это самостоятельно, без отвлечения разработчиков. К сожалению, идея не нашла своего воплощения. Хотя, думаю, идея эта здравая и не мешало бы изыскать время на ее воплощение.

Насколько я помню, главными препятствиями к ее реализации были две вещи:

  • во-первых, текст SQL-запроса в коде далеко не всегда задавался в виде константного строкового литерала. Иногда он собирался из нескольких кусков. И названия таблиц, столбцов, индексов, а так же состав столбцов или даже содержимое условных выражений в SQL-запросе вычислялись прямо в runtime в зависимости от ряда условий. В принципе, подобную проблему можно было бы решить применив какой-нибудь template engine (т.е. в конфиге бы хранился не готовый текст SQL-запроса, а шаблон для его генерации). Но это получалась бы уже не совсем тривиальная штука, отдавать которую в руки эксплуатации было чревато возникновением неожиданных проблем, с которыми нам бы самим потом пришлось бы разбираться :)
  • во-вторых, параметры в otl_stream должны получать свои значения в том порядке, в котором они заданы в тексте SQL-запроса. Если текст SQL-выражения хранится в конфиге, то оператор ни в коем случае не может менять порядок следования параметров в нем. А это, временами, нужно было делать. Чтобы разрешить такие вещи, нужно было бы придумать механизм поддержки именованных параметров для otl_stream-а. Т.е., чтобы в конфиге оператор мог записать "where rownum <= :MaxLines<UNSIGNED> and received_at < :TimeBordre<TIMESTAMP>", а в программе бы параметры со своими именами шли в другом порядке:

    s << named_parameter( "TimeBorder", make_otl_datetime( time_border ) )
       << named_parameter( "MaxLines", max_count );

    А вот на это уже не нашлось времени.

Вот такой пример технического долга. Вообще, технический долг -- непростая штука. Временами кажется, что это и не долг вовсе: ведь работает же все, а времени сделать нормально с самого начала никогда не хватает. Но, если речь идет о производстве компанией собственных продуктов, технический долг может стать реальной проблемой. Обуславливающей смерть продукта... Хотя это уже совсем-совсем другая история.

понедельник, 2 апреля 2012 г.

[prog.bugs] Помог диагностировать еще одну проблему в OTL

На прошлой неделе произошел вопиющий случай – начала течь память в новой версии одной из моих C++ных программ. Поскольку таких вещей уже очень давно не было, яростно бросился искать причину.

Оказалось, что виной был баг в OTL, который проявлялся при стечении следующих обстоятельств:

  • использовался механизм пулинга otl_stream-ов (#define OTL_STREAM_POOLING_ON);
  • otl_stream открывался не через передачу аргументов в конструктор, а через вызов метода open().

Если затем для такого otl_stream-а вызова метода close() не было (закрытие посредством деструктора) или же явно вызывался метод close(true) (т.е. с указанием сохранить поток в пуле), то память начинала течь.

Детали найденной проблемы были отосланы Сергею Кучину, разработчику OTL, который уже выпустил версию OTL 4.0.260, в которой данная проблема исправлена.

четверг, 22 марта 2012 г.

[prog] Нашел баг в OTL

Некоторые 32-битовые ODBC-драйвера (например, производства Oracle) не поддерживают 64-битовых целых чисел (т.е. не могут работать с bigint-ами). В библиотеке OTL для таких случаев есть обходной маневр: группа макросов OTL_BIGINT, OTL_BIGINT_TO_STR и OTL_STR_TO_BIGINT. Если их должным образом определить, то OTL автоматически заменяет работу с 64-битовыми числами на работу с их строковыми представлениями. И приложение даже не замечает, что ему приходится работать с ущербным ODBC-шным драйвером.

К сожалению, в версиях OTL до 4.0.257 включительно, комбинация из OTL_BIGINT/OTL_BIGINT_TO_STR/OTL_STR_TO_BIGINT и OTL_ODBC/OTL_ODBC_MULTI_MODE не обрабатывалась должным образом. Не производилось преобразования чисел в строку и обратно.

К счастью, OTL – это OpenSource-библиотека. И хотя там достаточно своеобразный подход к организации кода (вся библиотека – это один большой h-файл), но удалось весьма быстро найти как причину проблемы, так и способ ее устранения.

Сразу же после этого описание найденной ошибки были отправлены автору библиотеки, Сергею Кучину, который уже опубликовал версию 4.0.258 с исправлениями. За что ему респект и уважуха!

PS. Все-таки библиотеки с полными исходниками есть рулез! :)

PPS. А по поводу качества Oracle-вских продукт пердело поделок я еще скажу пару ласковых.

пятница, 3 декабря 2010 г.

[prog] OTL4, MSSQL, bulk insert, триггер на вставку и ошибка 24000

Нужно было сделать быструю вставку записей в табличку MSSQL. Задействовал нестандартную SQL-ную фичу с конструкцией insert into … select … union all … select … union all. Работал через OTL4 и сделал пул otl_nocommit_stream-ом (с разным количеством строк внутри). Например, если в пуле были otl-потоки для 20, 10, 5, 2 и 1-ой строки, а нужно было вставить 59 строк, то два раза использовался поток на 20 записей, один раз поток для 10 строк, один раз поток для 5 строк и два раза поток для 2-х строк.

Все работало хорошо до тех пор, пока в базе не появился триггер на вставку в эту таблицу. Триггер должен был срабатывать на insert и добавлять строки в другую таблицу. Но после того, как он срабатывал, у меня при выборе следующего otl-потока из пула возникала ошибка 24000: [Microsoft][SQL Server Native Client 10.0]Invalid cursor state.

Поиск по Интернету показал, что такие вещи при работе через ODBC уже случались. И в качестве workaround-а рекомендовали после каждого SQLExecute для вставки в основную таблицу делать обращение к SQLMoreResults.

Легко это сделать когда ты сам осуществляешь вызовы SQLExecute. Но за меня это делает OTL. Да еще делает это неявно для меня, внутри своих otl-потоков. Так что как применить данную рекомендацию поначалу было непонятно.

К счастью, OTL доступен в исходных текстах (более того, весь OTL – это один большой заголовочный файл). Посредством поиска удалось выяснить, что обращение к SQLMoreResults в OTL есть. Происходит оно внутри метода sql_row_count(), а sql_row_count() вызывается после обращения к SQLExecute.

Но sql_row_count() использует SQLMoreResults не всегда, а только когда определен макрос OTL_ODBC_ALTERNATE_RPC. И в документации к OTL_ODBC_ALTERNATE_RPC сказано, что он предназначен для PostgreSQL:

This #define should be used with the PostgreSQL ODBC driver. The driver returns as many row counts via SQLRowCount() calls as there are rows in a batch INSERT statement. The #define enables a loop that fetches all individual row counts and sums them up (+=). As a result, otl_stream::get_rpc() returns the total, which is correct for PostgreSQL. Normally, commercially available ODBC drivers return a single row count on a batch INSERT.

Тем не менее, определил макрос OTL_ODBC_ALTERNATE_RPC и все заработало!

Мораль сей басни – доступные исходники сторонних библиотек – это must have. :)