pg_pipeline 0.2.1 → 0.2.2
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/CHANGELOG.md +82 -18
- data/DESIGN.md +7 -7
- data/README.md +1 -1
- data/lib/pg_pipeline/client.rb +10 -4
- data/lib/pg_pipeline/connection_driver.rb +9 -3
- data/lib/pg_pipeline/errors.rb +1 -0
- data/lib/pg_pipeline/pool.rb +138 -26
- data/lib/pg_pipeline/session.rb +7 -3
- data/lib/pg_pipeline/transaction.rb +13 -12
- data/lib/pg_pipeline/version.rb +1 -1
- 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: 750bfa3d5740056066f5a3543e747f02701132ab941aabbd828d25ece368b1df
|
|
4
|
+
data.tar.gz: f822719ac13016e6a5e8fc373f6fcaf625cd97cc128cf9322a1231228d20ad40
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d7ea51781dc5150133a1f04d8c65ac2b57bc39df6de98f34534e6e490359dcb1b4f19684c2a950f6a22e138ef2d1ca17c4e034df961140afbc4f37d1dab3c76a
|
|
7
|
+
data.tar.gz: 45c4e56417cfa97147db391f2150dba6c5dc0e01f957a373429ef5f6dd7c6ee4baa50973e0a93c179f480bbf3ad79c20d5a501b4e72e705a743cb16b921b4607
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [0.2.
|
|
3
|
+
## [0.2.2] - 2026-07-30
|
|
4
|
+
|
|
5
|
+
Lifecycle-hardening release. Fixes five pool/task-ownership defects surfaced by
|
|
6
|
+
review, adds input validation, and ships deterministic regression coverage for
|
|
7
|
+
each fix.
|
|
8
|
+
|
|
9
|
+
### Fixed (pool & task lifecycle)
|
|
10
|
+
|
|
11
|
+
- Replacement pipeline drivers are now owned by the pool's stable parent task,
|
|
12
|
+
not the supervisor. Stopping the supervisor during shutdown can no longer
|
|
13
|
+
cascade-cancel a live replacement driver before it is gracefully drained.
|
|
14
|
+
- Returning a pinned connection re-checks the closing state after the yielding
|
|
15
|
+
recycle step (`ROLLBACK`/`DISCARD ALL`/reconnect); a connection recycled while
|
|
16
|
+
a concurrent `abort!` runs is closed instead of leaked into the free list.
|
|
17
|
+
- Task cancellation during pinned recycle (`Async::Cancel`/`Async::Stop` are
|
|
18
|
+
`Exception`s, not `StandardError`s) is caught solely to close the connection
|
|
19
|
+
and then re-raised, so cancellation semantics are preserved and the connection
|
|
20
|
+
is not leaked.
|
|
21
|
+
- Nested `Client#session`/`Client#transaction` on the same task is rejected with
|
|
22
|
+
`RecursiveCheckoutError` before the pinned semaphore is acquired, preventing a
|
|
23
|
+
self-deadlock (`pinned_size: 1`) or a silently different connection
|
|
24
|
+
(`pinned_size > 1`). Use `Transaction#savepoint` for nested atomicity.
|
|
25
|
+
- A closing or closed pool is terminal: `Pool#start` raises rather than
|
|
26
|
+
half-restarting onto stale driver state, including after an interrupted close.
|
|
27
|
+
Create a new pool instead.
|
|
28
|
+
- Partial startup now also cleans up if spawning the supervisor task fails after
|
|
29
|
+
drivers were created.
|
|
30
|
+
- Once shutdown begins, availability checks report `ShutdownError` before the
|
|
31
|
+
generic not-started state, preserving the definitely-not-dispatched failure
|
|
32
|
+
classification for concurrent submitters.
|
|
33
|
+
|
|
34
|
+
### Changed (hardening)
|
|
35
|
+
|
|
36
|
+
- Timing options (`reconnect_interval`, `reconnect_backoff_max`,
|
|
37
|
+
`health_interval`, `health_timeout`) reject negative, `NaN`, and infinite
|
|
38
|
+
values; `health_interval` additionally allows `0` ("probe every cycle").
|
|
39
|
+
- The supervisor wakes on the shorter of the enabled reconnect/health cadences,
|
|
40
|
+
so a short `health_interval` is honored independently of a large
|
|
41
|
+
`reconnect_interval`, using scheduler-aware `Kernel#sleep` rather than the
|
|
42
|
+
deprecated `Async::Task#sleep` wrapper.
|
|
43
|
+
- Reconnect backoff uses floating-point exponentiation and caps non-finite or
|
|
44
|
+
over-limit delays, avoiding giant integers without prematurely flattening the
|
|
45
|
+
backoff for very small base intervals.
|
|
46
|
+
- `Transaction#open`/`#savepoint_seq` are no longer publicly writable; control
|
|
47
|
+
state is read via `#open?` and mutated only internally.
|
|
48
|
+
- `Session#exec` accepts optional bind parameters (`exec(sql, params)`),
|
|
49
|
+
matching the documented API; any explicitly supplied params value, including
|
|
50
|
+
an empty array, uses `exec_params`, while omitted params keep simple-query mode.
|
|
51
|
+
- Driver shutdown attempts every driver even if one close raises or the closing
|
|
52
|
+
task is cancelled, then re-raises cancellation after best-effort cleanup;
|
|
53
|
+
watcher cleanup still stops the writer when stopping the reader fails.
|
|
54
|
+
- `Client#close`/`#abort!` now clear client lifecycle state even when terminal pool
|
|
55
|
+
cleanup reports an error, while a rejected re-entrant close leaves the live
|
|
56
|
+
client started.
|
|
57
|
+
|
|
58
|
+
### Compatibility
|
|
59
|
+
|
|
60
|
+
- Shutdown cleanup handles `Async::Cancel` explicitly. The supported dependency
|
|
61
|
+
floor (`async 2.42.0`) already defines that class, so no dead compatibility
|
|
62
|
+
fallback is required.
|
|
63
|
+
- CI now runs the unit suite against exact `async 2.42.0`, builds ruby-pg against
|
|
64
|
+
source-built client libpq 14.23/16.14/17.10 and runs each build against a live
|
|
65
|
+
PostgreSQL server, plus a separate server integration matrix for 14/16/17/18.
|
|
66
|
+
|
|
67
|
+
## [0.2.1]
|
|
68
|
+
|
|
69
|
+
- SessionGuard: bounded per-SQL memo cache for unsafe_reason. Repeated SQL
|
|
70
|
+
(parameterized queries) now costs ~0 allocations instead of ~330 objects/call
|
|
71
|
+
(measured); eliminates the code_only/byteslice line that showed up in profiles.
|
|
72
|
+
- Client#query: drop redundant sql dup (Request owns the one immutable copy).
|
|
73
|
+
- PoolOps.select_driver: allocation-free least-loaded scan (was 2-3 arrays/call);
|
|
74
|
+
behavior verified identical across 20k random cases.
|
|
75
|
+
- bench_kit/metrics.rb + rake bench:metrics: full picture in one run —
|
|
76
|
+
allocations/query + GC pressure, RubyProf process_time (CPU) and allocations,
|
|
77
|
+
RubyProf wall (sanity), and StackProf cpu/wall sampling. Asserts run outside
|
|
78
|
+
every profiler; warmup keeps connection setup out of frame.
|
|
79
|
+
- profile_ci_scenario.rb: correctness asserts moved out of the profiled block;
|
|
80
|
+
GC.start before profiling.
|
|
81
|
+
- Dev dependency: stackprof (~> 0.2).
|
|
82
|
+
|
|
83
|
+
## [0.2.0]
|
|
4
84
|
|
|
5
85
|
Initial experimental implementation.
|
|
6
86
|
|
|
@@ -103,7 +183,7 @@ Initial experimental implementation.
|
|
|
103
183
|
even when the losing Request was already settled.
|
|
104
184
|
- Strict guard precise denylist (no pg_typeof/setup/xact-advisory false positives).
|
|
105
185
|
- CI separates server-major coverage from source-built client-libpq 14/16 coverage.
|
|
106
|
-
-
|
|
186
|
+
- `bench_kit/` (fiber-storm latency, multi-worker connection-count smoke).
|
|
107
187
|
- Pool driver slot arrays self-heal size (reconnect/health safe under partial init).
|
|
108
188
|
- Unit coverage: failover, health_probe, pinned cancel on abort.
|
|
109
189
|
|
|
@@ -138,19 +218,3 @@ Initial experimental implementation.
|
|
|
138
218
|
boundary: cancellation during rollback/sanitize/reconnect closes the physical
|
|
139
219
|
connection instead of orphaning it outside the pool. Graceful close also closes
|
|
140
220
|
already-idle pinned sockets before waiting for active checkouts.
|
|
141
|
-
|
|
142
|
-
## 0.2.1 (perf + measurement)
|
|
143
|
-
|
|
144
|
-
- SessionGuard: bounded per-SQL memo cache for unsafe_reason. Repeated SQL
|
|
145
|
-
(parameterized queries) now costs ~0 allocations instead of ~330 objects/call
|
|
146
|
-
(measured); eliminates the code_only/byteslice line that showed up in profiles.
|
|
147
|
-
- Client#query: drop redundant sql dup (Request owns the one immutable copy).
|
|
148
|
-
- PoolOps.select_driver: allocation-free least-loaded scan (was 2-3 arrays/call);
|
|
149
|
-
behavior verified identical across 20k random cases.
|
|
150
|
-
- bench_kit/metrics.rb + rake bench:metrics: full picture in one run —
|
|
151
|
-
allocations/query + GC pressure, RubyProf process_time (CPU) and allocations,
|
|
152
|
-
RubyProf wall (sanity), and StackProf cpu/wall sampling. Asserts run outside
|
|
153
|
-
every profiler; warmup keeps connection setup out of frame.
|
|
154
|
-
- profile_ci_scenario.rb: correctness asserts moved out of the profiled block;
|
|
155
|
-
GC.start before profiling.
|
|
156
|
-
- Dev dependency: stackprof (~> 0.2).
|
data/DESIGN.md
CHANGED
|
@@ -254,10 +254,10 @@ chunked-rows, non-blocking cancel), never a floor.
|
|
|
254
254
|
- `SessionGuard` is policy/ergonomics, not a security boundary. `:strict` adds a
|
|
255
255
|
precise denylist (`nextval`/`setval`/`pg_export_snapshot`, …) without the old
|
|
256
256
|
broad `pg_*(` / `set*(` false positives; UDFs can still mutate session state.
|
|
257
|
-
- Live integration covers **server** PG 14/16/17.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
257
|
+
- Live integration covers **server** PG 14/16/17/18. A separate CI matrix
|
|
258
|
+
builds ruby-pg against source-built **client** libpq 14/16/17, asserts the
|
|
259
|
+
linked major, and runs both unit and live integration suites because pipeline
|
|
260
|
+
capability is determined by client libpq rather than server version.
|
|
261
261
|
- On libpq 14–16, `place_sync` uses `PQpipelineSync` / ruby-pg `sync_pipeline_sync`
|
|
262
262
|
(flush coupled); libpq 17+ uses `send_pipeline_sync` + explicit `sync_flush`.
|
|
263
263
|
- Pinned recycle still runs full `DISCARD ALL` (no lighter reset profile / recycle
|
|
@@ -284,15 +284,15 @@ Implemented:
|
|
|
284
284
|
live sibling; the failed Request object itself is never re-used.
|
|
285
285
|
- `abort!` CancelRequest on in-use pinned connections.
|
|
286
286
|
- Falcon per-worker contract + sizing formula; reactor-local Client fail-fast.
|
|
287
|
-
- Packaging: gemspec metadata, CI (unit + PG server 14/16/17
|
|
288
|
-
|
|
287
|
+
- Packaging: gemspec metadata, CI (unit + PG server 14/16/17/18 + client-libpq
|
|
288
|
+
14/16/17 + exact async floor), docker-compose,
|
|
289
|
+
examples, `bench_kit/pipeline_throughput.rb` + `bench_kit/multiworker_smoke.rb`.
|
|
289
290
|
- Live integration suite (multiplex, isolation, cancel-drain, savepoints, stats,
|
|
290
291
|
backpressure, pinned concurrency, indeterminate on abort, reconnect, backend
|
|
291
292
|
terminate recovery, pinned abort cancel).
|
|
292
293
|
|
|
293
294
|
Still open:
|
|
294
295
|
|
|
295
|
-
- Dedicated client-libpq 14/16/17 CI matrix (not only server majors).
|
|
296
296
|
- Lighter pinned reset / recycle-latency metrics under high-TPS tx.
|
|
297
297
|
- Force socket close if pinned cancel is ignored.
|
|
298
298
|
- Published bench numbers in README; tighten multiworker smoke to peak occupancy.
|
data/README.md
CHANGED
data/lib/pg_pipeline/client.rb
CHANGED
|
@@ -144,16 +144,22 @@ module PgPipeline
|
|
|
144
144
|
return unless started?(client)
|
|
145
145
|
|
|
146
146
|
ensure_context!(client)
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
begin
|
|
148
|
+
pool(client).graceful_close
|
|
149
|
+
ensure
|
|
150
|
+
client.__send__(:started=, false) if pool(client).closing?
|
|
151
|
+
end
|
|
149
152
|
end
|
|
150
153
|
|
|
151
154
|
def abort!(client)
|
|
152
155
|
return unless started?(client)
|
|
153
156
|
|
|
154
157
|
ensure_context!(client)
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
begin
|
|
159
|
+
pool(client).abort!
|
|
160
|
+
ensure
|
|
161
|
+
client.__send__(:started=, false) if pool(client).closing?
|
|
162
|
+
end
|
|
157
163
|
end
|
|
158
164
|
|
|
159
165
|
def pool(client)
|
|
@@ -481,11 +481,17 @@ module PgPipeline
|
|
|
481
481
|
end
|
|
482
482
|
|
|
483
483
|
def stop_watchers(d)
|
|
484
|
-
d.reader_task
|
|
485
|
-
d.writer_task
|
|
484
|
+
reader = d.reader_task
|
|
485
|
+
writer = d.writer_task
|
|
486
486
|
d.reader_task = nil
|
|
487
487
|
d.writer_task = nil
|
|
488
|
-
|
|
488
|
+
|
|
489
|
+
[reader, writer].each do |task|
|
|
490
|
+
task&.stop
|
|
491
|
+
rescue Async::Cancel, StandardError
|
|
492
|
+
nil
|
|
493
|
+
end
|
|
494
|
+
|
|
489
495
|
nil
|
|
490
496
|
end
|
|
491
497
|
|
data/lib/pg_pipeline/errors.rb
CHANGED
data/lib/pg_pipeline/pool.rb
CHANGED
|
@@ -18,6 +18,7 @@ module PgPipeline
|
|
|
18
18
|
DEFAULT_HEALTH_INTERVAL = 10.0
|
|
19
19
|
DEFAULT_HEALTH_TIMEOUT = 5.0
|
|
20
20
|
DISCARD_SEQUENCES_SERVER_VERSION = 90_400
|
|
21
|
+
CANCEL_SIGNAL = Async::Cancel
|
|
21
22
|
|
|
22
23
|
attr_reader :reconnects, :pipeline_size
|
|
23
24
|
|
|
@@ -39,11 +40,11 @@ module PgPipeline
|
|
|
39
40
|
@max_pending = max_pending
|
|
40
41
|
@max_in_flight = max_in_flight
|
|
41
42
|
@reconnect = reconnect
|
|
42
|
-
@reconnect_interval =
|
|
43
|
-
@reconnect_backoff_max =
|
|
43
|
+
@reconnect_interval = PoolOps.finite_float!(reconnect_interval, :reconnect_interval)
|
|
44
|
+
@reconnect_backoff_max = PoolOps.finite_float!(reconnect_backoff_max, :reconnect_backoff_max)
|
|
44
45
|
@health_check = health_check
|
|
45
|
-
@health_interval =
|
|
46
|
-
@health_timeout =
|
|
46
|
+
@health_interval = PoolOps.finite_float!(health_interval, :health_interval, allow_zero: true)
|
|
47
|
+
@health_timeout = PoolOps.finite_float!(health_timeout, :health_timeout)
|
|
47
48
|
@cancel_pinned_on_abort = cancel_pinned_on_abort
|
|
48
49
|
|
|
49
50
|
@drivers = []
|
|
@@ -55,6 +56,7 @@ module PgPipeline
|
|
|
55
56
|
@health_failures = 0
|
|
56
57
|
@supervisor_error = nil
|
|
57
58
|
@supervisor = nil
|
|
59
|
+
@task_parent = nil
|
|
58
60
|
|
|
59
61
|
@pinned_free = []
|
|
60
62
|
@pinned_in_use = {}
|
|
@@ -66,10 +68,17 @@ module PgPipeline
|
|
|
66
68
|
|
|
67
69
|
@started = false
|
|
68
70
|
@closing = false
|
|
71
|
+
@closed = false
|
|
69
72
|
end
|
|
70
73
|
|
|
71
|
-
def start(parent:
|
|
74
|
+
def start(parent: nil)
|
|
72
75
|
raise Error, "pool already started" if @started
|
|
76
|
+
if @closing || @closed
|
|
77
|
+
raise ShutdownError, "pool is closing or was closed and cannot be restarted; create a new Pool"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
parent ||= Async::Task.current
|
|
81
|
+
@task_parent = parent
|
|
73
82
|
|
|
74
83
|
begin
|
|
75
84
|
@pipeline_size.times { @drivers << start_pipeline_driver(parent) }
|
|
@@ -77,13 +86,13 @@ module PgPipeline
|
|
|
77
86
|
@driver_attempts = Array.new(@drivers.size, 0)
|
|
78
87
|
@driver_last_health = Array.new(@drivers.size, monotonic)
|
|
79
88
|
@pinned_gate = Async::Semaphore.new(@pinned_size) if @pinned_size.positive?
|
|
89
|
+
@started = true
|
|
90
|
+
@supervisor = parent.async { supervise } if @reconnect || @health_check
|
|
80
91
|
rescue Exception
|
|
81
92
|
cleanup_partial_start
|
|
82
93
|
raise
|
|
83
94
|
end
|
|
84
95
|
|
|
85
|
-
@started = true
|
|
86
|
-
@supervisor = parent.async { supervise } if @reconnect || @health_check
|
|
87
96
|
self
|
|
88
97
|
end
|
|
89
98
|
|
|
@@ -103,6 +112,12 @@ module PgPipeline
|
|
|
103
112
|
raise @pinned_error if @pinned_error
|
|
104
113
|
raise Error, "pinned pool is disabled (pinned_size=0)" if @pinned_size.zero?
|
|
105
114
|
|
|
115
|
+
if @pinned_owners[Async::Task.current].positive?
|
|
116
|
+
raise RecursiveCheckoutError,
|
|
117
|
+
"nested Client#session/transaction on the same task is not allowed; " \
|
|
118
|
+
"use Transaction#savepoint for nested atomicity"
|
|
119
|
+
end
|
|
120
|
+
|
|
106
121
|
@pinned_gate.acquire do
|
|
107
122
|
raise @pinned_error if @pinned_error
|
|
108
123
|
raise ShutdownError, "pool is closing" if @closing
|
|
@@ -143,6 +158,7 @@ module PgPipeline
|
|
|
143
158
|
health_failures: @health_failures,
|
|
144
159
|
supervisor_error: @supervisor_error&.message,
|
|
145
160
|
closing: @closing,
|
|
161
|
+
closed: @closed,
|
|
146
162
|
pinned_error: @pinned_error&.message
|
|
147
163
|
}
|
|
148
164
|
end
|
|
@@ -156,10 +172,24 @@ module PgPipeline
|
|
|
156
172
|
|
|
157
173
|
@closing = true
|
|
158
174
|
@started = false
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
175
|
+
first_error = nil
|
|
176
|
+
|
|
177
|
+
begin
|
|
178
|
+
first_error = preferred_shutdown_error(first_error, stop_supervisor)
|
|
179
|
+
first_error = preferred_shutdown_error(first_error, close_all_drivers(:graceful_close))
|
|
180
|
+
|
|
181
|
+
begin
|
|
182
|
+
wait_for_pinned_idle
|
|
183
|
+
rescue CANCEL_SIGNAL, StandardError => e
|
|
184
|
+
first_error = preferred_shutdown_error(first_error, e)
|
|
185
|
+
end
|
|
186
|
+
ensure
|
|
187
|
+
close_free_pinned
|
|
188
|
+
@closed = true
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
raise first_error if first_error
|
|
192
|
+
|
|
163
193
|
nil
|
|
164
194
|
end
|
|
165
195
|
|
|
@@ -168,31 +198,50 @@ module PgPipeline
|
|
|
168
198
|
|
|
169
199
|
@closing = true
|
|
170
200
|
@started = false
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
201
|
+
first_error = nil
|
|
202
|
+
|
|
203
|
+
begin
|
|
204
|
+
first_error = preferred_shutdown_error(first_error, stop_supervisor)
|
|
205
|
+
if @cancel_pinned_on_abort
|
|
206
|
+
first_error = preferred_shutdown_error(first_error, cancel_in_use_pinned)
|
|
207
|
+
end
|
|
208
|
+
first_error = preferred_shutdown_error(first_error, close_all_drivers(:abort!))
|
|
209
|
+
ensure
|
|
210
|
+
close_free_pinned
|
|
211
|
+
@closed = true
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
raise first_error if first_error
|
|
215
|
+
|
|
175
216
|
nil
|
|
176
217
|
end
|
|
177
218
|
|
|
178
219
|
private
|
|
179
220
|
|
|
180
221
|
def supervise
|
|
181
|
-
parent = Async::Task.current
|
|
182
222
|
until @closing
|
|
183
223
|
begin
|
|
184
|
-
reap_and_replace(
|
|
224
|
+
reap_and_replace(@task_parent) if @reconnect
|
|
185
225
|
health_probe if @health_check
|
|
186
226
|
@supervisor_error = nil
|
|
187
227
|
rescue StandardError => e
|
|
188
|
-
# Keep the loop alive: one probe/reconnect failure must not permanently
|
|
189
|
-
# disable health checks and replacement for the worker lifetime.
|
|
190
228
|
@supervisor_error = e
|
|
191
229
|
end
|
|
192
|
-
|
|
230
|
+
|
|
231
|
+
break if @closing
|
|
232
|
+
|
|
233
|
+
sleep(supervisor_sleep_interval)
|
|
193
234
|
end
|
|
194
235
|
end
|
|
195
236
|
|
|
237
|
+
def supervisor_sleep_interval
|
|
238
|
+
candidates = []
|
|
239
|
+
candidates << @reconnect_interval if @reconnect
|
|
240
|
+
candidates << @health_interval if @health_check && @health_interval.positive?
|
|
241
|
+
interval = candidates.min || @reconnect_interval
|
|
242
|
+
interval.positive? ? interval : @reconnect_interval
|
|
243
|
+
end
|
|
244
|
+
|
|
196
245
|
def reap_and_replace(parent)
|
|
197
246
|
now = monotonic
|
|
198
247
|
ensure_driver_slots!
|
|
@@ -242,28 +291,63 @@ module PgPipeline
|
|
|
242
291
|
end
|
|
243
292
|
|
|
244
293
|
def next_backoff(attempts)
|
|
245
|
-
|
|
246
|
-
delay
|
|
294
|
+
exponent = [Integer(attempts) - 1, 0].max
|
|
295
|
+
delay = @reconnect_interval * (2.0**exponent)
|
|
296
|
+
delay.finite? && delay < @reconnect_backoff_max ? delay : @reconnect_backoff_max
|
|
247
297
|
end
|
|
248
298
|
|
|
249
299
|
def cancel_in_use_pinned
|
|
300
|
+
cancellation = nil
|
|
301
|
+
|
|
250
302
|
@pinned_in_use.keys.each do |conn|
|
|
251
303
|
conn.cancel if conn.respond_to?(:cancel)
|
|
304
|
+
rescue CANCEL_SIGNAL => e
|
|
305
|
+
cancellation ||= e
|
|
252
306
|
rescue StandardError
|
|
253
307
|
nil
|
|
254
308
|
end
|
|
309
|
+
|
|
310
|
+
cancellation
|
|
255
311
|
end
|
|
256
312
|
|
|
257
313
|
def stop_supervisor
|
|
258
|
-
@supervisor
|
|
314
|
+
supervisor = @supervisor
|
|
259
315
|
@supervisor = nil
|
|
316
|
+
supervisor&.stop
|
|
317
|
+
nil
|
|
318
|
+
rescue CANCEL_SIGNAL => e
|
|
319
|
+
e
|
|
260
320
|
rescue StandardError
|
|
261
321
|
nil
|
|
262
322
|
end
|
|
263
323
|
|
|
324
|
+
def close_all_drivers(method_name)
|
|
325
|
+
first_error = nil
|
|
326
|
+
cancellation = nil
|
|
327
|
+
|
|
328
|
+
@drivers.each do |driver|
|
|
329
|
+
begin
|
|
330
|
+
driver.public_send(method_name)
|
|
331
|
+
rescue CANCEL_SIGNAL => e
|
|
332
|
+
cancellation ||= e
|
|
333
|
+
rescue StandardError => e
|
|
334
|
+
first_error ||= e
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
cancellation || first_error
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def preferred_shutdown_error(current, candidate)
|
|
342
|
+
return current unless candidate
|
|
343
|
+
return candidate if candidate.is_a?(CANCEL_SIGNAL)
|
|
344
|
+
|
|
345
|
+
current || candidate
|
|
346
|
+
end
|
|
347
|
+
|
|
264
348
|
def ensure_available!
|
|
349
|
+
raise ShutdownError, "pool is closing or closed" if @closing || @closed
|
|
265
350
|
raise Error, "pool not started" unless @started
|
|
266
|
-
raise ShutdownError, "pool is closing" if @closing
|
|
267
351
|
end
|
|
268
352
|
|
|
269
353
|
def start_pipeline_driver(parent)
|
|
@@ -282,7 +366,14 @@ module PgPipeline
|
|
|
282
366
|
PoolOps.safe_close(conn)
|
|
283
367
|
else
|
|
284
368
|
recycled = recycle_pinned_connection(conn)
|
|
285
|
-
|
|
369
|
+
|
|
370
|
+
if recycled
|
|
371
|
+
if @closing
|
|
372
|
+
PoolOps.safe_close(recycled)
|
|
373
|
+
else
|
|
374
|
+
@pinned_free << recycled
|
|
375
|
+
end
|
|
376
|
+
end
|
|
286
377
|
end
|
|
287
378
|
end
|
|
288
379
|
ensure
|
|
@@ -301,6 +392,9 @@ module PgPipeline
|
|
|
301
392
|
|
|
302
393
|
PoolOps.sanitize_pinned_connection(conn)
|
|
303
394
|
conn
|
|
395
|
+
rescue CANCEL_SIGNAL
|
|
396
|
+
PoolOps.safe_close(conn)
|
|
397
|
+
raise
|
|
304
398
|
rescue StandardError => cleanup_error
|
|
305
399
|
PoolOps.safe_close(conn)
|
|
306
400
|
|
|
@@ -322,13 +416,20 @@ module PgPipeline
|
|
|
322
416
|
def cleanup_partial_start
|
|
323
417
|
@drivers.each do |driver|
|
|
324
418
|
driver.abort!
|
|
325
|
-
rescue StandardError
|
|
419
|
+
rescue CANCEL_SIGNAL, StandardError
|
|
326
420
|
nil
|
|
327
421
|
end
|
|
328
422
|
@drivers.clear
|
|
423
|
+
@driver_backoff.clear
|
|
424
|
+
@driver_attempts.clear
|
|
425
|
+
@driver_last_health.clear
|
|
329
426
|
close_free_pinned
|
|
427
|
+
@pinned_gate = nil
|
|
428
|
+
@supervisor = nil
|
|
429
|
+
@task_parent = nil
|
|
330
430
|
@started = false
|
|
331
431
|
@closing = false
|
|
432
|
+
@closed = false
|
|
332
433
|
end
|
|
333
434
|
|
|
334
435
|
def close_free_pinned
|
|
@@ -351,6 +452,17 @@ module PgPipeline
|
|
|
351
452
|
raise ArgumentError, "#{name} must be an integer >= 1"
|
|
352
453
|
end
|
|
353
454
|
|
|
455
|
+
def finite_float!(value, name, allow_zero: false)
|
|
456
|
+
number = Float(value)
|
|
457
|
+
bound_ok = allow_zero ? number >= 0 : number.positive?
|
|
458
|
+
raise ArgumentError unless number.finite? && bound_ok
|
|
459
|
+
|
|
460
|
+
number
|
|
461
|
+
rescue ArgumentError, TypeError
|
|
462
|
+
requirement = allow_zero ? "non-negative finite" : "positive finite"
|
|
463
|
+
raise ArgumentError, "#{name} must be a #{requirement} number (got #{value.inspect})"
|
|
464
|
+
end
|
|
465
|
+
|
|
354
466
|
def nonnegative_integer!(value, name)
|
|
355
467
|
integer = Integer(value)
|
|
356
468
|
raise ArgumentError, "#{name} must be >= 0" if integer.negative?
|
data/lib/pg_pipeline/session.rb
CHANGED
|
@@ -13,7 +13,7 @@ module PgPipeline
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def query(sql, params = []) = SessionOps.query(self, sql, params)
|
|
16
|
-
def exec(sql) = SessionOps.exec(self, sql)
|
|
16
|
+
def exec(sql, params = nil) = SessionOps.exec(self, sql, params)
|
|
17
17
|
def prepare(name, sql, param_types = nil) = SessionOps.prepare(self, name, sql, param_types)
|
|
18
18
|
def exec_prepared(name, params = []) = SessionOps.exec_prepared(self, name, params)
|
|
19
19
|
def active? = @active
|
|
@@ -39,9 +39,13 @@ module PgPipeline
|
|
|
39
39
|
connection(session).exec_params(sql, params)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def exec(session, sql)
|
|
42
|
+
def exec(session, sql, params = nil)
|
|
43
43
|
ensure_active!(session)
|
|
44
|
-
|
|
44
|
+
if params.nil?
|
|
45
|
+
connection(session).exec(sql)
|
|
46
|
+
else
|
|
47
|
+
connection(session).exec_params(sql, params)
|
|
48
|
+
end
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
def prepare(session, name, sql, param_types)
|
|
@@ -6,8 +6,6 @@ require_relative "session"
|
|
|
6
6
|
|
|
7
7
|
module PgPipeline
|
|
8
8
|
class Transaction < Session
|
|
9
|
-
attr_accessor :open, :savepoint_seq
|
|
10
|
-
|
|
11
9
|
def initialize(conn)
|
|
12
10
|
super
|
|
13
11
|
@open = false
|
|
@@ -23,6 +21,10 @@ module PgPipeline
|
|
|
23
21
|
end
|
|
24
22
|
|
|
25
23
|
def open? = @open
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_accessor :open, :savepoint_seq
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
module TransactionOps
|
|
@@ -30,19 +32,17 @@ module PgPipeline
|
|
|
30
32
|
|
|
31
33
|
def run(tx)
|
|
32
34
|
SessionOps.ensure_active!(tx)
|
|
33
|
-
raise Error, "transaction is already open" if tx.open
|
|
35
|
+
raise Error, "transaction is already open" if tx.open?
|
|
34
36
|
|
|
35
37
|
conn = SessionOps.connection(tx)
|
|
36
38
|
conn.exec("BEGIN")
|
|
37
|
-
tx.open
|
|
39
|
+
tx.__send__(:open=, true)
|
|
38
40
|
begin
|
|
39
41
|
result = yield tx
|
|
40
42
|
conn.exec("COMMIT")
|
|
41
|
-
tx.open
|
|
43
|
+
tx.__send__(:open=, false)
|
|
42
44
|
result
|
|
43
45
|
rescue Exception
|
|
44
|
-
# Only roll back the transaction we opened. Early guard errors
|
|
45
|
-
# ("already open", inactive handle) must not hit this path.
|
|
46
46
|
rollback_quietly(tx)
|
|
47
47
|
raise
|
|
48
48
|
end
|
|
@@ -50,11 +50,12 @@ module PgPipeline
|
|
|
50
50
|
|
|
51
51
|
def savepoint(tx, name)
|
|
52
52
|
SessionOps.ensure_active!(tx)
|
|
53
|
-
raise Error, "savepoint requires an open transaction" unless tx.open
|
|
53
|
+
raise Error, "savepoint requires an open transaction" unless tx.open?
|
|
54
54
|
|
|
55
55
|
conn = SessionOps.connection(tx)
|
|
56
|
-
tx.savepoint_seq
|
|
57
|
-
|
|
56
|
+
seq = tx.__send__(:savepoint_seq) + 1
|
|
57
|
+
tx.__send__(:savepoint_seq=, seq)
|
|
58
|
+
point = name || "pgp_sp_#{seq}"
|
|
58
59
|
ident = conn.quote_ident(point)
|
|
59
60
|
|
|
60
61
|
conn.exec("SAVEPOINT #{ident}")
|
|
@@ -79,14 +80,14 @@ module PgPipeline
|
|
|
79
80
|
end
|
|
80
81
|
|
|
81
82
|
def rollback_quietly(tx)
|
|
82
|
-
return unless tx.open
|
|
83
|
+
return unless tx.open?
|
|
83
84
|
|
|
84
85
|
conn = SessionOps.connection(tx)
|
|
85
86
|
conn&.exec("ROLLBACK")
|
|
86
87
|
rescue PG::Error
|
|
87
88
|
nil
|
|
88
89
|
ensure
|
|
89
|
-
tx.open
|
|
90
|
+
tx.__send__(:open=, false)
|
|
90
91
|
end
|
|
91
92
|
end
|
|
92
93
|
end
|
data/lib/pg_pipeline/version.rb
CHANGED