Как изменение двух строк кода может занять несколько дней

    Интересно верит ли кто-либо еще что работу разработчика можно измерить количеством строк кода? Попробуем вместе развенчать этот старый, как мир, миф своими красными глазами.


    Сложно ли изменить две строчки кода?

    Герой этой истории — open source проект H2 database популярной реляционной базы данных для тестов, веб консоли для SQL и даже содержит внутри себя аналог LevelDB/Berkeley DB Java Edition/SQLite 3. Отличный проект, много раз использовал за свою практику и не было проблем. До тех пор пока не попытался использовать его совместно с redshift jdbc driver.

    Есть такая база данных в AWS, Redshift — форк времен PostgreSQL 8.0.2. Где-то в том же десятилетии появился и его конкурент greenplum-db… Несмотря на то что эта БД имеет массово-параллельную архитектуру, и прочие «плюшки» column-oriented DBMS, при работе с ней не покидает ощущение что ты в музее компьютерной истории. Понял, что это ощущение было неспроста, когда обнаружил что в приложении конфликтуют драйвера современного PostgreSQL 9.6 и Redshift драйвер ископаемого wire протокола postgresql 8.x.

    Обнаружил что используется PG wire protocol 8.x, когда подключался к PostgreSQL 9.6 в H2 web консоли. Результаты меня огорчили и я начал разбираться как же такое может происходить. Отладка привела в строчку получения соединения:

     DriverManager.getConnection(url, prop);

    Вроде бы все выглядит по спецификации, так как это не JNDI и не javax.sql.DataSource.

    Спускаемся глубже в DriverManager. Там все и так известно и ожидаемо. В его блоке статической инициализации используется ServiceLoader для загрузки реализующих java.sql.Driver и заявляющих об этом с помощью записи о реализации META-INF/services/java.sql.Driver в своем jar. Это достаточно давно отменило использование Class.forName(driver) — так все современные драйверы загружаются без этого ископаемого вызова. Ничего нового для меня тут нет.

    Драйверы при запросе соединения перебираются в порядке, как они зарегистрировались в поле registeredDrivers. DriverManager для каждого из них по цепочке вызывает driver.connect(url, info). Если конкретный драйвер вернул объект соединения с базой данных, возвращаем его из функции. Первый кто обработал connection URL из цепочки драйверов побеждает!

    Драйвер сам анализирует может ли он обработать подпротокол jdbc:subprotocol. На мою беду redshift jdbc драйвер обрабатывал кроме своего «redshift» еще и «postgresql», но с помощью древнего кода эпохи середины 2000х. Ясно, что connection url запрос никогда не дойдет до драйвера postgres 9.6.

    Один плюс в карму разработчикам redshift jdbc — спасибо хоть классы древней реализации PG в отдельный пакет спрятали, а не оставили конфликтовать с org.postgresql.Driver в jar hell. Попробовал использовать более «свежий» их драйвер, но он не работал внутри spring boot executable jar, так как в нем зависимости упаковали «матрешкой» — jar'ы зависимостей внутри jar драйвера.

    При этом пул соединений HikariCP правильно создает новый драйвер postgresql, в отличии от консоли H2. Раз уж пользователь указал driverClass, то он на нем и вызывает connect не полагаясь на DriverManager. Это работает в аду, учиненном redshift jdbc. Причина была найдена быстро и стало ясно как решать проблему.

    Патч был создан в выходной и отправлен как pull request в репозитарий проекта, заодно создал заявку об ошибке. После этого началась переписка и аргументация изменения для контрибьютора, второго по активности в репозитарии h2database. Выполнил все его требования и замечания для этого pull request и изменения приняли в основной код проекта. Ушло много свободного времени из-за двух строчек изменений и драйвера redshift. Но тут уже был азарт и дело принципа — выжить в мире где ископаемый протокол перекрывает современный. Спасибо ему за уделенное время, за то что вник в эту проблему. Верю что дотошность при приеме pull request в популярном open source проекте идет на пользу качеству. Прошло почти два выходных дня, пока две строчки для исправления бага появились в проекте.

    Другой pull request на новый функционал в schemaspy висит уже больше недели. Тут виноват сам, проблема что разрабатывал его на linux, а не работало на windows системе. Каюсь, что не дотестировал сразу.

    Делитесь про то, как несколько строк кода поглощали время. Есть интригующие истории и рассказы детективного жанра?
    Share post

    Similar posts

    Comments 44

      +10

      Несколько дней.
      Я хотел когда-то доработать что-то в hibernate, чтобы не писать лишнего при коннекте к firebird. Тогда у них был SVN а патчи надо было слать почтой. Отослал. 2 строки.
      Тот проект мой заглох, больше я с firebird не работал.
      Через 7 карт мне пришёл ответ, мол надо ж pull-request в github присылать. А я уже забыл, что и зачем делал…

        +4

        7 лет, конечно…

          +1
          Солидно! 7 лет
          +3
          Считаю, что нужно отличать дебаг и последующий фикс и новую функциональность.
          Для первого две строчки за пару дней — это отличный результат. Хуже, когда на это уходит месяц.
          А вот новый функционал пишется гораздо проще и веселее, и быстрее (если уж считать в SLOC).
            0
            Согласен. Ещё рефакторинг — источник нового кода, почти как новая фича
              0
              У нас был джун, он говорил на стендапах что-то вроде: «Я вчера рефакторил Редактор»
              Это означало, что он запилил пару новых фич в Редактор и ничего не рефакторил.
              0
              Ещё гораздо меньше строк пишется/генерируется IDE, если подключить к проекту lombok
                0
                Или Kotlin…
                Хотя в Kotlin (и Scala) пока нет удобных для Java аннотаций для логгирования
                @Log
                @Slf4j
                @Log4j
                @Log4j2
                  0
                  Или groovy)
                    0

                    зачем аннотация, когда есть kotlin-logging

                      +1
                      Потому что в Lombok используется аннотация? :) Искал решение похожее на то, что уже использовал.
                      Такой код:
                      companion object: KLogging()

                      выглядит длиннее чем одна коротка аннотация но лучше чем обычно.
                      val logger = LoggerFactory.getLogger("class name")
                        +1

                        А чем плох такой вариант?


                        val Any.logger: Logger by object : ReadOnlyProperty<Any, Logger> {
                            val loggers = ConcurrentHashMap<Class<*>, Logger>()
                            override fun getValue(thisRef: Any, property: KProperty<*>): Logger {
                                return loggers.computeIfAbsent(thisRef.javaClass, LoggerFactory::getLogger)
                            }
                        }

                        Да, не static final, но неужели не пофиг?

                          0
                          Вопрос в лаконичности.
                          Еще раз: в Lombok для создания объекта логгера используется одна короткая анннотация. В Kotlin это выглядит многословней. Вот вся претензия.
                            –1

                            экономия на спичках

                              0
                              Так наоборот же. Эту штуку надо написать всего один раз (и можно даже в библиотеке) и всё! Вам больше вообще ничего делать не надо. У всех классов уже есть логгер. В отличии от ломбока, в котором как раз надо еще целую аннотацию поставить.
                                0
                                Моя тупить. Можно пример минипроекта на plain Kotlin (не Android, spring, webservices, буквально три-четыре простеньких класса) который работает с этим?
                                  +1
                                  Если честно, то лень. Но это и есть весь код. Суете в либу на топ уровне что я писал ранее, а используется просто как
                                  import org.dou.lib.logger
                                  
                                  class Foo {
                                      init {
                                          logger.info("hello") //[main] INFO org.dou.Foo - hello
                                      }
                                  }
                                  


                                  На самом деле, я уже обнаружил две большие проблемы у этого варианта:
                                  1. Это публичное экстеншн проперти, так что я «logger» могу написать не только внутри класса, но и снаружи
                                  42.logger.info("msg") //[main] INFO java.lang.Integer - msg

                                  2. Он вообще не работает с топ левел функциями (вне класса/объекта). Т.к. там нет this, у которого logger и отрастает.

                                  В принципе, обе проблемы решаются если убрать «Any.» и сделать просто
                                  val logger: Logger get() { TODO() }
                                  (тоже на рутовом уровне), но здесь я пока не придумал как в теле геттера получать класс, откуда он вызывается. Каждый раз через исключение смотреть стектрейс — явно плохо.
                                    0
                                    Ну да, иначе никак не получается сейчас
                                    val public_strange_name_do_not_use_it = ConcurrentHashMap<String, Logger>()
                                    
                                    inline val logger: Logger
                                        get() {
                                            return public_strange_name_do_not_use_it.computeIfAbsent(Exception().stackTrace[0].className) { cn -> LoggerFactory.getLogger(Class.forName(cn)) }
                                        }


                                    По получаемому синтаксису вроде идеально получается, здесь только с перформансом уже проблемы скорее всего будут (по крайней мере, для библиотечной функции общего пользование такое явно недопустимо). В принципе, если дождаться JEP-303, то в паре с инлайном это будет идеальное решение. По идее, тогда даже мапа будет не нужна, т.к. можно будет повесить CallSite напрямую на нужный логгер, но в этом я пока не уверен, надо внимательнее посмотреть.
                                      0
                                      Во!
                                          public static CallSite bootstrapLoggerFactory(MethodHandles.Lookup lookup, String name, MethodType type) {
                                              Logger logger = LoggerFactory.getLogger(lookup.lookupClass());
                                              MethodHandle mh = MethodHandles.constant(Logger.class, logger);
                                              return new ConstantCallSite(mh);
                                          }
                                        0
                                        А, блин, лоханулся я. Это же фича джава компилятора будет, так что надо еще допиливать котлиновский чтобы он про Intrinsics.invokedynamic тоже узнал. А если в любом случае надо его пилить, то и JEP-303 уже можно не ждать и сделать свой велосипед.
                    +1

                    Когда новую функциональность надо вкорячить в существующий код – это тоже не всегда тривиально бывает.

                      0
                      Зависит от дизайна и техдолга существующего кода…
                    +5
                    Была ошибка в 1 символ (другая кавычка в JSON файле на стороне сервера)

                    1) создать стори в JIRA
                    2) создать бранч в git server project
                    3) создать бранч в git front project
                    4) исправить кавычку в server
                    5) добавить тест в front
                    6) прогнать все тесты
                    7) pull request
                    8) merge 1
                    9) merge 2
                    10) close story

                    Неделя. А фактической работы — 0.3 секунды.

                    И да — ту кавычку вставил MAC OS текстпад родной
                      0
                      Неделя на символ — это позабористее строчек!
                        +4
                        Когда «процессы» побеждают здравый смысл :(
                          0

                          Не очень понял, почему фикс делаем в "server project" а тест пишем во "front project". Было бы в одном месте, число действий сократилось бы вдвое.

                            0
                            UI Test на стороне телефона, который читает данные с сервера.
                              +2

                              Все равно неясно.


                              Если сервер возвращает JSON, который ломает клиента, то нужно либо пофиксить сервер (и написать в сервере тест "не должен возвращать плохой json"), либо научить клиента обрабатывать такую ситуацию (и написать в клиенте тест "не падать когда получил такой-то json").

                          +1
                          На тему обратной зависимости размера фикса от затраченного времени была отличная статья «The reverse correlation between size of change and length of investigation» в блоге Ayende Rahien (RavenDB).
                            0
                            Спасибо, интересно. Согласен с автором что race condition сложно воспроизвести
                            +4
                            Легко может. Особенно при программировании для встраиваемых систем, да ещё в случае многопроцессорных устройств! Когда аппаратные глюки накладываются на программные, причём в другом микроконтроллере.
                              0
                              Браво! Это вообще отдельная область разработки. Наводки, зависания из-за непропайки
                                +4
                                Чего только не случается. Непропайки, гонки фронтов. Последний раз провозился две недели потому что у микросхемы уважаемого производителя ADI почему то при понижении напряжения питания надо увеличивать время между записями в два регистра с миллисекунд буквально до 10 секунд. Без этого она не просто блокировала шину SPI и почему то начинала потреблять раз в 10 больше чем положено. В результате начинал греться стабилизатор и срабатывала тепловая защита. Попробуй разберись где там причина а где следствие.
                                Но на хабре почему то программистов микроконтроллеров не считают разработчиками вообще, нас даже изгоняли с этой площадки года на два или три в недалёком прошлом.
                                Наверно потому, что приравняли нас к ШАМАНАМ
                                  +3
                                  Угу, знакомо. Отладка кода осциллографом, отладка железа при помощи GDB… Правка бага в коде при помощи проводочка, правка ошибки в разводке кодом…

                                  Причем даже на довольно больших компах замашки embeded работают. Как понять, на какой скорости и в каком формате программа гонит данные по COM-порту? Берем осциллограф и смотрим… Это вот вчера было, когда некий древний linux при воде русских символов переключил формат консоли из 8N1 в 7N1. :-) Первая мысль — а давайте осциллографом глянем.
                                    0
                                    Видимо разработка специфичнее и требует большего количества знаний, документации больше, многое ценное под NDA и комьюнити меньше.
                                +1
                                Одна строчка
                                int x

                                Съела отпуск. Убила сорсник.
                                Когда-нибудь я допишу статью об этом…
                                  0
                                  Интрига!
                                    0
                                    Перекрытие имени в объемлющей области видимости? Бывает. Поэтому стоит включить соответствующую опцию в компиляторе.
                                      +1
                                      Если быть точным — сознательное перекрытие областей, как хак решивший кучу проблем. Всё сломалось, когда гуру клинкода эту строчку удалил (она специально была написана в модуле где не используется).
                                        +1
                                        Если это и решало кучу проблем, в конечном итоге такое хрупкое решение вряд ли можно признать хорошим. Поэтому не стоит винить «гуру клинкода», если только этот int x не был помечен очевидным и подробным комментарием.
                                          0
                                          //Do NOT touch! Very Important Code:
                                          int x 

                                          Позже пришлось добавить ещё и простыню текста снизу, поясняющую цель существования данного хака.
                                            +2
                                            Комментарий понятный, но не подробный, поэтому был больше похож на шутку. Очень важный код, серьёзно? Возможно, была бы та простыня текста там раньше, и не пришлось бы разгребать проблемы.
                                    +1
                                    Наверное, со своими пятью копейками про три дня ковыряния в коде DevExpress во время поиска причин непонятного поведения компонента WinForms после выхода новой версии с отсутствием документации по изменению этого поведения не стоит писать, потому что блог про OpenSource и Java? :)
                                      +1
                                      Почему же… Боль-то одна — красноглазие. Не мне одному будет интересно.

                                    Only users with full accounts can post comments. Log in, please.