pg_pipeline 0.3.3 → 0.3.4
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 +15 -0
- data/lib/pg_pipeline/bounded_queue.rb +8 -6
- data/lib/pg_pipeline/client.rb +55 -37
- data/lib/pg_pipeline/connection_driver.rb +349 -272
- data/lib/pg_pipeline/pool.rb +229 -157
- data/lib/pg_pipeline/prepared_statement.rb +5 -5
- data/lib/pg_pipeline/request.rb +18 -17
- data/lib/pg_pipeline/runtime/notification.rb +2 -1
- data/lib/pg_pipeline/runtime/semaphore.rb +3 -3
- data/lib/pg_pipeline/runtime/task.rb +14 -8
- data/lib/pg_pipeline/runtime.rb +17 -18
- data/lib/pg_pipeline/server_caps.rb +3 -3
- data/lib/pg_pipeline/session.rb +9 -7
- data/lib/pg_pipeline/session_guard.rb +108 -73
- data/lib/pg_pipeline/transaction.rb +33 -34
- data/lib/pg_pipeline/type_maps.rb +37 -22
- data/lib/pg_pipeline/version.rb +1 -1
- metadata +1 -1
data/lib/pg_pipeline/pool.rb
CHANGED
|
@@ -1,15 +1,63 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'pg'
|
|
4
4
|
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
10
|
-
require_relative
|
|
5
|
+
require_relative 'errors'
|
|
6
|
+
require_relative 'runtime'
|
|
7
|
+
require_relative 'connection_driver'
|
|
8
|
+
require_relative 'prepared_statement'
|
|
9
|
+
require_relative 'type_maps'
|
|
10
|
+
require_relative 'server_caps'
|
|
11
11
|
|
|
12
12
|
module PgPipeline
|
|
13
|
+
PoolSettings = Data.define(
|
|
14
|
+
:pipeline_size, :pinned_size, :max_pending, :max_in_flight, :reconnect,
|
|
15
|
+
:reconnect_interval, :reconnect_backoff_max, :health_check, :health_interval,
|
|
16
|
+
:health_timeout, :cancel_pinned_on_abort
|
|
17
|
+
) do
|
|
18
|
+
def self.build(options)
|
|
19
|
+
unknown = options.keys - members
|
|
20
|
+
raise ArgumentError, "unknown keyword: #{unknown.first}" unless unknown.empty?
|
|
21
|
+
|
|
22
|
+
new(**normalize_settings(defaults.merge(options)))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.defaults
|
|
26
|
+
{
|
|
27
|
+
pipeline_size: Pool::DEFAULT_PIPELINE_SIZE,
|
|
28
|
+
pinned_size: Pool::DEFAULT_PINNED_SIZE,
|
|
29
|
+
max_pending: ConnectionDriver::DEFAULT_MAX_PENDING,
|
|
30
|
+
max_in_flight: ConnectionDriver::DEFAULT_MAX_IN_FLIGHT,
|
|
31
|
+
reconnect: true,
|
|
32
|
+
reconnect_interval: Pool::DEFAULT_RECONNECT_INTERVAL,
|
|
33
|
+
reconnect_backoff_max: Pool::DEFAULT_RECONNECT_BACKOFF_MAX,
|
|
34
|
+
health_check: true,
|
|
35
|
+
health_interval: Pool::DEFAULT_HEALTH_INTERVAL,
|
|
36
|
+
health_timeout: Pool::DEFAULT_HEALTH_TIMEOUT,
|
|
37
|
+
cancel_pinned_on_abort: true
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.normalize_settings(values)
|
|
42
|
+
{
|
|
43
|
+
pipeline_size: PoolOps.positive_integer!(values.fetch(:pipeline_size), :pipeline_size),
|
|
44
|
+
pinned_size: PoolOps.nonnegative_integer!(values.fetch(:pinned_size), :pinned_size),
|
|
45
|
+
max_pending: values.fetch(:max_pending),
|
|
46
|
+
max_in_flight: values.fetch(:max_in_flight),
|
|
47
|
+
reconnect: values.fetch(:reconnect),
|
|
48
|
+
reconnect_interval: PoolOps.finite_float!(values.fetch(:reconnect_interval), :reconnect_interval),
|
|
49
|
+
reconnect_backoff_max: PoolOps.finite_float!(values.fetch(:reconnect_backoff_max), :reconnect_backoff_max),
|
|
50
|
+
health_check: values.fetch(:health_check),
|
|
51
|
+
health_interval: PoolOps.finite_float!(values.fetch(:health_interval), :health_interval, allow_zero: true),
|
|
52
|
+
health_timeout: PoolOps.finite_float!(values.fetch(:health_timeout), :health_timeout),
|
|
53
|
+
cancel_pinned_on_abort: values.fetch(:cancel_pinned_on_abort)
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private_class_method :defaults, :normalize_settings
|
|
58
|
+
end
|
|
59
|
+
private_constant :PoolSettings
|
|
60
|
+
|
|
13
61
|
class Pool
|
|
14
62
|
DEFAULT_PIPELINE_SIZE = 4
|
|
15
63
|
DEFAULT_PINNED_SIZE = 2
|
|
@@ -24,30 +72,20 @@ module PgPipeline
|
|
|
24
72
|
|
|
25
73
|
attr_reader :reconnects, :pipeline_size
|
|
26
74
|
|
|
27
|
-
def initialize(connection_args,
|
|
28
|
-
pipeline_size: DEFAULT_PIPELINE_SIZE,
|
|
29
|
-
pinned_size: DEFAULT_PINNED_SIZE,
|
|
30
|
-
max_pending: ConnectionDriver::DEFAULT_MAX_PENDING,
|
|
31
|
-
max_in_flight: ConnectionDriver::DEFAULT_MAX_IN_FLIGHT,
|
|
32
|
-
reconnect: true,
|
|
33
|
-
reconnect_interval: DEFAULT_RECONNECT_INTERVAL,
|
|
34
|
-
reconnect_backoff_max: DEFAULT_RECONNECT_BACKOFF_MAX,
|
|
35
|
-
health_check: true,
|
|
36
|
-
health_interval: DEFAULT_HEALTH_INTERVAL,
|
|
37
|
-
health_timeout: DEFAULT_HEALTH_TIMEOUT,
|
|
38
|
-
cancel_pinned_on_abort: true)
|
|
75
|
+
def initialize(connection_args, **options)
|
|
39
76
|
@connection_args = connection_args
|
|
40
|
-
|
|
41
|
-
@
|
|
42
|
-
@
|
|
43
|
-
@
|
|
44
|
-
@
|
|
45
|
-
@
|
|
46
|
-
@
|
|
47
|
-
@
|
|
48
|
-
@
|
|
49
|
-
@
|
|
50
|
-
@
|
|
77
|
+
settings = PoolSettings.build(options)
|
|
78
|
+
@pipeline_size = settings.pipeline_size
|
|
79
|
+
@pinned_size = settings.pinned_size
|
|
80
|
+
@max_pending = settings.max_pending
|
|
81
|
+
@max_in_flight = settings.max_in_flight
|
|
82
|
+
@reconnect = settings.reconnect
|
|
83
|
+
@reconnect_interval = settings.reconnect_interval
|
|
84
|
+
@reconnect_backoff_max = settings.reconnect_backoff_max
|
|
85
|
+
@health_check = settings.health_check
|
|
86
|
+
@health_interval = settings.health_interval
|
|
87
|
+
@health_timeout = settings.health_timeout
|
|
88
|
+
@cancel_pinned_on_abort = settings.cancel_pinned_on_abort
|
|
51
89
|
|
|
52
90
|
@drivers = []
|
|
53
91
|
@driver_backoff = []
|
|
@@ -81,20 +119,10 @@ module PgPipeline
|
|
|
81
119
|
end
|
|
82
120
|
|
|
83
121
|
def start
|
|
84
|
-
|
|
85
|
-
if @closing || @closed
|
|
86
|
-
raise ShutdownError, "pool is closing or was closed and cannot be restarted; create a new Pool"
|
|
87
|
-
end
|
|
88
|
-
raise Error, "pool start requires an active Fiber scheduler" unless Fiber.scheduler
|
|
122
|
+
validate_start!
|
|
89
123
|
|
|
90
124
|
begin
|
|
91
|
-
|
|
92
|
-
@driver_backoff = Array.new(@drivers.size, 0.0)
|
|
93
|
-
@driver_attempts = Array.new(@drivers.size, 0)
|
|
94
|
-
@driver_last_health = Array.new(@drivers.size, monotonic)
|
|
95
|
-
@pinned_gate = Runtime::Semaphore.new(@pinned_size) if @pinned_size.positive?
|
|
96
|
-
@started = true
|
|
97
|
-
@supervisor = Runtime.spawn(name: :supervisor) { supervise } if @reconnect || @health_check
|
|
125
|
+
start_runtime
|
|
98
126
|
rescue Exception
|
|
99
127
|
cleanup_partial_start
|
|
100
128
|
raise
|
|
@@ -103,6 +131,35 @@ module PgPipeline
|
|
|
103
131
|
self
|
|
104
132
|
end
|
|
105
133
|
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def validate_start!
|
|
137
|
+
raise Error, 'pool already started' if @started
|
|
138
|
+
if @closing || @closed
|
|
139
|
+
raise ShutdownError,
|
|
140
|
+
'pool is closing or was closed and cannot be restarted; create a new Pool'
|
|
141
|
+
end
|
|
142
|
+
raise Error, 'pool start requires an active Fiber scheduler' unless Fiber.scheduler
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def start_runtime
|
|
146
|
+
@pipeline_size.times { @drivers << start_pipeline_driver }
|
|
147
|
+
driver_count = @drivers.size
|
|
148
|
+
@driver_backoff = Array.new(driver_count, 0.0)
|
|
149
|
+
@driver_attempts = Array.new(driver_count, 0)
|
|
150
|
+
@driver_last_health = Array.new(driver_count, monotonic)
|
|
151
|
+
pinned_size = @pinned_size
|
|
152
|
+
@pinned_gate = Runtime::Semaphore.new(pinned_size) if pinned_size.positive?
|
|
153
|
+
@started = true
|
|
154
|
+
start_supervisor if @reconnect || @health_check
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def start_supervisor
|
|
158
|
+
@supervisor = Runtime.spawn(name: :supervisor) { supervise }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
public
|
|
162
|
+
|
|
106
163
|
def closing? = @closing
|
|
107
164
|
|
|
108
165
|
def pipeline_driver
|
|
@@ -110,7 +167,7 @@ module PgPipeline
|
|
|
110
167
|
|
|
111
168
|
driver = PoolOps.select_driver_into(@drivers, @rr, @rr_slot)
|
|
112
169
|
@rr = @rr_slot[0]
|
|
113
|
-
raise NotDispatchedError,
|
|
170
|
+
raise NotDispatchedError, 'no live pipeline connections; request was not dispatched' unless driver
|
|
114
171
|
|
|
115
172
|
driver
|
|
116
173
|
end
|
|
@@ -171,9 +228,7 @@ module PgPipeline
|
|
|
171
228
|
|
|
172
229
|
def dispatch_prepare(statement)
|
|
173
230
|
drivers = @drivers.select(&:available?)
|
|
174
|
-
if drivers.empty?
|
|
175
|
-
raise NotDispatchedError, "no live pipeline connections; statement was not prepared"
|
|
176
|
-
end
|
|
231
|
+
raise NotDispatchedError, 'no live pipeline connections; statement was not prepared' if drivers.empty?
|
|
177
232
|
|
|
178
233
|
drivers.map { |driver| Request.prepare(statement).tap { |r| driver.submit(r) } }
|
|
179
234
|
end
|
|
@@ -181,14 +236,12 @@ module PgPipeline
|
|
|
181
236
|
def await_prepares!(requests)
|
|
182
237
|
first_error = nil
|
|
183
238
|
requests.each do |request|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
first_error = e
|
|
191
|
-
end
|
|
239
|
+
RequestOps.clear_result(request.wait)
|
|
240
|
+
rescue StandardError => e
|
|
241
|
+
if first_error
|
|
242
|
+
e.clear_result! if e.respond_to?(:clear_result!)
|
|
243
|
+
else
|
|
244
|
+
first_error = e
|
|
192
245
|
end
|
|
193
246
|
end
|
|
194
247
|
raise first_error if first_error
|
|
@@ -201,17 +254,17 @@ module PgPipeline
|
|
|
201
254
|
@prepared_generation += 1
|
|
202
255
|
end
|
|
203
256
|
|
|
204
|
-
def with_pinned
|
|
257
|
+
def with_pinned(&block)
|
|
205
258
|
ensure_available!
|
|
206
259
|
raise @pinned_error if @pinned_error
|
|
207
|
-
raise Error,
|
|
260
|
+
raise Error, 'pinned pool is disabled (pinned_size=0)' if @pinned_size.zero?
|
|
208
261
|
|
|
209
262
|
owner = Fiber.current
|
|
210
263
|
assert_not_nested_pinned!(owner)
|
|
211
264
|
|
|
212
265
|
@pinned_gate.acquire do
|
|
213
266
|
assert_pinned_open!
|
|
214
|
-
run_pinned(owner)
|
|
267
|
+
run_pinned(owner, &block)
|
|
215
268
|
end
|
|
216
269
|
end
|
|
217
270
|
|
|
@@ -243,7 +296,7 @@ module PgPipeline
|
|
|
243
296
|
return unless @started
|
|
244
297
|
|
|
245
298
|
if @pinned_owners[Fiber.current].positive?
|
|
246
|
-
raise Error,
|
|
299
|
+
raise Error, 'cannot close the pool from inside Client#session/transaction'
|
|
247
300
|
end
|
|
248
301
|
|
|
249
302
|
@closing = true
|
|
@@ -278,9 +331,7 @@ module PgPipeline
|
|
|
278
331
|
|
|
279
332
|
begin
|
|
280
333
|
first_error = preferred_shutdown_error(first_error, stop_supervisor)
|
|
281
|
-
if @cancel_pinned_on_abort
|
|
282
|
-
first_error = preferred_shutdown_error(first_error, cancel_in_use_pinned)
|
|
283
|
-
end
|
|
334
|
+
first_error = preferred_shutdown_error(first_error, cancel_in_use_pinned) if @cancel_pinned_on_abort
|
|
284
335
|
first_error = preferred_shutdown_error(first_error, close_all_drivers(:abort!))
|
|
285
336
|
ensure
|
|
286
337
|
close_free_pinned
|
|
@@ -317,7 +368,7 @@ module PgPipeline
|
|
|
317
368
|
@supervisor_error = e
|
|
318
369
|
@pause_failures = (@pause_failures || 0) + 1
|
|
319
370
|
|
|
320
|
-
unless ENV[
|
|
371
|
+
unless ENV['PG_PIPELINE_SILENCE_WARNINGS']
|
|
321
372
|
warn("pg_pipeline: supervisor pause failed (#{e.class}: #{e.message}) " \
|
|
322
373
|
"[#{@pause_failures}/#{MAX_PAUSE_FAILURES}]")
|
|
323
374
|
end
|
|
@@ -331,11 +382,13 @@ module PgPipeline
|
|
|
331
382
|
end
|
|
332
383
|
|
|
333
384
|
def supervisor_sleep_interval
|
|
385
|
+
reconnect_interval = @reconnect_interval
|
|
386
|
+
health_interval = @health_interval
|
|
334
387
|
candidates = []
|
|
335
|
-
candidates <<
|
|
336
|
-
candidates <<
|
|
337
|
-
interval = candidates.min ||
|
|
338
|
-
interval.positive? ? interval :
|
|
388
|
+
candidates << reconnect_interval if @reconnect
|
|
389
|
+
candidates << health_interval if @health_check && health_interval.positive?
|
|
390
|
+
interval = candidates.min || reconnect_interval
|
|
391
|
+
interval.positive? ? interval : reconnect_interval
|
|
339
392
|
end
|
|
340
393
|
|
|
341
394
|
def reap_and_replace
|
|
@@ -343,46 +396,61 @@ module PgPipeline
|
|
|
343
396
|
ensure_driver_slots!
|
|
344
397
|
|
|
345
398
|
@drivers.each_index do |index|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
next if now < (@driver_backoff[index] || 0.0)
|
|
399
|
+
replace_unavailable_driver(index, now)
|
|
400
|
+
end
|
|
401
|
+
end
|
|
350
402
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
end
|
|
403
|
+
def replace_unavailable_driver(index, now)
|
|
404
|
+
driver = @drivers[index]
|
|
405
|
+
return if driver.available? || !driver.dead? || now < (@driver_backoff[index] || 0.0)
|
|
406
|
+
|
|
407
|
+
begin
|
|
408
|
+
@drivers[index] = start_pipeline_driver
|
|
409
|
+
reset_driver_retry_state(index)
|
|
410
|
+
rescue StandardError
|
|
411
|
+
backoff_driver_retry(index, now)
|
|
361
412
|
end
|
|
362
413
|
end
|
|
363
414
|
|
|
415
|
+
def reset_driver_retry_state(index)
|
|
416
|
+
@driver_backoff[index] = 0.0
|
|
417
|
+
@driver_attempts[index] = 0
|
|
418
|
+
@driver_last_health[index] = monotonic
|
|
419
|
+
@reconnects += 1
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def backoff_driver_retry(index, now)
|
|
423
|
+
attempts = (@driver_attempts[index] || 0) + 1
|
|
424
|
+
@driver_attempts[index] = attempts
|
|
425
|
+
@driver_backoff[index] = now + next_backoff(attempts)
|
|
426
|
+
end
|
|
427
|
+
|
|
364
428
|
def health_probe
|
|
365
429
|
now = monotonic
|
|
366
430
|
ensure_driver_slots!
|
|
367
431
|
|
|
368
432
|
@drivers.each_index do |index|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
next if now - (@driver_last_health[index] || 0.0) < @health_interval
|
|
433
|
+
probe_driver_health(index, now)
|
|
434
|
+
end
|
|
435
|
+
end
|
|
373
436
|
|
|
374
|
-
|
|
437
|
+
def probe_driver_health(index, now)
|
|
438
|
+
driver = @drivers[index]
|
|
439
|
+
return unless driver.available? && driver.load.zero?
|
|
440
|
+
return if now - (@driver_last_health[index] || 0.0) < @health_interval
|
|
375
441
|
|
|
376
|
-
|
|
377
|
-
|
|
442
|
+
@driver_last_health[index] = now
|
|
443
|
+
run_driver_health_check(driver)
|
|
444
|
+
end
|
|
378
445
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
446
|
+
def run_driver_health_check(driver)
|
|
447
|
+
return if driver.health_check(@health_timeout)
|
|
448
|
+
|
|
449
|
+
@health_failures += 1
|
|
450
|
+
driver.abort!
|
|
451
|
+
rescue StandardError => e
|
|
452
|
+
@health_failures += 1
|
|
453
|
+
@supervisor_error = e
|
|
386
454
|
end
|
|
387
455
|
|
|
388
456
|
def ensure_driver_slots!
|
|
@@ -395,13 +463,14 @@ module PgPipeline
|
|
|
395
463
|
def next_backoff(attempts)
|
|
396
464
|
exponent = [Integer(attempts) - 1, 0].max
|
|
397
465
|
delay = @reconnect_interval * (2.0**exponent)
|
|
398
|
-
|
|
466
|
+
maximum = @reconnect_backoff_max
|
|
467
|
+
delay.finite? && delay < maximum ? delay : maximum
|
|
399
468
|
end
|
|
400
469
|
|
|
401
470
|
def cancel_in_use_pinned
|
|
402
471
|
cancellation = nil
|
|
403
472
|
|
|
404
|
-
@pinned_in_use.
|
|
473
|
+
@pinned_in_use.each_key do |conn|
|
|
405
474
|
conn.cancel if conn.respond_to?(:cancel)
|
|
406
475
|
rescue CANCEL_SIGNAL => e
|
|
407
476
|
cancellation ||= e
|
|
@@ -422,7 +491,7 @@ module PgPipeline
|
|
|
422
491
|
begin
|
|
423
492
|
supervisor.wait(SUPERVISOR_JOIN_TIMEOUT)
|
|
424
493
|
rescue Runtime::TimeoutError
|
|
425
|
-
unless ENV[
|
|
494
|
+
unless ENV['PG_PIPELINE_SILENCE_WARNINGS']
|
|
426
495
|
warn("pg_pipeline: supervisor did not exit within #{SUPERVISOR_JOIN_TIMEOUT}s and has been leaked")
|
|
427
496
|
end
|
|
428
497
|
rescue CANCEL_SIGNAL, StandardError
|
|
@@ -439,13 +508,11 @@ module PgPipeline
|
|
|
439
508
|
cancellation = nil
|
|
440
509
|
|
|
441
510
|
@drivers.each do |driver|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
first_error ||= e
|
|
448
|
-
end
|
|
511
|
+
driver.public_send(method_name)
|
|
512
|
+
rescue CANCEL_SIGNAL => e
|
|
513
|
+
cancellation ||= e
|
|
514
|
+
rescue StandardError => e
|
|
515
|
+
first_error ||= e
|
|
449
516
|
end
|
|
450
517
|
|
|
451
518
|
cancellation || first_error
|
|
@@ -459,14 +526,15 @@ module PgPipeline
|
|
|
459
526
|
end
|
|
460
527
|
|
|
461
528
|
def ensure_available!
|
|
462
|
-
raise ShutdownError,
|
|
463
|
-
raise Error,
|
|
529
|
+
raise ShutdownError, 'pool is closing or closed' if @closing || @closed
|
|
530
|
+
raise Error, 'pool not started' unless @started
|
|
464
531
|
end
|
|
465
532
|
|
|
466
533
|
def start_pipeline_driver
|
|
467
534
|
conn = PoolOps.new_connection(@connection_args)
|
|
468
535
|
prepare_registered_statements(conn)
|
|
469
|
-
driver = ConnectionDriver.new(conn, max_pending: @max_pending, max_in_flight: @max_in_flight,
|
|
536
|
+
driver = ConnectionDriver.new(conn, max_pending: @max_pending, max_in_flight: @max_in_flight,
|
|
537
|
+
type_maps: @type_maps)
|
|
470
538
|
driver.start
|
|
471
539
|
rescue Exception
|
|
472
540
|
PoolOps.safe_close(conn) if conn
|
|
@@ -479,35 +547,42 @@ module PgPipeline
|
|
|
479
547
|
loop do
|
|
480
548
|
generation = @prepared_generation || 0
|
|
481
549
|
statements = (@prepared_statements || {}).values.dup
|
|
482
|
-
|
|
483
|
-
statements.each do |statement|
|
|
484
|
-
next if prepared.key?(statement.physical_name)
|
|
485
|
-
|
|
486
|
-
result = if statement.param_types.nil?
|
|
487
|
-
conn.prepare(statement.physical_name, statement.sql)
|
|
488
|
-
else
|
|
489
|
-
conn.prepare(statement.physical_name, statement.sql, statement.param_types)
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
RequestOps.clear_result(result)
|
|
493
|
-
prepared[statement.physical_name] = true
|
|
494
|
-
end
|
|
550
|
+
prepare_statements(conn, statements, prepared)
|
|
495
551
|
|
|
496
552
|
break if generation == (@prepared_generation || 0)
|
|
497
553
|
end
|
|
498
554
|
end
|
|
499
555
|
|
|
556
|
+
def prepare_statements(conn, statements, prepared)
|
|
557
|
+
statements.each do |statement|
|
|
558
|
+
prepare_statement_on_connection(conn, statement, prepared) unless prepared.key?(statement.physical_name)
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def prepare_statement_on_connection(conn, statement, prepared)
|
|
563
|
+
param_types = statement.param_types
|
|
564
|
+
name = statement.physical_name
|
|
565
|
+
sql = statement.sql
|
|
566
|
+
result = if param_types.nil?
|
|
567
|
+
conn.prepare(name, sql)
|
|
568
|
+
else
|
|
569
|
+
conn.prepare(name, sql, param_types)
|
|
570
|
+
end
|
|
571
|
+
RequestOps.clear_result(result)
|
|
572
|
+
prepared[name] = true
|
|
573
|
+
end
|
|
574
|
+
|
|
500
575
|
def assert_not_nested_pinned!(owner)
|
|
501
576
|
return unless @pinned_owners[owner].positive?
|
|
502
577
|
|
|
503
578
|
raise RecursiveCheckoutError,
|
|
504
|
-
|
|
505
|
-
|
|
579
|
+
'nested Client#session/transaction on the same fiber is not allowed; ' \
|
|
580
|
+
'use Transaction#savepoint for nested atomicity'
|
|
506
581
|
end
|
|
507
582
|
|
|
508
583
|
def assert_pinned_open!
|
|
509
584
|
raise @pinned_error if @pinned_error
|
|
510
|
-
raise ShutdownError,
|
|
585
|
+
raise ShutdownError, 'pool is closing' if @closing
|
|
511
586
|
end
|
|
512
587
|
|
|
513
588
|
def run_pinned(owner)
|
|
@@ -534,8 +609,9 @@ module PgPipeline
|
|
|
534
609
|
return_pinned_connection(conn) if conn
|
|
535
610
|
ensure
|
|
536
611
|
@pinned_active -= 1
|
|
537
|
-
@pinned_owners[owner]
|
|
538
|
-
@pinned_owners
|
|
612
|
+
owner_count = @pinned_owners[owner] - 1
|
|
613
|
+
@pinned_owners[owner] = owner_count
|
|
614
|
+
@pinned_owners.delete(owner) if owner_count.zero?
|
|
539
615
|
@pinned_idle.signal if @pinned_active.zero?
|
|
540
616
|
end
|
|
541
617
|
|
|
@@ -556,30 +632,30 @@ module PgPipeline
|
|
|
556
632
|
end
|
|
557
633
|
|
|
558
634
|
def recycle_pinned_connection(conn)
|
|
559
|
-
if conn.server_version < DISCARD_SEQUENCES_SERVER_VERSION
|
|
560
|
-
PoolOps.safe_close(conn)
|
|
561
|
-
return PoolOps.new_connection(@connection_args)
|
|
562
|
-
end
|
|
635
|
+
return replace_pinned_connection(conn) if conn.server_version < DISCARD_SEQUENCES_SERVER_VERSION
|
|
563
636
|
|
|
564
637
|
PoolOps.sanitize_pinned_connection(conn)
|
|
565
638
|
conn
|
|
566
639
|
rescue CANCEL_SIGNAL
|
|
567
640
|
PoolOps.safe_close(conn)
|
|
568
641
|
raise
|
|
569
|
-
rescue StandardError =>
|
|
570
|
-
PoolOps.safe_close(conn)
|
|
571
|
-
|
|
642
|
+
rescue StandardError => e
|
|
572
643
|
begin
|
|
573
|
-
|
|
644
|
+
replace_pinned_connection(conn)
|
|
574
645
|
rescue StandardError => replacement_error
|
|
575
646
|
@pinned_error = ConnectionLostError.new(
|
|
576
|
-
"pinned connection reset failed (#{
|
|
647
|
+
"pinned connection reset failed (#{e.class}: #{e.message}) " \
|
|
577
648
|
"and replacement failed (#{replacement_error.class}: #{replacement_error.message})"
|
|
578
649
|
)
|
|
579
650
|
nil
|
|
580
651
|
end
|
|
581
652
|
end
|
|
582
653
|
|
|
654
|
+
def replace_pinned_connection(conn)
|
|
655
|
+
PoolOps.safe_close(conn)
|
|
656
|
+
PoolOps.new_connection(@connection_args)
|
|
657
|
+
end
|
|
658
|
+
|
|
583
659
|
def wait_for_pinned_idle
|
|
584
660
|
@pinned_idle.wait while @pinned_active.positive?
|
|
585
661
|
end
|
|
@@ -613,14 +689,7 @@ module PgPipeline
|
|
|
613
689
|
module PoolOps
|
|
614
690
|
module_function
|
|
615
691
|
|
|
616
|
-
def positive_integer!(value, name)
|
|
617
|
-
integer = Integer(value)
|
|
618
|
-
raise ArgumentError, "#{name} must be >= 1" if integer < 1
|
|
619
|
-
|
|
620
|
-
integer
|
|
621
|
-
rescue ArgumentError, TypeError
|
|
622
|
-
raise ArgumentError, "#{name} must be an integer >= 1"
|
|
623
|
-
end
|
|
692
|
+
def positive_integer!(value, name) = DriverOps.positive_integer!(value, name)
|
|
624
693
|
|
|
625
694
|
def finite_float!(value, name, allow_zero: false)
|
|
626
695
|
number = Float(value)
|
|
@@ -629,7 +698,7 @@ module PgPipeline
|
|
|
629
698
|
|
|
630
699
|
number
|
|
631
700
|
rescue ArgumentError, TypeError
|
|
632
|
-
requirement = allow_zero ?
|
|
701
|
+
requirement = allow_zero ? 'non-negative finite' : 'positive finite'
|
|
633
702
|
raise ArgumentError, "#{name} must be a #{requirement} number (got #{value.inspect})"
|
|
634
703
|
end
|
|
635
704
|
|
|
@@ -642,16 +711,18 @@ module PgPipeline
|
|
|
642
711
|
raise ArgumentError, "#{name} must be an integer >= 0"
|
|
643
712
|
end
|
|
644
713
|
|
|
645
|
-
def select_driver_into(drivers,
|
|
714
|
+
def select_driver_into(drivers, round_robin, slot)
|
|
646
715
|
size = drivers.length
|
|
647
716
|
if size.zero?
|
|
648
|
-
slot[0] =
|
|
717
|
+
slot[0] = round_robin
|
|
649
718
|
return nil
|
|
650
719
|
end
|
|
651
720
|
|
|
652
|
-
start =
|
|
721
|
+
start = round_robin % size
|
|
653
722
|
best = nil
|
|
654
|
-
best_index
|
|
723
|
+
best_index = 0
|
|
724
|
+
best_load = 0
|
|
725
|
+
offset = 0
|
|
655
726
|
|
|
656
727
|
while offset < size
|
|
657
728
|
index = start + offset
|
|
@@ -668,13 +739,13 @@ module PgPipeline
|
|
|
668
739
|
best_load = load
|
|
669
740
|
end
|
|
670
741
|
|
|
671
|
-
slot[0] = best ? (best_index + 1) % size :
|
|
742
|
+
slot[0] = best ? (best_index + 1) % size : round_robin
|
|
672
743
|
best
|
|
673
744
|
end
|
|
674
745
|
|
|
675
|
-
def select_driver(drivers,
|
|
676
|
-
slot = [
|
|
677
|
-
driver = select_driver_into(drivers,
|
|
746
|
+
def select_driver(drivers, round_robin)
|
|
747
|
+
slot = [round_robin]
|
|
748
|
+
driver = select_driver_into(drivers, round_robin, slot)
|
|
678
749
|
[driver, slot[0]]
|
|
679
750
|
end
|
|
680
751
|
|
|
@@ -683,20 +754,21 @@ module PgPipeline
|
|
|
683
754
|
end
|
|
684
755
|
|
|
685
756
|
def sanitize_pinned_connection(conn)
|
|
686
|
-
raise ConnectionLostError,
|
|
687
|
-
raise ConnectionLostError,
|
|
757
|
+
raise ConnectionLostError, 'pinned connection is closed' if conn.finished?
|
|
758
|
+
raise ConnectionLostError, 'pinned connection is bad' unless conn.status == PG::CONNECTION_OK
|
|
688
759
|
|
|
689
|
-
|
|
760
|
+
transaction_status = conn.transaction_status
|
|
761
|
+
case transaction_status
|
|
690
762
|
when PG::PQTRANS_IDLE
|
|
691
763
|
nil
|
|
692
764
|
when PG::PQTRANS_INTRANS, PG::PQTRANS_INERROR
|
|
693
|
-
conn.exec(
|
|
765
|
+
conn.exec('ROLLBACK')
|
|
694
766
|
else
|
|
695
767
|
raise ConnectionLostError,
|
|
696
|
-
"pinned connection returned in unsafe transaction state #{
|
|
768
|
+
"pinned connection returned in unsafe transaction state #{transaction_status}"
|
|
697
769
|
end
|
|
698
770
|
|
|
699
|
-
conn.exec(
|
|
771
|
+
conn.exec('DISCARD ALL')
|
|
700
772
|
true
|
|
701
773
|
end
|
|
702
774
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
require_relative 'request'
|
|
5
5
|
|
|
6
6
|
module PgPipeline
|
|
7
7
|
class PreparedStatement
|
|
@@ -23,7 +23,7 @@ module PgPipeline
|
|
|
23
23
|
alias call query
|
|
24
24
|
|
|
25
25
|
def inspect
|
|
26
|
-
flag = @typed ?
|
|
26
|
+
flag = @typed ? ' typed' : ''
|
|
27
27
|
"#<#{self.class} name=#{@name.inspect} sql=#{@sql.inspect}#{flag}>"
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -41,8 +41,8 @@ module PgPipeline
|
|
|
41
41
|
|
|
42
42
|
def snapshot_name(name)
|
|
43
43
|
value = name.to_s
|
|
44
|
-
raise ArgumentError,
|
|
45
|
-
raise ArgumentError,
|
|
44
|
+
raise ArgumentError, 'prepared statement name must not be empty' if value.empty?
|
|
45
|
+
raise ArgumentError, 'prepared statement name must not contain NUL' if value.include?("\0")
|
|
46
46
|
|
|
47
47
|
value.frozen? ? value : value.dup.freeze
|
|
48
48
|
end
|