Table of Content

In the previous post, I was measuring bare query/execute/copy functions of the library. Although it’s useful, it doesn’t render the whole picture because it’s unclear how an application will benefit from faster DB access.

This post covers the second group of benchmarks I made: a simple HTTP server that reads random data from the database and responds with JSON. The benchmark compares PG2 and Next.JDBC as before.

Introduction

Some general notes: the server uses Ring Jetty version 1.7.1, JVM 21, Ring-JSON 0.5.1 for middleware. The handles are synchronous. The application uses connection pools for both libraries, and the pools are opened in advance when the server gets started. The maximum allowed pool size is 64. HTTP requests are sent by the ab utility with different -n and -c keys; the -l flag is always sent.

The full source code of the benchmark can be found in the repository.

Here are the most important fragments of code. Depending on what type argument is passed, either JDBC or PG2 version of the app is run:

(defn -main [& [type]]
  (case type
    "pg" (-main-pg)
    "jdbc" (-main-jdbc)))

JDBC version:

(defn -main-jdbc [& _]
  (with-open [^HikariDataSource datasource
              (cp/make-datasource cp-options)]
    (let [handler
          (make-jdbc-handler datasource)]
      (jetty/run-jetty
       (-> handler
           (wrap-json-response)
           (wrap-ex))
       JETTY))))

The wrap-json-response middleware comes from the ring.middleware.json namespace, and the wrap-ex is defined in-place:

(defn wrap-ex [handler]
  (fn [request]
    (try
      (handler request)
      (catch Throwable e
        {:status 500
         :body
         (with-out-str
           (-> e Throwable->map pprint/pprint))}))))

A function that builds a hander closed over the pool:

(defn make-jdbc-handler [^HikariDataSource datasource]
  (fn handler [request]
    (let [data
          (with-open [conn
                      (jdbc/get-connection datasource)]
            (jdbc/execute! conn [QUERY_SELECT_JSON]))]
      {:status 200
       :body data})))

The PG2 handler is designed in a similar way: it’s a function closed over the pool:

(defn make-pg-handler [pool]
  (fn handler [request]
    (let [data
          (pool/with-connection [conn pool]
            (pg/query conn QUERY_SELECT_JSON))]
      {:status 200
       :body data})))

But it’s wrapped with a special pg.ring.json/wrap-json-response middleware that comes from the box with PG2:

(defn -main-pg [& _]
  (pool/with-pool [pool pg-config]
    (let [handler
          (make-pg-handler pool)]
      (jetty/run-jetty
       (-> handler
           (pg.ring.json/wrap-json-response)
           (wrap-ex))
       JETTY))))

Why didn’t we use the standard wrap-json-response from Ring JSON? As I mentioned in the previous post, PG2 ships it’s own JSON module which is based on Jsonista. But the middleware from Ring JSON uses Cheshire. Both Jsonista and Cheshire use Jackson Java library in different ways. The Jsonista approach is faster; also PG2 extends Jsonista with java.time.* classes so you can encode LocalTime or OffsetDateTime without extending protocols and so on.

As a result, PG2 can be used as a part of the HTTP stack for JSON middleware.

Test 1. Sending 1000 requests in series

The command:

ab -n 1000 -c 1 -l http://127.0.0.1:18080/

Requests per second:

The server driven with PG2 handles two times more RPS than its JDBC counterpart.

Test 2. Sending 1000 requests with concurrency of 16

The command:

ab -n 1000 -c 16 -l http://127.0.0.1:18080/

The second test sends 16 parallel requests to the server. Both servers handle more RPS, two times difference is the same:

Test 3. Sending 1000 requests with concurrency of 64

ab -n 1000 -c 64 -l http://127.0.0.1:18080/

Requests per second:

With 64 incoming parallel requests, Jetty and PG2 handle up to 3.3K RPS. Pay attention that this is not a hello-world application: on each request, it fetches 500 randomly generated rows from the database and serializes them into JSON.

The JDBC-driven server has only 1900 RPS which is also good but… on Arm M1 with 10 cores I’ve been getting socket errors many times duing benchmarks. And PG2 didn’t have them! Maybe it is because of garbage collection or Jetty could not serve all the inqueued requests.


This is all I have for PG2 benchmarks. Not as detailed as it was the last time but at least it measures a real application, not a library. If you application relies on the database heavily, most likely you can double RPS by switching to another DB client.

I hope I’ve intrigued you to try PG2 in your pet project. If I really have, drop me a line on how it goes! Meanwhile, I’m writing documentation.