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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8199974c055231c0da5e834b017c72ba4e9b407edfb738ac86fbfd9f599ad4a9
4
- data.tar.gz: af55c47ebb15d25ef602b586e1754bb0f4a5504e47153058badf1dee65c06bc2
3
+ metadata.gz: 2071a0941e2455164373e80f84f9cd536fd7bc82d216ded50b109c01e56e8b04
4
+ data.tar.gz: 9261c128b18ccb53a941fba5e880021571a40d973e8cd1294d3c9a7bd3ff9cab
5
5
  SHA512:
6
- metadata.gz: ef8df2a5c1a960c78d8efbd3f2f786e9bcf2da56dc3b3a0db67505c5fd52641203a0a901d4e985c42c525551030081cff197e4a28765d81abcf7ca64f626b81a
7
- data.tar.gz: 5193e34bf05c0a06483ecb2235a6b276c87c355197c5fc049bb30732f589cbb501817fb0ab73401cd95a297bf69941398bfe56812850cf73905e6d8f8d178ae7
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
@@ -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.10' 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.10
4
+ version: 0.32.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko