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


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

Герой этой истории — 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 системе. Каюсь, что не дотестировал сразу.

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

Комментарии (43)


  1. alxt
    26.12.2017 05:44

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


    1. alxt
      26.12.2017 05:44

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


    1. igor_suhorukov Автор
      26.12.2017 09:03

      Солидно! 7 лет


  1. Areso
    26.12.2017 06:29

    Считаю, что нужно отличать дебаг и последующий фикс и новую функциональность.
    Для первого две строчки за пару дней — это отличный результат. Хуже, когда на это уходит месяц.
    А вот новый функционал пишется гораздо проще и веселее, и быстрее (если уж считать в SLOC).


    1. igor_suhorukov Автор
      26.12.2017 09:05

      Согласен. Ещё рефакторинг — источник нового кода, почти как новая фича


      1. TheShock
        27.12.2017 01:36

        У нас был джун, он говорил на стендапах что-то вроде: «Я вчера рефакторил Редактор»
        Это означало, что он запилил пару новых фич в Редактор и ничего не рефакторил.


    1. igor_suhorukov Автор
      26.12.2017 09:14

      Ещё гораздо меньше строк пишется/генерируется IDE, если подключить к проекту lombok


      1. TheKnight
        26.12.2017 12:49

        Или Kotlin…
        Хотя в Kotlin (и Scala) пока нет удобных для Java аннотаций для логгирования

        @Log
        @Slf4j
        @Log4j
        @Log4j2


        1. igor_suhorukov Автор
          26.12.2017 13:26

          Или groovy)


        1. Borz
          26.12.2017 15:57

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


          1. TheKnight
            26.12.2017 16:10

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

            companion object: KLogging()

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


            1. dougrinch
              28.12.2017 00:18

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


              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, но неужели не пофиг?


              1. TheKnight
                28.12.2017 14:37

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


                1. Borz
                  28.12.2017 16:13
                  -1

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


                1. dougrinch
                  28.12.2017 17:58

                  Так наоборот же. Эту штуку надо написать всего один раз (и можно даже в библиотеке) и всё! Вам больше вообще ничего делать не надо. У всех классов уже есть логгер. В отличии от ломбока, в котором как раз надо еще целую аннотацию поставить.


                  1. TheKnight
                    28.12.2017 19:45

                    Моя тупить. Можно пример минипроекта на plain Kotlin (не Android, spring, webservices, буквально три-четыре простеньких класса) который работает с этим?


                    1. dougrinch
                      28.12.2017 21:03

                      Если честно, то лень. Но это и есть весь код. Суете в либу на топ уровне что я писал ранее, а используется просто как

                      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() }
                      (тоже на рутовом уровне), но здесь я пока не придумал как в теле геттера получать класс, откуда он вызывается. Каждый раз через исключение смотреть стектрейс — явно плохо.


                      1. dougrinch
                        28.12.2017 21:22

                        Ну да, иначе никак не получается сейчас

                        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 напрямую на нужный логгер, но в этом я пока не уверен, надо внимательнее посмотреть.


                        1. dougrinch
                          28.12.2017 21:57

                          Во!

                              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);
                              }


                          1. dougrinch
                            29.12.2017 04:34

                            А, блин, лоханулся я. Это же фича джава компилятора будет, так что надо еще допиливать котлиновский чтобы он про Intrinsics.invokedynamic тоже узнал. А если в любом случае надо его пилить, то и JEP-303 уже можно не ждать и сделать свой велосипед.


    1. norlin
      26.12.2017 12:04

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


      1. igor_suhorukov Автор
        26.12.2017 13:23

        Зависит от дизайна и техдолга существующего кода…


  1. PapaBubaDiop
    26.12.2017 11:47
    +1

    Была ошибка в 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 текстпад родной


    1. igor_suhorukov Автор
      26.12.2017 12:03

      Неделя на символ — это позабористее строчек!


    1. farwayer
      26.12.2017 21:50

      Когда «процессы» побеждают здравый смысл :(


    1. justboris
      27.12.2017 00:24

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


      1. PapaBubaDiop
        27.12.2017 00:56

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


        1. justboris
          27.12.2017 01:16

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


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


  1. m08pvv
    26.12.2017 12:11
    +1

    На тему обратной зависимости размера фикса от затраченного времени была отличная статья «The reverse correlation between size of change and length of investigation» в блоге Ayende Rahien (RavenDB).


    1. igor_suhorukov Автор
      26.12.2017 12:24

      Спасибо, интересно. Согласен с автором что race condition сложно воспроизвести


  1. progchip666
    26.12.2017 21:35
    +1

    Легко может. Особенно при программировании для встраиваемых систем, да ещё в случае многопроцессорных устройств! Когда аппаратные глюки накладываются на программные, причём в другом микроконтроллере.


    1. igor_suhorukov Автор
      26.12.2017 22:07

      Браво! Это вообще отдельная область разработки. Наводки, зависания из-за непропайки


      1. progchip666
        26.12.2017 22:26
        +3

        Чего только не случается. Непропайки, гонки фронтов. Последний раз провозился две недели потому что у микросхемы уважаемого производителя ADI почему то при понижении напряжения питания надо увеличивать время между записями в два регистра с миллисекунд буквально до 10 секунд. Без этого она не просто блокировала шину SPI и почему то начинала потреблять раз в 10 больше чем положено. В результате начинал греться стабилизатор и срабатывала тепловая защита. Попробуй разберись где там причина а где следствие.
        Но на хабре почему то программистов микроконтроллеров не считают разработчиками вообще, нас даже изгоняли с этой площадки года на два или три в недалёком прошлом.
        Наверно потому, что приравняли нас к ШАМАНАМ


        1. Jef239
          26.12.2017 23:18
          +1

          Угу, знакомо. Отладка кода осциллографом, отладка железа при помощи GDB… Правка бага в коде при помощи проводочка, правка ошибки в разводке кодом…

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


        1. igor_suhorukov Автор
          26.12.2017 23:44

          Видимо разработка специфичнее и требует большего количества знаний, документации больше, многое ценное под NDA и комьюнити меньше.


  1. Nick_mentat
    27.12.2017 15:06
    +1

    Одна строчка

    int x

    Съела отпуск. Убила сорсник.
    Когда-нибудь я допишу статью об этом…


    1. igor_suhorukov Автор
      27.12.2017 15:37

      Интрига!


    1. Comdiv
      27.12.2017 23:44

      Перекрытие имени в объемлющей области видимости? Бывает. Поэтому стоит включить соответствующую опцию в компиляторе.


      1. Nick_mentat
        28.12.2017 12:31

        Если быть точным — сознательное перекрытие областей, как хак решивший кучу проблем. Всё сломалось, когда гуру клинкода эту строчку удалил (она специально была написана в модуле где не используется).


        1. Comdiv
          28.12.2017 15:26
          +1

          Если это и решало кучу проблем, в конечном итоге такое хрупкое решение вряд ли можно признать хорошим. Поэтому не стоит винить «гуру клинкода», если только этот int x не был помечен очевидным и подробным комментарием.


          1. Nick_mentat
            29.12.2017 02:47

            //Do NOT touch! Very Important Code:
            int x 

            Позже пришлось добавить ещё и простыню текста снизу, поясняющую цель существования данного хака.


  1. Sellec
    27.12.2017 17:10
    +1

    Наверное, со своими пятью копейками про три дня ковыряния в коде DevExpress во время поиска причин непонятного поведения компонента WinForms после выхода новой версии с отсутствием документации по изменению этого поведения не стоит писать, потому что блог про OpenSource и Java? :)


    1. igor_suhorukov Автор
      27.12.2017 17:32

      Почему же… Боль-то одна — красноглазие. Не мне одному будет интересно.