• Приглашаю на митап

    В следующий четверг (28 ноября) Health Samurai проводят очередной кложурный митап. Я буду рассказывать про свою библиотеку PG2 — это которая клиент к Постгресу. Начало в 19:00, нужна регистрация. Ссылка на страницу организаторов.

    Из анонса может показаться, что доклад будет про библиотеку, но на самом деле я бы хотел поговорить о другом. Пока я работал над ней, то столкнулся со спецификой, о которой раньше не думал, и теперь хочу ей поделиться. Кроме самой библиотеки, расскажу о следующих вещах:

    • как устроен Postgres Wire Protocol
    • зачем браться за свое решение, когда есть другие, проверенные годами
    • сопоставление типов Postgresql и Java/Clоjure
    • заморочки с парсингом
    • мысли об API и дизайне

    Подойдет всем, кто как-то связан базами данных. Запись на Ютубе будет через несколько дней после митапа.

  • Можешь поправить

    Есть одна обидная вещь, я называю ее “можешь поправить”. Это когда находишь ошибку, которой много лет и которую воспринимают как данность. И когда обращаешь внимание, тебе отвечают — можешь поправить.

    Текст ошибки совершенно нечитаем? Можешь поправить. Сервис делает тысячу запросов вместо одного? Можешь поправить. Хрупкие билды? Можешь поправить. Не собирается под твоей операционкой? Можешь поправить. Не работает в Фаерфоксе? Можешь поправить.

    Ну вы поняли: “можешь поправить” — это вежливая форма “е…сь сам”. Ощущение, что на полу лежит какашка, и все старательно ее обходят, дожидаясь, кто вступит первым. Как правило, первым вступаю я.

    Повторюсь, это очень обидная вещь, и вдобавок тревожный маркер о состоянии команды. В данном случае нужно править не только технические косяки, но и отношение к ним в команде.

  • Пробел в урлах

    Хотя интернету 55 лет, мы до сих пор не починили пробел в урлах.

    На выходных была ситуация: упали крон-джобы, и вообще произошла какая-то фигня. Файлы в S3 есть, а клиенты жалуются, что нету.

    Смотрю — кто-то поменял название папки в S3. Раньше было Daily_Reports, а теперь Daily Reports (с пробелом). Половина клиентов пишет файлы нормально. Но есть другие клиенты на питоне и баше, которые кодируют урлы дважды. В результате Daily Reports становится новой папкой Daily%20Reports в S3. Один клиент пишет в Daily Reports и ему ок. Второй клиент ищет файлы в Daily%20Reports, не находит и падает.

    Увы, мой быдлокод тоже упал. У меня такая задача: прилетает S3-урл вида

    s3://some.bucket.com/path/to/file.txt
    

    и мне нужно вытащить из него бакет, в данном случае хост. Делаю так:

    (-> s3-url java.net.URI. .getHost)
    

    Но когда в урле оказался пробел, класс URI валится — вай, не по стандарту, не знаю-не могу. Поменял на класс URL — он парсит урлы с пробелами нормально, но теперь ему не нравится схема s3:// — опять не по стандарту. Сделал так: беру урл, меняю схему автозаменой на http://, оборачиваю в URL и достаю хост. Обмазал тестами.

    “Какая шаткая система, если её может разрушить пригоршня ягод!” (с)

  • О переменных среды

    Про переменные среды нужно знать две вещи. Первая — если не было значения по умолчанию, функция должна бросить исключение, где написано, какой именно переменной не нашлось. Вторая — нельзя читать переменные посреди программы. Нужно сделать это один раз на старте. Эти два правила улучшают код на порядок.

    Теперь подробнее. Как правило, если переменная среды не установлена, то попытка ее прочесть вернет пустую строку или null. Это неправильно. Должно выскочить исключение с примерно таким текстом:

    System.getenv("DB_PASSWORD") =>
    RuntimeException "env variable DB_PASSWORD is not set"
    

    Если вместо исключения будет null, то он провалится в дальнейшие вычисления, например, в формирование урла или бакета. Постоянно вижу такое в коде:

    host = "api." + getenv("ENV_PREFIX") + ".acme.com"
    

    Если ENV_PREFIX не задан, то получится api.null.acme.com. Из-за этого HTTP-клиент пойдет на левый хост и кинет непонятное исключение. То же самое с бакетом в S3: Амазон скажет, мол, нет такого бакета, а вы будете рвать волосы.

    Простой фикс — написать свою функцию, которая бросит исключение. Потом заменить все коробочные getenv на ваш. Пример:

    (dеfn env!
      ([varname]
       (or (System/getenv varname)
           (throw (new RuntimeError ...))))
      ([varname dеfault]
       (or (System/getenv varname)
           dеfault)))
    

    Исключение не кидается, если передан дефолт.

    Второе. Чтение переменной среды — это грязная операция. Формально никакого IO не происходит, потому что переменные уже в памяти программы. Но это сторонняя зависимость, которую ваш код не контролирует. По факту чтение переменной не отличается от чтения файла. Нет файла — программа сломается; нет переменной — тоже.

    Поэтому, если уж вы связались с переменными среды, читайте их на старте в какую-то мапку. Передавайте эту мапку в функции, чтобы они были чистыми. Это легко тестировать: подал на вход то, это, пятое-десятое. А когда код завязан на getenv, с ним невозможно работать.

    Приходилось работать в проекте, который писали одни чудики. Они прочитали The Twelve-Factor App и особенно прониклись пунктом насчет переменных среды. Результат можно описать одним словом: пи…ц. Представьте проект на 600 файлов, где на каждый чих читается переменная среды, да к тому же приводится к нужному типу. На старте ничего не проверяется: запустил код без переменной — узнал об этом в продакшене. Какой-то чел добавил глобальный кэш переменных и целый ворох связанных с ним проблем.

    Чудики вынесли настройки в переменные среды, чтобы быть свободными от конфигурации. Так им сказали в The Twelve-Factor App. А потом написали ENV-файлы на три экрана. Было несколько ENV-файлов, которые загружались в особом порядке, переопределяя значения друг друга. Например, сначала базовый энв, потом энв текущего окружения (тест, прод, стейджинг), потому энв текущей машины. Удачной отладки.

    Из этого вывод: много чего можно прочитать в интернете, но если нет своей головы, оно не поможет. Нужно делать так, чтобы было удобно, а не как написано в The Twelve-Factor App или на Хакер-Ньюз.

  • Лучший язык

    По мотивам обсуждений во внутреннем чатике, вот вам мысль.

    Лучший язык для решения задачи — тот, который знаешь лучше всего. Если это Питон — пиши на Питоне. Если лучше всего знаешь Джаву — пиши на Джаве. Если это JS или PHP, то что ж… пиши на них.

    Когда вам говорят, что C++ лучше подходит для игр, а R для статистики, не нужно вестись на эту удочку. Язык-то задаче подходит, а вы подходите языку?

    Даже если взять “подходящий” язык, который вы плохо знаете, результат будет хуже, чем с “неподходящим”, но знакомым языком. Под “знаете” имеются в виду не основы синтаксиса и курсы, а пять лет опыта и больше.

    Простыми словами, если с Питоном вы 10 лет, а на плюсах писали дай бог в универе, то умножение матриц на Питоне будет медленней, но понятней и проще в поддержке.

    Игры, кстати, хорошо пишутся на C-шарпе в том же Юнити. Полно печатных плат, которые выполняют быдлокод на Питоне и JS. Если вы знаете Лисп как бог, то не составит труда написать мини-язык, который собирается в машкоды — так писали серию игр Crash Bandicoot. В случае с Питоном почти все либы написаны на Си, а Питон — это клей, чтобы их вызвать.

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

  • AI и новички

    В Кложурной слаке в канале для офтопа пошла речь о языковых моделях. Приведу хорошую цитату оттуда:

    Library suggestions seems to be one of the areas they’re very weak: they hallucinate a lot about namespaces and functions so the suggested code looks feasible but won’t run, and it actually leads to beginners then raising issues against the suggested library that “gpt suggested this code but it doesn’t work as expected”

    Прямо в точку. У меня было несколько раз, когда человек копировал выхлоп из GPT и жаловался, что он не работает. На первый взгляд код правильный, а потом смотришь: здесь нет двоеточия, здесь левый символ, здесь ссылка на неизвестный неймспейс. Между кодом — повествовательный стиль изложения: вы можете сделать то, можете это.

    Досадно, что каждый раз человек сливается. Начинаешь объяснять, что не так, а в ответ — тишина. Видимо, человек рассчитывал, что GPT прав, ошибка на моей стороне и сейчас я быстро поправлю. Но оказывается не так, нужно думать, разбираться и вообще — все сложно.

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

  • Всюду AI

    Наиболее раздражающий тренд сегодня — это когда каждая программа подсовывает AI. Открываешь гугло-док, а сверху плашка: попробуй Gemini at no cost. Открываешь Feedly — а там AI-анализатор твоих фидов. Открываешь Хром — а там AI-алгоритм для группировки вкладок.

    Все это в выпадашках, выпадашках, выпадашках. Даже если закрыл, покажут через два дня опять.

    Когда-нибудь AI-истерия, конечно, пройдет. Просто иной раз думаешь — почему я опять должен пережидать очередной вирусняк? Ждать, пока программы отпустит от блокчейна, BLM, прайда и прочего? Я ведь не просил, оно само пришло.

  • Ring JDK Adapter

    Ring JDK Adapter is a small wrapper on top of a built-in HTTP server available in Java. It’s like Jetty but has no dependencies. It’s almost as fast as Jetty, too (see benchmars below).

    Why

    Sometimes you want a local HTTP server in Clojure, e.g. for testing or mocking purposes. There is a number of adapters for Ring but all of them rely on third party servers like Jetty, Undertow, etc. Running them means to fetch plenty of dependencies. This is tolerable to some extent, yet sometimes you really want something quick and simple.

    Since version 9 or 11 (I don’t remember for sure), Java ships its own HTTP server. The package name is com.sun.net.httpserver and the module name is jdk.httpserver. The library provides an adapter to serve Ring handlers. It’s completely free from any dependencies.

    Ring JDK Adapter is a great choice for local HTTP stubs or mock services that mimic HTTP services. Despite some people think it’s for development purposes only, the server is pretty fast! One can use it even in production.

    Availability

    It’s worth mentioning that some Java installations may miss the jdk.httpserver module. Please ensure the JVM you’re using in production supports it first. Check out the following links:

    Installation

    ;; lein
    [com.github.igrishaev/ring-jdk-adapter "0.1.0"]
    
    ;; deps
    com.github.igrishaev/ring-jdk-adapter {:mvn/version "0.1.0"}
    

    Requires Java version at least 16, Clojure at least 1.8.0.

    Quick Demo

    Import the namespace, declare a Ring handler as usual:

    (ns demo
      (:require
       [ring.adapter.jdk :as jdk]))
    
    (defn handler [request]
      {:status 200
       :headers {"Content-Type" "text/plain"}
       :body "Hello world!"})
    

    Pass it into the server function and check the http://127.0.0.1:8082 page in your browser:

    (def server
      (jdk/server handler {:port 8082}))
    

    The server function returns an instance of the Server class. To stop it, pass the result into the jdk/stop or jdk/close functions:

    (jdk/stop server)
    

    Since the Server class implements AutoCloseable interface, it’s compatible with the with-open macro:

    (with-open [server (jdk/server handler opt?)]
      ...)
    

    The server gets closed once you’ve exited the macro. Here is a similar with-server macro which acts the same:

    (jdk/with-server [handler opt?]
      ...)
    

    Parameters

    The server function and the with-server macro accept the second optional map of the parameters:

    Name Default Description
    :host 127.0.0.1 Host name to listen
    :port 8080 Port to listen
    :stop-delay-sec 0 How many seconds to wait when stopping the server
    :root-path / A path to mount the handler
    :threads 0 Amount of CPU threads. When > thn 0, a new FixedThreadPool executor is used
    :executor null A custom instance of Executor. Might be a virtual executor as well
    :socket-backlog 0 A numeric value passed into the HttpServer.create method

    Example:

    (def server
      (jdk/server handler
                  {:host "0.0.0.0" ;; listen all addresses
                   :port 8800      ;; a custom port
                   :threads 8      ;; use custom fixed trhead executor
                   :root-path "/my/app"}))
    

    When run, the handler above is be available by the address http://127.0.0.1:8800/my/app in the browser.

    Body Type

    JDK adapter supports the following response :body types:

    • java.lang.String
    • java.io.InputStream
    • java.io.File
    • java.lang.Iterable<?> (see below)
    • null (nothing gets sent)

    When the body is Iterable (might be a lazy seq as well), every item is sent as a string in UTF-8 encoding. Null values are skipped.

    Middleware

    To gain all the power of Ring (parsed parameters, JSON, sessions, etc), wrap your handler with the standard middleware:

    (ns demo
      (:require
        [ring.middleware.params :refer [wrap-params]]
        [ring.middleware.keyword-params :refer [wrap-keyword-params]]
        [ring.middleware.multipart-params :refer [wrap-multipart-params]]))
    
    (let [handler (-> handler
                      wrap-keyword-params
                      wrap-params
                      wrap-multipart-params)]
      (jdk/server handler {:port 8082}))
    

    The wrapped handler will receive a request map with parsed :query-params, :form-params, and :params fields. These middleware come from the ring-core library which you need to add into your dependencies. The same applies to handling JSON and the ring-json library.

    Exception Handling

    If something gets wrong while handling a request, you’ll get a plain text page with a short message and a stack trace:

    (defn handler [request]
      (/ 0 0) ;; !
      {:status 200
       :headers {"Content-Type" "text/plain"}
       :body "hello"})
    

    This is what you’ll get in the browser:

    failed to execute ring handler
    java.lang.ArithmeticException: Divide by zero
    	at clojure.lang.Numbers.divide(Numbers.java:190)
    	at clojure.lang.Numbers.divide(Numbers.java:3911)
    	at bench$handler.invokeStatic(form-init14855917186251843338.clj:8)
    	at bench$handler.invoke(form-init14855917186251843338.clj:7)
    	at ring.adapter.jdk.Handler.handle(Handler.java:112)
    	at jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)
    	at jdk.httpserver/sun.net.httpserver.AuthFilter.doFilter(AuthFilter.java:82)
    	at jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:101)
    	at jdk.httpserver/sun.net.httpserver.ServerImpl$Exchange$LinkHandler.handle(ServerImpl.java:873)
    	at jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98)
    	at jdk.httpserver/sun.net.httpserver.ServerImpl$Exchange.run(ServerImpl.java:849)
    	at jdk.httpserver/sun.net.httpserver.ServerImpl$DefaultExecutor.execute(ServerImpl.java:204)
    	at jdk.httpserver/sun.net.httpserver.ServerImpl$Dispatcher.handle(ServerImpl.java:567)
    	at jdk.httpserver/sun.net.httpserver.ServerImpl$Dispatcher.run(ServerImpl.java:532)
    	at java.base/java.lang.Thread.run(Thread.java:1575)
    

    To prevent this data from being leaked to the client, use your own wrap-exception middleware, something like this:

    (defn wrap-exception [handler]
      (fn [request]
        (try
          (handler request)
          (catch Exception e
            (log/errorf e ...)
            {:status 500
             :headers {...}
             :body "No cigar! Roll again!"}))))
    

    Benchmarks

    As mentioned above, the JDK server although though is for dev purposes only, is not so bad! The chart below proves it’s almost as fast as Jetty. There are five attempts of ab -l -n 1000 -c 50 ... made against both Jetty and JDK servers (1000 requests in total, 50 parallel). The levels of RPS are pretty equal: about 12-13K requests per second.

    Measured on Macbook M3 Pro 32Gb, default settings, the same REPL.

  • Python Software Foundation

    По мотивам событий с Линуксом обратим внимание на Питон. Итак, имеем Python Software Foundation, некоммерческая организация, у которой на уме только добро и радуга.

    Смотрим: штаб-квартира в США, адрес Wilmington, Delaware, United States.

    Все руководители из США: на странице персонала выделяем любое имя, копируем в Гугл и добавляем “linkedin”. Страну видно даже без перехода в профиль.

    Ключевые спонсоры: NVidia, Bloomberg, Microsoft, AWS, Red Hat и другие.

    Любой суд признает Питон цифровым продуктом США со всем последствиями. Не то чтобы я об этом волнуюсь, но нужно иметь в виду: в любой момент разговоры про опенсорс закончатся, и подключатся юристы.

  • Почтовые рассылки

    Когда читаю рассылки Линукса или Постгреса, убеждаюсь: можно быть крутым разработчиком, но не уметь пользоваться почтой. Напомню главный критерий: никогда не цитировать все письмо. Нужно цитировать только то, на что конкретно отвечаешь или то, что было давно и теперь долго искать.

    Цитирование всего письма — это засорение переписки и визуальный шум. Бывает, человек отвечает двумя предложениями, а за ними борода цитат на два экрана. Обязательно найдется чудак с консольным клиентом, который сломает цитирование левым тегом, и оно не будет распознаваться другими клиентами.

    Наверное, на десять человек приходится один, у которого почта настроена нормально. Когда я смотрю на это, думаю: ты хоть видел со стороны, как выглядит переписка? В чем сложность зайти в опции и снять флажок, чтобы при ответе письмо было пустым? Сегодня даже самый кривой почтовый клиент выстраивает цепочки писем.

    Перечитать: как пользоваться почтой.

Страница 8 из 93