• 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 и другие.

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

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

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

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

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

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

  • Картина с Лениным

    В одном из чатов скинули мем. Давайте проигнорируем текстовую часть и рассмотрим картину. Она хороша свой композицией, и сейчас я объясню почему.

    Композиция в живописи — это такое расположение объектов, при котором они согласованы и уравновешены. Согласованность означает правильное расположение в пространстве, непротиворечивость углов, света, геометрии. Уравновешенность означает, что на каждый ключевой объект приходится парный, противоположный ему в цвете, размере, динамике.

    Звучит непонятно, но вот несколько примеров. Центральный объект никогда не находится в центре картины, потому что иначе рушится композиция. Центр — это смерть композиции, ее тупик. Какой-то объект может быть в центре, но другие, более важные объекты, должны оттягивать на себя внимание зрителя. Большой темный объект можно уравновесить небольшим светлым с другой стороны. Статику уравновешивают движением. Например, когда герой заносит меч над чудовищем, его массу и статику уравновешивает тонкость и скорость меча.

    Теперь посмотрим на картину. Очевидно, Ленин — главный персонаж, но он находится не в центре, а левее. Напротив него юноша, с которым он ведет диалог. Юноша “подсвечен”, чтобы было понятно, к кому обращается Ленин. Их лица — ключевые объекты картины, а между ними — указательный палец Ленина. Именно палец находится в центре картины, как бы говоря: истина здесь. Таким образом палец и лица по обе стороны образуют центр композиции.

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

    Геометрия: обратите внимание на положение Ленина и зрителей. Ленин отделен от них небольшим коридорчиком, и в результате персонаж уравновешивает толпу. Если поделить картину ровно пополам, то получится один человек против многих. Это снова динамика, контраст величин. В целом толпа образует полукруг, все взгляды устремлены в центр — и композиция получается замкнутой.

    Наверное, кто-то лучше разбирается в живописи и видит больше деталей. Но мне хватает и этих. Не важно, какое у вас отношение к Ленину — полюбоваться картиной стоит.

    Полная версия картины:

    “В.И. Ленин среди делегатов III съезда РКСМ”. Белоусов П.П., холст, масло.

  • Теория Дарвина

    Теория Дарвина о происхождении видов считается главенствующей, занимает центральное место в учебниках. Но она так и не нашла поддержки в обществе. Как двести лет назад ее не принимали, так не принимают и сейчас.

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

    Забавляет, что когда спрашивают про теорию Дарвина, употребляют слово “веришь”. Ты веришь в теорию Дарвина? Для меня это звучит как “веришь в синус” или “веришь в производную.” Все это модели, работа которых доказана — как в них можно верить или не верить? Ну, не веришь ты в производную — это ей все равно, ровно как и теории Дарвина.

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

    Интересно, думал ли Дарвин о том, сколько времени уйдет на адаптацию общества к его теории? Сколько бы столетий он не прикинул — боюсь, он ошибался. Нужно еще.

  • Цветокоррекция

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

    Поставьте любой фильм на паузу и убедитесь, что картинка выглядит как сквозь бутылочное стекло.

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

    Я в курсе, зачем нужна коррекция, и я за то, когда ее применяют по делу. Например, в классической “Матрице”, когда действие происходит в программе, все зеленое. Когда герои перемещаются в реальный мир — цветное. Коррекцией выделяют особые моменты, например сны, кошмары или катастрофы. Коррекция нужна для компоузинга, чтобы актеры, фон и эффекты были согласованы.

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

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

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

    Замечаю такую же историю в играх. Запускаешь игрушку, пропускаешь логотипы и вжух — экран моргает, и накладывается глобальная коррекция цветов. Синий становится мокрым асфальтом, красный — бордовым, появляются серо-бурые оттенки. Если свернуть игру, можно наблюдать эффект в операционке. Опять же, зачем насильно глушить цвета? Кто просил?

    Коррекция нужна, но слишком часто я замечаю обратное.

  • Память у Андроида (3)

    И еще про Андроид. Я не хотел, но сама жизнь вынуждает.

    Этим утром обратилась женщина, друг семьи. Классика: телефон — Андроид, 32 гига, забит под ноль фотками и видео всяких утренников. Вацап раздулся до 8 гигов. Женщина жила в таком режиме годами, телефон как-то шевелился. Но вот приплыли: Вацап устарел, просит обновить. Женщина не понимает, что от нее хотят.

    Нажимаю “скачать”, Вацап перекидывает в Google Play. А там плашка: в целях безопасности подтвердите аккаунт, запрашивает пароль к гугло-почте. Разумеется, женщина не знает не то что пароль, а что такое гугло-аккаунт в принципе. Судя по всему, этот аккаунт ей создали в отделении связи, где она покупала телефон.

    Сразу вопрос: какие именно “цели безопасности” преследует Гугл? В чем проблема обновить приложение? Это же всем на пользу: Гуглу, производителю, пользователю.

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

    Ладно, качаю APK с сайта Вацапа. Разрешаю установку из сторонних источников. Тыкаю на APK — он немного тупит, потом говорит, что не смог. Никаких деталей, ошибок, все молчком. Я думал минут десять, ребутнул телефон и догадался: бедняге не хватает места. А сказать об этом нельзя? Удалил левые приложения, какой-то утренник — и APK поставился.

    Женщина видит Вацап и чуть не плачет от радости. У нее там родительский чат, секции внуков, подруги детства, соседи, ТСЖ, работа, клиенты. Предлагает деньги, еле отговорил. Представьте, что потеряли доступ к почте, на которой сидели 10 лет — вот так себя чувствовала она.

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

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

    Какой-то сплошной гиговский бред, и ничего больше.

  • PG2 release 0.1.18

    PG2 version 0.1.18 is available (it’s a client for Postgres). This release brings two major features:

    • built-in pgvector extension support;
    • better type mapping between Postgres and Clojure.

    PGVector Support

    Pgvector is a well known extension for PostgreSQL. It provides a fast and robust vector type which is quite useful for heavy computations. Pgvector also provides a sparse version of a vector to save space.

    This section covers how to use types provided by the extension with PG2.

    Vector

    First, install pgvector as the official readme file prescribes. Now that you have it installed, try a simple table with the vector column:

    (def conn
      (jdbc/get-connection {...}))
    
    (pg/query conn "create temp table test (id int, items vector)")
    
    (pg/execute conn "insert into test values (1, '[1,2,3]')")
    (pg/execute conn "insert into test values (2, '[1,2,3,4,5]')")
    
    (pg/execute conn "select * from test order by id")
    
    ;; [{:id 1, :items "[1,2,3]"} {:id 2, :items "[1,2,3,4,5]"}]
    

    It works, but we got the result unparsed: the :items field in each row is a string. This is because, to take a custom type into account when encoding and decoding data, you need to specify something. Namely, pass the :with-pgvector? flag to the config map as follows:

    (def config
      {:host "127.0.0.1"
       :port 5432
       :user "test"
       :password "test"
       :database "test"
       :with-pgvector? true})
    
    (def conn
      (jdbc/get-connection config))
    

    Now the strings are parsed into a Clojure vector of double values:

    (pg/execute conn "select * from test order by id")
    
    [{:id 1, :items [1.0 2.0 3.0]}
     {:id 2, :items [1.0 2.0 3.0 4.0 5.0]}]
    

    To insert a vector, pass it as a Clojure vector as well:

    (pg/execute conn "insert into test values ($1, $2)"
                {:params [3 [1 2 3 4 5]]})
    

    It can be also a lazy collection of numbers produced by a map call:

    (pg/execute conn "insert into test values ($1, $2)"
                {:params [4 (map inc [1 2 3 4 5])]})
    

    The vector column above doesn’t have an explicit size. Thus, vectors of any size can be stored in that column. You can limit the size by providing it in parentheses:

    (pg/query conn "create temp table test2 (id int, items vector(5))")
    

    Now if you pass a vector of a different size, you’ll get an error response from the database:

    (pg/execute conn "insert into test2 values (1, '[1,2,3]')")
    
    ;; Server error response: {severity=ERROR, code=22000, file=vector.c, line=77,
    ;; function=CheckExpectedDim, message=expected 5 dimensions, not 3,
    ;; verbosity=ERROR}
    

    The vector type supports both text and binary modes of PostgreSQL wire protocol.

    Sparse Vector

    The pgvector extension provides a special sparsevec type to store vectors where only certain elements are filled. All the rest elements are considered as zero. For example, you have a vector of 1000 items where the 3rd item is 42.001, and 10th item is 99.123. Storing it as a native vector of 1000 double numbers is inefficient. It can be written as follows which takes much less:

    {3:42.001,10:99.123}/1000
    

    The sparsevec Postgres type acts exactly like this: internally, it’s a sort of a map that stores the size (1000) and the {index -> value} mapping. An important note is that indexes are counted from one, not zero (see the README.md file of the extension for details).

    PG2 provides a special wrapper for a sparse vector. A brief demo:

    (pg/execute conn "create temp table test3 (id int, v sparsevec)")
    
    (pg/execute conn "insert into test3 values (1, '{2:42.00001,7:99.00009}/9')")
    
    (pg/execute conn "select * from test3")
    
    ;; [{:v <SparseVector {2:42.00001,7:99.00009}/9>, :id 1}]
    

    The v field above is an instance of the org.pg.type.SparseVector class. Let’s look at it closer:

    ;; put it into a separate variable
    (def -sv
      (-> (pg/execute conn "select * from test3")
          first
          :v))
    
    (type -sv)
    
    org.pg.type.SparseVector
    

    The -sv value has a number of interesting traits. To turn in into a native Clojure map, just deref it:

    @-sv
    
    {:nnz 2, :index {1 42.00001, 6 99.00009}, :dim 9}
    

    It mimics the nth access as the standard Clojure vector does:

    (nth -sv 0) ;; 0.0
    (nth -sv 1) ;; 42.00001
    (nth -sv 2) ;; 0.0
    

    To turn in into a native vector, just pass it into the vec function:

    (vec -sv)
    
    [0.0 42.00001 0.0 0.0 0.0 0.0 99.00009 0.0 0.0]
    

    There are several ways you can insert a sparse vector into the database. First, pass an ordinary vector:

    (pg/execute conn "insert into test3 values ($1, $2)"
                {:params [2 [5 2 6 0 2 5 0 0]]})
    

    Internally, zero values get eliminated, and the vector is transformed into a SparseVector instance. Now read it back:

    (pg/execute conn "select * from test3 where id = 2")
    
    [{:v <SparseVector {1:5.0,2:2.0,3:6.0,5:2.0,6:5.0}/8>, :id 2}]
    

    The second way is to pass a SparseVector instance produced by the pg.type/->sparse-vector function. It accepts the size of the vector and a mapping of {index => value}:

    (require '[pg.type :as t])
    
    (pg/execute conn "insert into test3 values ($1, $2)"
                {:params [3 (t/->sparse-vector 9 {0 523.23423
                                                  7 623.52346})]})
    

    Finally, you can pass a string representation of a sparse vector:

    (pg/execute conn "insert into test3 values ($1, $2)"
                {:params [3 "{1:5.0,2:2.0,3:6.0,5:2.0,6:5.0}/8"]})
    

    Like the vector type, sparsevec can be also limited to a certain size:

    create table ... (id int, items sparsevec(5))
    

    The sparsevec type supports both binary and text Postgres wire protocol.

    Custom Schemas

    The text above assumes you have the pgvector extension installed globally meaning it is hosted in the public schema. Sometimes though, extensions are setup per schema. For example only a schema named sales has access to the pgvector extension but nobody else.

    If it’s your case and you installed pgvector into a certain schema, the standard :with-pgvector? flag won’t work. By default, PG2 scans the pg_types table for the public.vector and public.sparsevec types. Since the schema name is not public but sales, you need to specify it by passing a special option called :type-map. It’s a map where keys are fully qualified type names (either a keyword or a string), and values are predefined instances of the IProcessor interface:

    (def config
      {:host "127.0.0.1"
       :port 5432
       :user "test"
       :password "test"
       :database "test"
       :type-map {"sales.vector" t/vector
                  "sales.sparsevec" t/sparsevec}})
    

    You can rely on keywords as well:

    (def config
      {:host "127.0.0.1"
       :port 5432
       :user "test"
       :password "test"
       :database "test"
       :type-map {:sales/vector t/vector
                  :sales/sparsevec t/sparsevec}})
    

    The t alias references the pg.type namespace.

    Now if you install the extension into the statistics schema as well, add it into the map:

    (def config
      {:host "127.0.0.1"
       :port 5432
       :user "test"
       :password "test"
       :database "test"
       :type-map {:sales/vector t/vector
                  :sales/sparsevec t/sparsevec
                  :statistics/vector t/vector
                  :statistics/sparsevec t/sparsevec}})
    

    Should you make a mistake in a fully qualified type name, it will be ignored, and you’ll get value from the database unparsed. The actual value depends on the binary encoding and decoding options of a connection. By default, it uses text protocol so you’ll get a string like “[1, 2, 3]”. For binary encoding and decoding, you’ll get a byte array that holds raw Postgres payload.

    Custom Type Processors

    PG2 version 0.1.18 has the entire type system refactored. It introduces a conception of type processors which allows to connect Postgres types with Java/Clojure ones with ease.

    When reading data from Postgres, the client knows only the OID of a type of a column. This OID is just an integer number points to a certain type. The default builtin types are hard-coded in Postgres, and thus their OIDs are known in advance.

    Say, it’s for sure that the int4 type has OID 23, and text has OID 25. That’s true for any Postgres installation. Any Postgres client has a kind of a hash map or a Enum class with these OIDs.

    Things get worse when you define custom types. These might be either enums or complex types defined by extensions: pgvector, postgis and so on. You cannot guess OIDs of types any longer because they are generated in runtime. Their actual values depend on a specific machine. On prod, the public.vector type has OID 10541, on pre-prod it’s 9621, and in Docker you’ll get 1523.

    Moreover, a type name is unique only across a schema that’s holding it. You can easily have two different enum types called status defined in various schemas. Thus, relying on a type name is not a good option unless it’s fully qualified.

    To deal with all said above, a new conception of type mapping was introduced.

    First, if a certain OID is builtin (meaning it exists the list of predefined OIDs), it gets processed as before.

    When you connect to a database, you can pass a mapping like {schema.typename => Processor}. When pg2 has established a connection, it executes an internal query to discover type mapping. Namely, it reads the pg_type table to get OIDs that have provided schemas and type name. The query looks like this:

    select
        pg_type.oid, pg_namespace.nspname || '.' || pg_type.typname as type
    from
        pg_type, pg_namespace
    where
        pg_type.typnamespace = pg_namespace.oid
        and pg_namespace.nspname || '.' || pg_type.typname in (
            'schema1.type1',
            'schema2.type2',
            ...
        );
    

    It returns pairs of OID and the full type name:

    121512 | schema1.type1
     21234 | schema2.type2
    

    Now PG2 knows that the OID 121512 specifies schema1.type1 but nothing else.

    Finally, from the map {schema.typename => Processor} you submitted before, PG2 builds a map {OID => Processor}. If the OID is not a default one, it checks this map trying to find a processor object.

    A processor object is an instance of the org.pg.processor.IProcessor interface, or, if more precisely, an abstract AProcessor which is partially implemented. It has four methods:

    ByteBuffer encodeBin(Object value,  CodecParams codecParams);
        String encodeTxt(Object value,  CodecParams codecParams);
        Object decodeBin(ByteBuffer bb, CodecParams codecParams);
        Object decodeTxt(String text,   CodecParams codecParams);
    

    Depending on whether you’re decoding (reading) the data or encoding them (e.g. passing parameters), and the current format (text or binary), a corresponding method is called. By extending all four methods, you can handle any type you want.

    At the moment, there are about 25 processors implementing standard types: int2, int4, text, float4, and so on. Find them in the pg-core/src/java/org/pg/processor directory. There is also a couple of processors for the pgvector extension in the pgvector subdirectory.

    The next step is to implement processors for the postgis extension.

  • Память у Андроида (2)

    Продолжение прошлой заметки про Андроид.

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

    Человек запускает менеджер очистки, но это сплошное издевательство. Менеджер говорит: у тебя занято 20 гигов фотками и видео, удаляй сам. А так я могу очистить временные файлы на сумму 30 мегабайтов.

    Я проходил это десять раз, не меньше. Покупаешь телефон маме, жене, детям, родственникам — и через полгода они приходят к тебе с вопросом “закончилось место”. Ты скидываешь фотки на комп, и на год тебя оставляют в покое.

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

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

    Когда я на это жалуюсь, мне говорят — что ты хотел? Тут ничего не поделаешь. А на самом деле поделать можно много чего.

    Заботу о диске должна брать на себя операционная система. Во-первых, если свободного места меньше 70%, то старые фотки уменьшаются в разрешении. Современные телефоны производят джипеги по 3-7 магабайтов — такие фотографии можно печатать в натуральный рост. Это, мягко говоря, избыточно для экрана размером с ладонь. Когда места перестает хватать, старые фотки сжимаются до 700 килобайт, освобождая от 2.5 до 5 магабайтов. Сто фоток — полгига.

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

    То же самое с видео: если сейчас ночь, и заряд батареи выше порога, то берется старое видео и перегоняется в низкое разрешение. Как и в случае с джипегами, на экране размером в ладонь никто не заметит разницы, а это дает 200-300 мегов с каждого ролика.

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

    Вы скажете, что никто не будет этого делать. А между прочим, это в интересах самих приложений. Потому что если на телефоне 2% свободного места, то пользоваться условным Телеграмом невозможно. А если каждое приложение освободит по 200-400 мегабайтов, то в сумме будет пара гигов, и телефон худо-бедно заработает.

    Наконец, самое важное — старые данные можно удалять без спроса пользователя. Да, вот так просто взять и удалить. Объяснение этому простое: представьте, что человек собрался в компании и хочет снять видео. А места нет. И у нас выбор: либо сказать, что места нет и заставить чистить файлы самому, либо удалить фотографии пятилетней давности, которые человек ни разу не открывал. Что лучше?

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

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

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

    Допускаю, что есть программы, которые умеют что-то подобное, но это сторонние программы, их нужно ставить, покупать, разбираться с ними. Нет из коробки — нет как такового. А еще телефон, который вместо двух лет прослужил бы пять, мягко говоря, не согласуется с бизнес-моделью всяких Гуглов, Ксяоми и прочих.

    Хочу лишь показать — многие вещи можно переосмыслить, и технически они возможны. Дело в бизнесе и отношении к ним.

  • Память у Андроида (1)

    В Андроидах — я имею в виду телефоны — мне не нравится одна вещь, которая там с рождения. Это вечная борьба с тем, кто и куда пишет: во внутреннюю память или на сд-карту.

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

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

    Разумеется, в Андроиде нет централизованной настройки, что и куда писать. Нужно открыть камеру, многоточие, гайки и там выбрать SD-карту. Приложения настраиваются отдельно — то ли в Google Play, то ли еще где. Перенос приложений между памятью и картой — это кровавая боль. Нельзя выделить и перенести сразу десять приложений. Нужно вручную останавливать каждое и переносить.

    Пишу это, потому что наболело. Жена пожаловалась, что телефон Xiaomi тормозит. В нем 32 гига памяти, и дополнительно я вставил карту на 32 гига. Везде указал приоритет записи на карту. И что вы думаете? Каким-то образом настройки слетели, и память оказалась забита видео и фотками под ноль. На карте 20 гиг свободного места, а телефон едва ворочается. Открывает менеджер диска, запускает сканирование и говорит: могу освободить аж 30 мегабайтов за счет каких-то там темповых файлов.

    Просто какая-то дичь. Что мешает переместить фотки на SD-карту в фоне? Пользователь ничего не заметит. У нас везде ИИ, всякие модели, предсказание фраз и покупок — и блин, телефон не может перетащить фотки. Он будет тормозить, тупить, но ни в коем случае не решит проблему.

    Это беда каждого Андроида: за пять тысяч рублей, за пятьдесят, за сто пятьдесят. Она была пятнадцать лет назад, когда я купил первый смарт на втором Андроиде. Она есть сейчас, она будет еще через пятнадцать лет.

    Я согласен, что иметь память и съемную карту удобно в техническом плане. Но на практике это напоминает Линукс: вы можете настроить что угодно, и поэтому всегда будете настраивать все, что угодно.

    Продолжение

Страница 1 из 85