PG docs, part 3
(This is a new documentation chapter from the PG project.)
ToC
Connection Pool
- Basic usage
- With-pool & with-open
- Config
- Thread safety
- Pool Exhausting
- Exception handling
- Logs
- Component
To interact with a database effectively, you need a connection pool. A single connection is fragile on its own: you can easily lose it by an accidental lag in the network.
Another thing that is worth bearing in mind is, that opening a new connection every time you want to reach PostgreSQL is expensive. Every connection starts a new process on the server. If your code opens too many connections, say in a cycle or within threads/futures, sooner or later you’ll reach an error response saying “too many connections”.
The connection pool is an object that holds several open connections at once. It allows you to borrow a connection for some period of time. A borrowed connection can be only used in a block of code that has borrowed it but nowhere else. Once the block of code has done with its duties, the connection gets returned to the pool.
The pool is also capable of calculating the lifetime of connections and their expiration moments. Once a connection has expired, it gets terminated and the pool spawns a new connection.
The connection pool is shipped in a dedicated library
com.github.igrishaev/pg-pool as it depends on the logging facility.
Basic usage
Everything related to the pool is located in the pg.pool namespace. To run a
pool, at least you need a Postgres config which is passed as the first argument
to the make-pool function. This config is used to spawn new connections. The
second map controls the inner pool logic and might be skipped as it has
defaults.
(ns repl
(:require
[pg.client :as pg]
[pg.pool :as pool]))
(def pg-config
{:host "127.0.0.1"
:port 10150
:user "test"
:password "test"
:database "test"})
(def pool-config
{:min-size 1
:max-size 4
:ms-lifetime (* 1000 60 60)})
(def pool
(pool/make-pool pg-config pool-config))
(println pool)
;; < PG pool, min: 1, max: 4, free: 1, used: 0, lifetime: 3600000 ms >
Once you’ve created a pool, borrow a connection using the with-connection
macro. The connection will be bound to the first conn symbol:
(pool/with-connection [conn pool]
(pg/query conn "select 1 as one"))
;; [{:one 1}]
The rest of the logic depends on the client library but not the pool. Being
inside the with-connection macro, you can call pg/query, pg/execute and
other client API.
Exiting the macro body will put the connection back keeping it open. The
connection is put at the end of the queue and taken from the head. Say, if you
have four connections in the pool, you’ll face the same connection each fourth
with-connection call.
Once you’ve done with the pool, terminate it by calling
pool/terminate. Terminating a pool closes all the open connections including
those that are borrowed at the moment.
(pool/terminate pool)
With-pool & with-open
To open a pool temporarily, use the with-pool macro. It wraps a block of code
with logic that opens and terminates a pool. The first argument is a symbol that
a new pool is bound to.
(pool/with-pool [pool pg-config pool-config]
(pool/with-connection [conn pool]
(pg/query conn "select 1 as one")))
;; [{:one 1}]
Since the Pool object implements the Closeable interface, it can be used
within the with-open macro:
(with-open [pool (pool/make-pool pg-config)]
(pool/with-connection [conn pool]
...))
Config
Functions and macros that create a pool take an extra map of pool options:
| Parameter | Default | Comment |
|---|---|---|
:min-size |
2 | The minimum number of connections to be opened during the initialization. |
:max-size |
8 | The maximum number of connections to be opened during the initialization. |
:ms-lifetime |
3.600.000 (1 hour) | The number of milliseconds in which a connection is considered expired. |
Example:
(def pool-config
{:min-size 1
:max-size 4
:ms-lifetime (* 1000 60 15)}) ;; 15 minutes
(pool/make-pool pg-config pool-config)
Thread safety
Unlike a connection object, the pool is thread-safe indeed. What it means is, that two parallel threads cannot obtain the same connection at once. This logic is serialized: each thread will obtain its own connection one after another.
Pool Exhausting
Should you exhaust the pool – meaning you try to borrow a connection while
there aren’t any idle connects and the max-size of busy connections is reached
– the pool will throw an exception to let you know about this. Here is a simple
way to trigger this behavior:
;; create a pool with up to 2 connections
(pool/with-pool [pool pg-config {:min-size 1
:max-size 2}]
;; spawn two hanging queries that lock connections
(future
(pool/with-connection [conn pool]
(pg/query conn "select pg_sleep(600) as sleep")))
(future
(pool/with-connection [conn pool]
(pg/query conn "select pg_sleep(600) as sleep")))
;; wait for a bit to let the futures start their work
(Thread/sleep 100)
;; try to get a new connection => exception
(pool/with-connection [conn pool]
(pg/query conn "select pg_sleep(600) as sleep")))
;; Execution error (ExceptionInfo) at pg.pool/-borrow-connection (pool.clj:95).
;; pool is exhausted: 2 connections in use
At the moment, there is no way to block the execution and wait until there is a
free connection again. Although it’s possible with a blocking version of
ArrayDeque class.
Exception handling
Should any exception occur in the middle of the with-connection macro, the DB
connection gets terminated. A new connection is spawned to substitute it in the
pool.
Logs
The pool logs its events using the clojure.tools.logging library. The
namespace is pg.pool. Here is an example of logs you would get:
16:20:35 DEBUG pg.pool - a new connection created: pg10177
16:20:35 DEBUG pg.pool - connection pg10177 has been acquired
16:20:35 DEBUG pg.pool - a new connection created: pg10178
16:20:35 DEBUG pg.pool - connection pg10177 has been acquired
...
16:20:35 DEBUG pg.pool - connection pg10177 has been released
16:20:35 DEBUG pg.pool - terminating the pool...
16:20:35 DEBUG pg.pool - terminating connection pg10178
16:20:35 DEBUG pg.pool - terminating connection pg10177
16:20:35 DEBUG pg.pool - pool termination done
A snippet for Logback configuration: add it into your logback.xml file:
<logger name="pg.pool" level="DEBUG" additivity="false">
</logger>
Component
The pool may act as a Component. The pool/component function takes both config
maps (pg- and pool-) and returns an object that implements both start and
stop methods from the Lifecycle protocol. The object doesn’t open
connections on creation; it waits until you start it.
Here is a quick example where we initiate and start a component manually:
(require
'[com.stuartsierra.component :as component])
;; create an idle pool component
(def pool-idle
(pool/component pg-config))
;; start it
(def pool-started
(component/start pool-idle))
;; use it
(pool/with-connection [conn pool-started]
(pg/query conn "select 42 as the_answer"))
[{:the_answer 42}]
;; stop it
(def pool-stopped
(component/stop pool-started))
Although it works, it’s better to use components within a system. Below, is an
example of a component that depends on a pool. Both :some-job and :pool form
a system. When started, the Job component receives a pool with pre-allocated
connections. Then we reach the database using the with-connection macro.
(defrecord SomeJob [;; opt
params
;; deps
pool]
component/Lifecycle
(start [this]
(pool/with-connection [conn pool]
...))
(stop [this]
...))
(def system
{:pool
(pool/component pg-config)
:some-job
(-> (map->SomeJob {...})
(component/using [:pool]))})
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter