pgtk 0.31.9 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +41 -0
- data/lib/pgtk/pool.rb +35 -2
- data/lib/pgtk/version.rb +1 -1
- data/lib/pgtk/wire.rb +14 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2071a0941e2455164373e80f84f9cd536fd7bc82d216ded50b109c01e56e8b04
|
|
4
|
+
data.tar.gz: 9261c128b18ccb53a941fba5e880021571a40d973e8cd1294d3c9a7bd3ff9cab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b17ff29e64a14aefdd0df18e6bb7c998d484cedffda0669adf15c6ff068c37591e34ee0769182f91718ab4f9e508bbdf34c577cd094c9c96d172fe338d270cf2
|
|
7
|
+
data.tar.gz: c93fc458236e81969a54fe57f4988b2f23a2f541a4c0d7fbf7849c51c0b98f67c7c5b659458de2b06da3ca47ccd0b713abc8f2f4102c43ac897456249e4e1693
|
data/README.md
CHANGED
|
@@ -118,6 +118,22 @@ pgsql = Pgtk::Pool.new(Pgtk::Wire::Yaml.new('config.yml'), max: 5)
|
|
|
118
118
|
pgsql.start! # Start it with five simultaneous connections
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
By default, the pool runs `SELECT 1` on a slot that has been idle for more
|
|
122
|
+
than 60 seconds before yielding it to the caller, and renews the slot in-line
|
|
123
|
+
if that probe fails.
|
|
124
|
+
This guards against managed-PostgreSQL setups behind a TLS-terminating proxy,
|
|
125
|
+
where a cold slot's SSL state can drift out of sync without libpq noticing —
|
|
126
|
+
the next real query then fails with a `decryption failed or bad record mac`
|
|
127
|
+
error.
|
|
128
|
+
You can tune the threshold or disable validation entirely:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# Validate slots idle for more than 30 seconds
|
|
132
|
+
pgsql = Pgtk::Pool.new(wire, max: 5, idle: 30)
|
|
133
|
+
# Disable validation (e.g. for local Unix-socket PostgreSQL)
|
|
134
|
+
pgsql = Pgtk::Pool.new(wire, max: 5, idle: nil)
|
|
135
|
+
```
|
|
136
|
+
|
|
121
137
|
You can also let it pick the connection parameters from the environment
|
|
122
138
|
variable `DATABASE_URL`, formatted like
|
|
123
139
|
`postgres://user:password@host:5432/dbname`:
|
|
@@ -126,6 +142,31 @@ variable `DATABASE_URL`, formatted like
|
|
|
126
142
|
pgsql = Pgtk::Pool.new(Pgtk::Wire::Env.new)
|
|
127
143
|
```
|
|
128
144
|
|
|
145
|
+
Both `Pgtk::Wire::Direct` and `Pgtk::Wire::Env` accept extra keyword
|
|
146
|
+
arguments and forward them to `PG.connect`, so any
|
|
147
|
+
[libpq parameter](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS)
|
|
148
|
+
can be passed through (`sslmode`, `connect_timeout`, `keepalives`,
|
|
149
|
+
`keepalives_idle`, `application_name`, and so on):
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
Pgtk::Wire::Direct.new(
|
|
153
|
+
host: 'db.example.com', port: 5432, dbname: 'app',
|
|
154
|
+
user: 'u', password: 'p',
|
|
155
|
+
sslmode: 'require', connect_timeout: 5, keepalives: 1
|
|
156
|
+
)
|
|
157
|
+
Pgtk::Wire::Env.new('DATABASE_URL', sslmode: 'require', keepalives: 1)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`Pgtk::Wire::Env` also honors the URL query string, so options can be
|
|
161
|
+
configured from the environment alone:
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
DATABASE_URL=postgres://u:p@h:5432/d?sslmode=require&keepalives=1&keepalives_idle=30
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Explicit keyword arguments win over options carried in the URL query string
|
|
168
|
+
on conflict.
|
|
169
|
+
|
|
129
170
|
Now you can fetch some data from the DB:
|
|
130
171
|
|
|
131
172
|
```ruby
|
data/lib/pgtk/pool.rb
CHANGED
|
@@ -57,13 +57,25 @@ class Pgtk::Pool
|
|
|
57
57
|
|
|
58
58
|
# Constructor.
|
|
59
59
|
#
|
|
60
|
+
# The +idle+ option guards against the cold-slot SSL desync that bites
|
|
61
|
+
# managed PostgreSQL behind a TLS proxy: a slot sits idle long enough for
|
|
62
|
+
# the proxy and the client to disagree about SSL state, libpq still reports
|
|
63
|
+
# +CONNECTION_OK+, and the next real query blows up with a decryption error.
|
|
64
|
+
# When a slot has been idle longer than +idle+ seconds, the pool runs
|
|
65
|
+
# +SELECT 1+ on it before yielding; if that fails, the slot is renewed
|
|
66
|
+
# in-line and the caller never sees the error. Set to +nil+ to skip
|
|
67
|
+
# validation entirely (e.g. for local Unix-socket PostgreSQL).
|
|
68
|
+
#
|
|
60
69
|
# @param [Pgtk::Wire] wire The wire
|
|
61
70
|
# @param [Integer] max Total amount of PostgreSQL connections in the pool
|
|
62
71
|
# @param [Numeric] timeout Max seconds to wait for a free connection
|
|
72
|
+
# @param [Numeric, nil] idle Seconds of idleness after which to validate
|
|
73
|
+
# a connection on checkout, or +nil+ to disable validation
|
|
63
74
|
# @param [Object] log The log
|
|
64
|
-
def initialize(wire, max: 8, timeout: 1, log: Loog::NULL)
|
|
75
|
+
def initialize(wire, max: 8, timeout: 1, idle: 60, log: Loog::NULL)
|
|
65
76
|
@wire = wire
|
|
66
77
|
@max = max
|
|
78
|
+
@idle = idle
|
|
67
79
|
@log = log
|
|
68
80
|
@pool = IterableQueue.new(max, timeout)
|
|
69
81
|
@started = false
|
|
@@ -100,6 +112,14 @@ class Pgtk::Pool
|
|
|
100
112
|
@max.times do
|
|
101
113
|
@pool.push(@wire.connection)
|
|
102
114
|
end
|
|
115
|
+
(2 * @max).times do
|
|
116
|
+
connect { |c| c.exec('SELECT 1') }
|
|
117
|
+
rescue StandardError => e
|
|
118
|
+
@log.warn("Pool warm-up query failed, slot will be retried: #{e.message.strip}")
|
|
119
|
+
end
|
|
120
|
+
@max.times do
|
|
121
|
+
connect { |c| c.exec('SELECT 1') }
|
|
122
|
+
end
|
|
103
123
|
@started = true
|
|
104
124
|
@log.debug("PostgreSQL pool started with #{@max} connections")
|
|
105
125
|
end
|
|
@@ -316,7 +336,7 @@ class Pgtk::Pool
|
|
|
316
336
|
def connect
|
|
317
337
|
conn = @pool.pop
|
|
318
338
|
begin
|
|
319
|
-
reason = cause(conn)
|
|
339
|
+
reason = cause(conn) || stale(conn)
|
|
320
340
|
if reason
|
|
321
341
|
begin
|
|
322
342
|
conn = renew(conn, reason)
|
|
@@ -336,6 +356,7 @@ class Pgtk::Pool
|
|
|
336
356
|
raise(e)
|
|
337
357
|
end
|
|
338
358
|
ensure
|
|
359
|
+
conn.instance_variable_set(:@pgtk_last_used, Time.now) if @idle && !conn.finished?
|
|
339
360
|
@pool.push(conn)
|
|
340
361
|
end
|
|
341
362
|
end
|
|
@@ -349,6 +370,18 @@ class Pgtk::Pool
|
|
|
349
370
|
"inspection failed: #{e.message.strip}"
|
|
350
371
|
end
|
|
351
372
|
|
|
373
|
+
def stale(conn)
|
|
374
|
+
return nil if @idle.nil?
|
|
375
|
+
last = conn.instance_variable_get(:@pgtk_last_used)
|
|
376
|
+
return nil if last.nil? || Time.now - last < @idle
|
|
377
|
+
begin
|
|
378
|
+
conn.exec('SELECT 1')
|
|
379
|
+
nil
|
|
380
|
+
rescue StandardError => e
|
|
381
|
+
"validation failed after #{last.ago} idle: #{e.message.strip}"
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
352
385
|
def info(conn)
|
|
353
386
|
pipelines = { PG::Constants::PQ_PIPELINE_ON => 'ON', PG::Constants::PQ_PIPELINE_OFF => 'OFF',
|
|
354
387
|
PG::Constants::PQ_PIPELINE_ABORTED => 'ABORTED' }
|
data/lib/pgtk/version.rb
CHANGED
data/lib/pgtk/wire.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
|
5
5
|
|
|
6
|
+
require 'cgi'
|
|
6
7
|
require 'pg'
|
|
7
8
|
require 'uri'
|
|
8
9
|
require 'yaml'
|
|
@@ -25,7 +26,9 @@ module Pgtk::Wire
|
|
|
25
26
|
# @param [String] dbname Database name
|
|
26
27
|
# @param [String] user Username
|
|
27
28
|
# @param [String] password Password
|
|
28
|
-
|
|
29
|
+
# @param [Hash] opts Extra options forwarded to +PG.connect+ (e.g. +sslmode+,
|
|
30
|
+
# +connect_timeout+, +keepalives+, +keepalives_idle+, +application_name+)
|
|
31
|
+
def initialize(host:, port:, dbname:, user:, password:, **opts)
|
|
29
32
|
raise(ArgumentError, "The host can't be nil") if host.nil?
|
|
30
33
|
@host = host
|
|
31
34
|
raise(ArgumentError, "The port can't be nil") if port.nil?
|
|
@@ -33,11 +36,12 @@ module Pgtk::Wire
|
|
|
33
36
|
@dbname = dbname
|
|
34
37
|
@user = user
|
|
35
38
|
@password = password
|
|
39
|
+
@opts = opts
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
# Create a new connection to PostgreSQL server.
|
|
39
43
|
def connection
|
|
40
|
-
PG.connect(dbname: @dbname, host: @host, port: @port, user: @user, password: @password)
|
|
44
|
+
PG.connect(dbname: @dbname, host: @host, port: @port, user: @user, password: @password, **@opts)
|
|
41
45
|
end
|
|
42
46
|
end
|
|
43
47
|
|
|
@@ -54,21 +58,27 @@ module Pgtk::Wire
|
|
|
54
58
|
# Constructor.
|
|
55
59
|
#
|
|
56
60
|
# @param [String] var The name of the environment variable with the connection URL
|
|
57
|
-
|
|
61
|
+
# @param [Hash] opts Extra options forwarded to +PG.connect+ (e.g. +sslmode+,
|
|
62
|
+
# +connect_timeout+, +keepalives+, +keepalives_idle+, +application_name+).
|
|
63
|
+
# Explicit kwargs win over options carried in the URL query string on conflict.
|
|
64
|
+
def initialize(var = 'DATABASE_URL', **opts)
|
|
58
65
|
raise(ArgumentError, "The name of the environment variable can't be nil") if var.nil?
|
|
59
66
|
@value = ENV.fetch(var, nil)
|
|
60
67
|
raise(ArgumentError, "The environment variable #{@value.inspect} is not set") if @value.nil?
|
|
68
|
+
@opts = opts
|
|
61
69
|
end
|
|
62
70
|
|
|
63
71
|
# Create a new connection to PostgreSQL server.
|
|
64
72
|
def connection
|
|
65
73
|
uri = URI(@value)
|
|
74
|
+
extras = uri.query ? URI.decode_www_form(uri.query).to_h.transform_keys(&:to_sym) : {}
|
|
66
75
|
Pgtk::Wire::Direct.new(
|
|
67
76
|
host: CGI.unescape(uri.host),
|
|
68
77
|
port: uri.port || 5432,
|
|
69
78
|
dbname: CGI.unescape(uri.path[1..]),
|
|
70
79
|
user: CGI.unescape(uri.userinfo.split(':')[0]),
|
|
71
|
-
password: CGI.unescape(uri.userinfo.split(':')[1])
|
|
80
|
+
password: CGI.unescape(uri.userinfo.split(':')[1]),
|
|
81
|
+
**extras.merge(@opts)
|
|
72
82
|
).connection
|
|
73
83
|
end
|
|
74
84
|
end
|