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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52f06b5f116d87eb7229752520f8cfff19e5c97d53c94a5acac935ad5f5d74e7
4
- data.tar.gz: 83f18d647dbcd7ab1e503ac14da13d3abf77353f70acc2937090b237bcf28af7
3
+ metadata.gz: 2071a0941e2455164373e80f84f9cd536fd7bc82d216ded50b109c01e56e8b04
4
+ data.tar.gz: 9261c128b18ccb53a941fba5e880021571a40d973e8cd1294d3c9a7bd3ff9cab
5
5
  SHA512:
6
- metadata.gz: 4f70d7c3755cee075cd586b4ed3830cc2c932bc4a0213e36fe18104e9b4c4c6ac74fcb00e985aeda37766c62b399ffd829a537f59582be7cb2be31a7eacfaac6
7
- data.tar.gz: 9cf6517ef4f27aaf6403fd3dbc522b11c69853a7ccc704f009126f29d7b1aedbcf852522f3d90ff242b3bf482f51634f33c9c2373a88c880e8281b20a731403e
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
@@ -10,5 +10,5 @@ require_relative '../pgtk'
10
10
  # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
11
11
  # License:: MIT
12
12
  module Pgtk
13
- VERSION = '0.31.9' unless defined?(VERSION)
13
+ VERSION = '0.32.0' unless defined?(VERSION)
14
14
  end
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
- def initialize(host:, port:, dbname:, user:, password:)
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
- def initialize(var = 'DATABASE_URL')
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgtk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.31.9
4
+ version: 0.32.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko