pgtk 0.17.4 → 0.18.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: 7c88391a71242e30946bd4c1cb06879c4cf83ce1a4ba00e0511c1784e8713e5a
4
- data.tar.gz: '08e01c0c7bca206247159c4d672c274c30aa79be9db4c1605d9c8dd395e14bf4'
3
+ metadata.gz: e3c09e42b393c24a720d0ca3c63da7d4618cfa430fd40122a516525ef64e0e9c
4
+ data.tar.gz: 0bf318a887749f9edf00e78a5b229321d77804a36e1afb4a248e8952c757e5b3
5
5
  SHA512:
6
- metadata.gz: b27a880f60936cfbbdf99c32ab463380a31724b619aa9bd7564b6fe9286075d04759d4fda4203778a84ee2492b15ff5dffea84a2b482d117a4d40edad72f0141
7
- data.tar.gz: 770f922faf767f2af485a8433ca29aa8e5c227ed9aca25323eeabf6b718ed77bf6255047c12bb588d8ff31a21f56f8676affb23742069c732dc9803a639177b6
6
+ metadata.gz: c4ff1a0b73c3d8c7a7f9d917b08284e534f38cd09034b8bb8a031c6ad3412546faf10cbb79a5e66f1f991098bf221642303949fa08d5490e584ed0064b76f70c
7
+ data.tar.gz: 7632624121254f3c0c8a428c3a0187ec112c813f7ac7fadd1a271ecc11db65b92afe8c27cfe45dadb9aa42893849129dd7e0f0d56783c9ac91640face5b1ec97
data/README.md CHANGED
@@ -173,6 +173,42 @@ pool = Pgtk::Spy.new(pool) do |sql|
173
173
  end
174
174
  ```
175
175
 
176
+ ## Query Timeouts with `Pgtk::Impatient`
177
+
178
+ To prevent queries from running indefinitely, use `Pgtk::Impatient` to enforce
179
+ timeouts on database operations:
180
+
181
+ ```ruby
182
+ require 'pgtk/impatient'
183
+ # Wrap the pool with a 5-second timeout for all queries
184
+ impatient = Pgtk::Impatient.new(pool, 5)
185
+ ```
186
+
187
+ The impatient decorator ensures queries don't hang your application:
188
+
189
+ ```ruby
190
+ begin
191
+ # This query will be terminated if it takes longer than 5 seconds
192
+ impatient.exec('SELECT * FROM large_table WHERE complex_condition')
193
+ rescue Pgtk::Impatient::TooSlow => e
194
+ puts "Query timed out: #{e.message}"
195
+ end
196
+ ```
197
+
198
+ You can exclude specific queries from timeout enforcement using regex patterns:
199
+
200
+ ```ruby
201
+ # Don't timeout any SELECT queries or specific maintenance operations
202
+ impatient = Pgtk::Impatient.new(pool, 2, /^SELECT/, /^VACUUM/)
203
+ ```
204
+
205
+ Key features:
206
+
207
+ 1. Configurable timeout in seconds for each query
208
+ 2. Raises `Pgtk::Impatient::TooSlow` exception when timeout is exceeded
209
+ 3. Can exclude queries matching specific patterns from timeout checks
210
+ 4. Also sets PostgreSQL's `statement_timeout` for transactions
211
+
176
212
  ## Query Caching with `Pgtk::Stash`
177
213
 
178
214
  For applications with frequent read queries,
@@ -205,6 +241,37 @@ for simple queries:
205
241
  3. Write operations (`INSERT`, `UPDATE`, `DELETE`) bypass
206
242
  the cache and invalidate all cached queries for affected tables
207
243
 
244
+ ## Automatic Retries with `Pgtk::Retry`
245
+
246
+ For resilient database operations, `Pgtk::Retry` provides automatic retry
247
+ functionality for failed `SELECT` queries:
248
+
249
+ ```ruby
250
+ require 'pgtk/retry'
251
+ # Wrap the pool with retry functionality (default: 3 attempts)
252
+ retry_pool = Pgtk::Retry.new(pgsql)
253
+ # Or specify custom number of attempts
254
+ retry_pool = Pgtk::Retry.new(pgsql, attempts: 5)
255
+ ```
256
+
257
+ The retry decorator automatically retries `SELECT` queries that fail due to
258
+ transient errors (network issues, connection problems, etc.):
259
+
260
+ ```ruby
261
+ # This SELECT will be retried up to 3 times if it fails
262
+ users = retry_pool.exec('SELECT * FROM users WHERE active = true')
263
+
264
+ # Non-SELECT queries are NOT retried to prevent duplicate writes
265
+ retry_pool.exec('INSERT INTO logs (message) VALUES ($1)', ['User logged in'])
266
+ ```
267
+
268
+ Key features:
269
+
270
+ 1. Only `SELECT` queries are retried (to prevent duplicate data modifications)
271
+ 2. Retries happen immediately without delay
272
+ 3. The original error is raised after all retry attempts are exhausted
273
+ 4. Works seamlessly with other decorators like `Pgtk::Spy` and `Pgtk::Impatient`
274
+
208
275
  ## Some Examples
209
276
 
210
277
  This library works in
data/lib/pgtk/retry.rb ADDED
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative '../pgtk'
7
+
8
+ # Retry is a decorator for Pool that automatically retries failed SELECT queries.
9
+ # It provides fault tolerance for transient database errors by retrying read-only
10
+ # operations a configurable number of times before giving up.
11
+ #
12
+ # This class implements the same interface as Pool but adds retry logic specifically
13
+ # for SELECT queries. Non-SELECT queries are executed without retry to maintain
14
+ # data integrity and avoid unintended side effects from duplicate writes.
15
+ #
16
+ # Basic usage:
17
+ #
18
+ # # Create and configure a regular pool
19
+ # pool = Pgtk::Pool.new(wire).start(4)
20
+ #
21
+ # # Wrap the pool in a retry decorator with 3 attempts
22
+ # retry_pool = Pgtk::Retry.new(pool, attempts: 3)
23
+ #
24
+ # # SELECT queries are automatically retried on failure
25
+ # begin
26
+ # retry_pool.exec('SELECT * FROM users WHERE id = $1', [42])
27
+ # rescue PG::Error => e
28
+ # puts "Query failed after 3 attempts: #{e.message}"
29
+ # end
30
+ #
31
+ # # Non-SELECT queries are not retried
32
+ # retry_pool.exec('UPDATE users SET active = true WHERE id = $1', [42])
33
+ #
34
+ # # Transactions pass through without retry logic
35
+ # retry_pool.transaction do |t|
36
+ # t.exec('SELECT * FROM accounts') # No retry within transaction
37
+ # t.exec('UPDATE accounts SET balance = balance + 100')
38
+ # end
39
+ #
40
+ # # Combining with other decorators
41
+ # impatient = Pgtk::Impatient.new(retry_pool, 5)
42
+ # spy = Pgtk::Spy.new(impatient) do |sql, duration|
43
+ # puts "Query: #{sql} (#{duration}s)"
44
+ # end
45
+ #
46
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
47
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
48
+ # License:: MIT
49
+ class Pgtk::Retry
50
+ # Constructor.
51
+ #
52
+ # @param [Pgtk::Pool] pool The pool to decorate
53
+ # @param [Integer] attempts Number of attempts to make (default: 3)
54
+ def initialize(pool, attempts: 3)
55
+ @pool = pool
56
+ @attempts = attempts
57
+ end
58
+
59
+ # Get the version of PostgreSQL server.
60
+ #
61
+ # @return [String] Version of PostgreSQL server
62
+ def version
63
+ @pool.version
64
+ end
65
+
66
+ # Execute a SQL query with automatic retry for SELECT queries.
67
+ #
68
+ # @param [String] sql The SQL query with params inside (possibly)
69
+ # @param [Array] args List of arguments
70
+ # @return [Array] Result rows
71
+ def exec(sql, *args)
72
+ query = sql.is_a?(Array) ? sql.join(' ') : sql
73
+ if query.strip.upcase.start_with?('SELECT')
74
+ attempt = 0
75
+ begin
76
+ @pool.exec(sql, *args)
77
+ rescue StandardError => e
78
+ attempt += 1
79
+ raise e if attempt >= @attempts
80
+ retry
81
+ end
82
+ else
83
+ @pool.exec(sql, *args)
84
+ end
85
+ end
86
+
87
+ # Run a transaction without retry logic.
88
+ #
89
+ # @yield [Object] Yields the transaction object
90
+ # @return [Object] Result of the block
91
+ def transaction(&block)
92
+ @pool.transaction(&block)
93
+ end
94
+ end
data/lib/pgtk/version.rb CHANGED
@@ -11,5 +11,5 @@ require_relative '../pgtk'
11
11
  # License:: MIT
12
12
  module Pgtk
13
13
  # Current version of the library.
14
- VERSION = '0.17.4'
14
+ VERSION = '0.18.0'
15
15
  end
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'loog'
7
+ require 'pg'
8
+ require 'qbash'
9
+ require 'rake'
10
+ require 'tmpdir'
11
+ require 'yaml'
12
+ require_relative 'test__helper'
13
+ require_relative '../lib/pgtk/pool'
14
+ require_relative '../lib/pgtk/retry'
15
+
16
+ # Retry test.
17
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
18
+ # Copyright:: Copyright (c) 2017-2025 Yegor Bugayenko
19
+ # License:: MIT
20
+ class TestRetry < Pgtk::Test
21
+ def test_takes_version
22
+ fake_pool do |pool|
23
+ v = Pgtk::Retry.new(pool).version
24
+ refute_nil(v)
25
+ end
26
+ end
27
+
28
+ def test_executes_select_without_error
29
+ fake_pool do |pool|
30
+ retry_pool = Pgtk::Retry.new(pool, attempts: 3)
31
+ result = retry_pool.exec('SELECT 1 as value')
32
+ assert_equal('1', result.first['value'])
33
+ end
34
+ end
35
+
36
+ def test_retries_select_on_failure
37
+ fake_pool do |pool|
38
+ counter = 0
39
+ stub_pool = Object.new
40
+ def stub_pool.version
41
+ 'stub'
42
+ end
43
+ stub_pool.define_singleton_method(:exec) do |sql, *args|
44
+ counter += 1
45
+ raise PG::Error, 'Connection lost' if counter < 3
46
+ pool.exec(sql, *args)
47
+ end
48
+ retry_pool = Pgtk::Retry.new(stub_pool, attempts: 3)
49
+ result = retry_pool.exec('SELECT 2 as num')
50
+ assert_equal('2', result.first['num'])
51
+ assert_equal(3, counter)
52
+ end
53
+ end
54
+
55
+ def test_fails_after_max_attempts
56
+ fake_pool do |_pool|
57
+ stub_pool = Object.new
58
+ def stub_pool.version
59
+ 'stub'
60
+ end
61
+ stub_pool.define_singleton_method(:exec) do |_sql, *_args|
62
+ raise PG::Error, 'Persistent failure'
63
+ end
64
+ retry_pool = Pgtk::Retry.new(stub_pool, attempts: 2)
65
+ assert_raises(PG::Error) do
66
+ retry_pool.exec('SELECT * FROM users')
67
+ end
68
+ end
69
+ end
70
+
71
+ def test_does_not_retry_insert
72
+ fake_pool do |_pool|
73
+ counter = 0
74
+ stub_pool = Object.new
75
+ def stub_pool.version
76
+ 'stub'
77
+ end
78
+ stub_pool.define_singleton_method(:exec) do |_sql, *_args|
79
+ counter += 1
80
+ raise PG::Error, 'Insert failed'
81
+ end
82
+ retry_pool = Pgtk::Retry.new(stub_pool, attempts: 3)
83
+ assert_raises(PG::Error) do
84
+ retry_pool.exec('INSERT INTO book (title) VALUES ($1)', ['Test Book'])
85
+ end
86
+ assert_equal(1, counter)
87
+ end
88
+ end
89
+
90
+ def test_does_not_retry_update
91
+ fake_pool do |_pool|
92
+ counter = 0
93
+ stub_pool = Object.new
94
+ def stub_pool.version
95
+ 'stub'
96
+ end
97
+ stub_pool.define_singleton_method(:exec) do |_sql, *_args|
98
+ counter += 1
99
+ raise PG::Error, 'Update failed'
100
+ end
101
+ retry_pool = Pgtk::Retry.new(stub_pool, attempts: 3)
102
+ assert_raises(PG::Error) do
103
+ retry_pool.exec('UPDATE book SET title = $1 WHERE id = $2', ['New Title', 1])
104
+ end
105
+ assert_equal(1, counter)
106
+ end
107
+ end
108
+
109
+ def test_does_not_retry_delete
110
+ fake_pool do |_pool|
111
+ counter = 0
112
+ stub_pool = Object.new
113
+ def stub_pool.version
114
+ 'stub'
115
+ end
116
+ stub_pool.define_singleton_method(:exec) do |_sql, *_args|
117
+ counter += 1
118
+ raise PG::Error, 'Delete failed'
119
+ end
120
+ retry_pool = Pgtk::Retry.new(stub_pool, attempts: 3)
121
+ assert_raises(PG::Error) do
122
+ retry_pool.exec('DELETE FROM book WHERE id = $1', [1])
123
+ end
124
+ assert_equal(1, counter)
125
+ end
126
+ end
127
+
128
+ def test_handles_select_with_leading_whitespace
129
+ fake_pool do |pool|
130
+ counter = 0
131
+ stub_pool = Object.new
132
+ def stub_pool.version
133
+ 'stub'
134
+ end
135
+ stub_pool.define_singleton_method(:exec) do |sql, *args|
136
+ counter += 1
137
+ raise PG::Error, 'Connection lost' if counter < 2
138
+ pool.exec(sql, *args)
139
+ end
140
+ retry_pool = Pgtk::Retry.new(stub_pool, attempts: 3)
141
+ result = retry_pool.exec(' SELECT 3 as value')
142
+ assert_equal('3', result.first['value'])
143
+ assert_equal(2, counter)
144
+ end
145
+ end
146
+
147
+ def test_handles_select_case_insensitive
148
+ fake_pool do |pool|
149
+ counter = 0
150
+ stub_pool = Object.new
151
+ def stub_pool.version
152
+ 'stub'
153
+ end
154
+ stub_pool.define_singleton_method(:exec) do |sql, *args|
155
+ counter += 1
156
+ raise PG::Error, 'Connection lost' if counter < 2
157
+ pool.exec(sql, *args)
158
+ end
159
+ retry_pool = Pgtk::Retry.new(stub_pool, attempts: 3)
160
+ result = retry_pool.exec('select 4 as value')
161
+ assert_equal('4', result.first['value'])
162
+ assert_equal(2, counter)
163
+ end
164
+ end
165
+
166
+ def test_handles_array_sql
167
+ fake_pool do |pool|
168
+ counter = 0
169
+ stub_pool = Object.new
170
+ def stub_pool.version
171
+ 'stub'
172
+ end
173
+ stub_pool.define_singleton_method(:exec) do |sql, *args|
174
+ counter += 1
175
+ raise PG::Error, 'Connection lost' if counter < 2
176
+ pool.exec(sql, *args)
177
+ end
178
+ retry_pool = Pgtk::Retry.new(stub_pool, attempts: 3)
179
+ result = retry_pool.exec(%w[SELECT 5 as value])
180
+ assert_equal('5', result.first['value'])
181
+ assert_equal(2, counter)
182
+ end
183
+ end
184
+
185
+ def test_transaction_passes_through
186
+ fake_pool do |pool|
187
+ retry_pool = Pgtk::Retry.new(pool)
188
+ retry_pool.transaction do |t|
189
+ id = t.exec(
190
+ 'INSERT INTO book (title) VALUES ($1) RETURNING id',
191
+ ['Transaction Book']
192
+ ).first['id'].to_i
193
+ assert_predicate(id, :positive?)
194
+ end
195
+ end
196
+ end
197
+
198
+ def test_preserves_original_error_type
199
+ fake_pool do |_pool|
200
+ stub_pool = Object.new
201
+ def stub_pool.version
202
+ 'stub'
203
+ end
204
+ stub_pool.define_singleton_method(:exec) do |_sql, *_args|
205
+ raise ArgumentError, 'Invalid argument'
206
+ end
207
+ retry_pool = Pgtk::Retry.new(stub_pool, attempts: 2)
208
+ assert_raises(ArgumentError) do
209
+ retry_pool.exec('SELECT * FROM table')
210
+ end
211
+ end
212
+ end
213
+
214
+ def test_retries_with_unicode_query
215
+ fake_pool do |pool|
216
+ counter = 0
217
+ stub_pool = Object.new
218
+ def stub_pool.version
219
+ 'stub'
220
+ end
221
+ stub_pool.define_singleton_method(:exec) do |sql, *args|
222
+ counter += 1
223
+ raise PG::Error, 'Connection lost' if counter < 2
224
+ pool.exec(sql, *args)
225
+ end
226
+ retry_pool = Pgtk::Retry.new(stub_pool, attempts: 3)
227
+ result = retry_pool.exec('SELECT \'привет\' as greeting')
228
+ assert_equal('привет', result.first['greeting'])
229
+ assert_equal(2, counter)
230
+ end
231
+ end
232
+ 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.17.4
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -176,6 +176,7 @@ files:
176
176
  - lib/pgtk/liquibase_task.rb
177
177
  - lib/pgtk/pgsql_task.rb
178
178
  - lib/pgtk/pool.rb
179
+ - lib/pgtk/retry.rb
179
180
  - lib/pgtk/spy.rb
180
181
  - lib/pgtk/stash.rb
181
182
  - lib/pgtk/version.rb
@@ -190,6 +191,7 @@ files:
190
191
  - test/test_liquibase_task.rb
191
192
  - test/test_pgsql_task.rb
192
193
  - test/test_pool.rb
194
+ - test/test_retry.rb
193
195
  - test/test_stash.rb
194
196
  - test/test_wire.rb
195
197
  homepage: https://github.com/yegor256/pgtk