pgtk 0.31.10 → 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 +27 -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
|
|
@@ -324,7 +336,7 @@ class Pgtk::Pool
|
|
|
324
336
|
def connect
|
|
325
337
|
conn = @pool.pop
|
|
326
338
|
begin
|
|
327
|
-
reason = cause(conn)
|
|
339
|
+
reason = cause(conn) || stale(conn)
|
|
328
340
|
if reason
|
|
329
341
|
begin
|
|
330
342
|
conn = renew(conn, reason)
|
|
@@ -344,6 +356,7 @@ class Pgtk::Pool
|
|
|
344
356
|
raise(e)
|
|
345
357
|
end
|
|
346
358
|
ensure
|
|
359
|
+
conn.instance_variable_set(:@pgtk_last_used, Time.now) if @idle && !conn.finished?
|
|
347
360
|
@pool.push(conn)
|
|
348
361
|
end
|
|
349
362
|
end
|
|
@@ -357,6 +370,18 @@ class Pgtk::Pool
|
|
|
357
370
|
"inspection failed: #{e.message.strip}"
|
|
358
371
|
end
|
|
359
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
|
+
|
|
360
385
|
def info(conn)
|
|
361
386
|
pipelines = { PG::Constants::PQ_PIPELINE_ON => 'ON', PG::Constants::PQ_PIPELINE_OFF => 'OFF',
|
|
362
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
|