Clojure: connection pooling with hikari-cp


Connection pooling is a technique for improving app performance. A pool (cache) of reusable connections is maintained meaning when users connect to the database they can reuse an existing connection. When the user finishes using the connection it is placed back in the pool for other users to use. This reduces the overhead of connecting to the database by decreasing network traffic, limiting the cost of creating new connections, and reducing the load on the garbage collector. Effectively improving the responsiveness of your app for any task that requires connecting to the database.

This guide will use hikari-cp a Clojure wrapper around HikariCP a java database connection pooling library. It assumes you are using leiningen as your build/dependency management tool and have a postgresql database set up and running. It also won't cover using environment variables to store your database url out of source control (which is highly recommended for security).

Initial set up

Start postgres.

pg_ctl -D pg start

Create a database called databasename.

createdb databasename

Dependencies

Add org.clojure/java.jdbc, org.postgresql/postgresql and hikari-cp to your dependencies.

(defproject hikari-cp-example "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.10.1"]
                 [org.clojure/java.jdbc "0.7.7"]
                 [org.postgresql/postgresql "42.2.3"]
                 [hikari-cp "2.7.1"]])

Parsing a database URL

Create a helper function for passing urls.

(ns hikari-cp-example.core
  (:require [clojure.java.jdbc :as sql]
            [hikari-cp.core :as cp]
            [clojure.string :as str]))

(defn db-info-from-url [db-url]
  (let [db-uri              (java.net.URI. db-url)
        [username password] (str/split (or (.getUserInfo db-uri) ":") #":")]
    {:username      (or username (System/getProperty "user.name"))
     :password      (or password "")
     :port-number   (.getPort db-uri)
     :database-name (str/replace-first (.getPath db-uri) "/" "")
     :server-name   (.getHost db-uri)}))

This function parses urls that are of the form postgresql://username:password@localhost:5432/databasename". It also handles defaults if certain parameters are missing.

Setting up the connection pool

Define the connection.

(def datasource-options
  (merge (db-info-from-url "postgresql://localhost:5432/databasename")
         {:auto-commit        true
          :read-only          false
          :adapter            "postgresql"
          :connection-timeout 30000
          :validation-timeout 5000
          :idle-timeout       600000
          :max-lifetime       1800000
          :minimum-idle       10
          :maximum-pool-size  20
          :pool-name          "db-pool"
          :register-mbeans    false}))

Define a connection creating function using defonce to ensure it is only create once and delay to make it lazy.

(defonce datasource
  (delay (cp/make-datasource datasource-options)))

Create the connection.

(def database-connection {:datasource @datasource})

You should see some logs from hikari start to flood the repl.

...
16:28:58.495 [nRepl-session-7849a8f5-c0c2-4a6e-99f7-af9c375c9653] DEBUG com.zaxxer.hikari.HikariConfig - schema..........................none
16:28:58.495 [nRepl-session-7849a8f5-c0c2-4a6e-99f7-af9c375c9653] DEBUG com.zaxxer.hikari.HikariConfig - threadFactory...................internal
16:28:58.495 [nRepl-session-7849a8f5-c0c2-4a6e-99f7-af9c375c9653] DEBUG com.zaxxer.hikari.HikariConfig - transactionIsolation............default
16:28:58.495 [nRepl-session-7849a8f5-c0c2-4a6e-99f7-af9c375c9653] DEBUG com.zaxxer.hikari.HikariConfig - username........................"anders"
16:28:58.495 [nRepl-session-7849a8f5-c0c2-4a6e-99f7-af9c375c9653] DEBUG com.zaxxer.hikari.HikariConfig - validationTimeout...............5000
16:28:58.497 [nRepl-session-7849a8f5-c0c2-4a6e-99f7-af9c375c9653] INFO com.zaxxer.hikari.HikariDataSource - db-pool - Starting...
...

Stop the logs flooding the repl

The hikari logs can be quite noisy. Logback can be used to filter out the DEBUG and INFO level messages.

Add logback as a dependency.

(defproject hikari-cp-example "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.10.0"]
                 [org.clojure/java.jdbc "0.7.7"]
                 [org.postgresql/postgresql "42.2.3"]
                 [hikari-cp "2.7.1"]
                 [ch.qos.logback/logback-classic "1.2.3"]])

Create a logback.xml in your project resources folder (lein will add this to the class path).

<configuration debug="false">
 <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-10contextName %logger{36} - %msg%n</pattern>
    </encoder>
 </appender>
 <logger name="com.zaxxer.hikari">
     <level value="error"/>
 </logger>
  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
  </root>
</configuration>

Now only ERROR level hikari messages will be logged.

This concludes this guide to setting up connection pooling with hikari-cp. The full example project can be found here.

For a detailed explanation on what the pool size should be check out this post.