pg_pipeline 0.2.5 → 0.3.1
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 +101 -0
- data/README.md +61 -1
- data/lib/pg_pipeline/bounded_queue.rb +10 -12
- data/lib/pg_pipeline/client.rb +12 -17
- data/lib/pg_pipeline/connection_driver.rb +156 -82
- data/lib/pg_pipeline/errors.rb +1 -0
- data/lib/pg_pipeline/pool.rb +82 -48
- data/lib/pg_pipeline/request.rb +5 -7
- data/lib/pg_pipeline/runtime/notification.rb +32 -0
- data/lib/pg_pipeline/runtime/queue.rb +31 -0
- data/lib/pg_pipeline/runtime/semaphore.rb +138 -0
- data/lib/pg_pipeline/runtime/task.rb +124 -0
- data/lib/pg_pipeline/runtime.rb +98 -0
- data/lib/pg_pipeline/session_guard.rb +3 -8
- data/lib/pg_pipeline/transaction.rb +23 -2
- data/lib/pg_pipeline/version.rb +1 -1
- data/lib/pg_pipeline.rb +1 -0
- metadata +26 -19
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "pg"
|
|
4
|
-
require "async"
|
|
5
|
-
require "async/queue"
|
|
6
4
|
|
|
7
5
|
require_relative "errors"
|
|
6
|
+
require_relative "runtime"
|
|
8
7
|
require_relative "bounded_queue"
|
|
9
8
|
require_relative "server_caps"
|
|
10
9
|
require_relative "request"
|
|
@@ -21,7 +20,7 @@ module PgPipeline
|
|
|
21
20
|
:owner_task, :reader_task, :writer_task
|
|
22
21
|
|
|
23
22
|
attr_writer :readable_events, :results_read, :units_completed, :flush_calls,
|
|
24
|
-
:flush_incomplete, :dispatches
|
|
23
|
+
:flush_incomplete, :dispatches, :leaked_watchers
|
|
25
24
|
|
|
26
25
|
def readable_events = @readable_events || 0
|
|
27
26
|
def results_read = @results_read || 0
|
|
@@ -29,6 +28,7 @@ module PgPipeline
|
|
|
29
28
|
def flush_calls = @flush_calls || 0
|
|
30
29
|
def flush_incomplete = @flush_incomplete || 0
|
|
31
30
|
def dispatches = @dispatches || 0
|
|
31
|
+
def leaked_watchers = @leaked_watchers || 0
|
|
32
32
|
|
|
33
33
|
def initialize(conn, max_pending: DEFAULT_MAX_PENDING, max_in_flight: DEFAULT_MAX_IN_FLIGHT)
|
|
34
34
|
@max_pending = DriverOps.positive_integer!(max_pending, :max_pending)
|
|
@@ -38,37 +38,19 @@ module PgPipeline
|
|
|
38
38
|
@caps = ServerCaps.from_connection(conn)
|
|
39
39
|
@caps.assert_supported!
|
|
40
40
|
DriverOps.warn_flush_coupling_once(@caps)
|
|
41
|
-
|
|
42
41
|
@requests = BoundedQueue.new(@max_pending)
|
|
43
|
-
@events =
|
|
44
|
-
@reader_rearm =
|
|
45
|
-
@writer_commands =
|
|
46
|
-
|
|
42
|
+
@events = Runtime::Queue.new
|
|
43
|
+
@reader_rearm = Runtime::Queue.new
|
|
44
|
+
@writer_commands = Runtime::Queue.new
|
|
45
|
+
|
|
46
|
+
@accepting = @running = @draining = @needs_flush = @writer_armed = @request_event_pending = false
|
|
47
|
+
@readable_events = @results_read = @units_completed = @flush_calls =
|
|
48
|
+
@flush_incomplete = @dispatches = @leaked_watchers = @submitting = 0
|
|
49
|
+
@socket = @owner_task = @reader_task = @writer_task = @dispatching = nil
|
|
47
50
|
@inflight = []
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@accepting = false
|
|
52
|
-
@running = false
|
|
53
|
-
@draining = false
|
|
54
|
-
@needs_flush = false
|
|
55
|
-
@writer_armed = false
|
|
56
|
-
@request_event_pending = false
|
|
57
|
-
|
|
58
|
-
@readable_events = 0
|
|
59
|
-
@results_read = 0
|
|
60
|
-
@units_completed = 0
|
|
61
|
-
@flush_calls = 0
|
|
62
|
-
@flush_incomplete = 0
|
|
63
|
-
@dispatches = 0
|
|
64
|
-
|
|
65
|
-
@socket = nil
|
|
66
|
-
@owner_task = nil
|
|
67
|
-
@reader_task = nil
|
|
68
|
-
@writer_task = nil
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def start(parent: Async::Task.current) = DriverOps.start(self, parent)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def start = DriverOps.start(self)
|
|
72
54
|
def submit(request) = DriverOps.submit(self, request)
|
|
73
55
|
def load = @requests.size + @inflight.size + @submitting + (@dispatching ? 1 : 0)
|
|
74
56
|
def available? = @accepting && @running
|
|
@@ -91,7 +73,8 @@ module PgPipeline
|
|
|
91
73
|
dispatches: dispatches,
|
|
92
74
|
units_per_readable: DriverOps.ratio(units_completed, readable_events),
|
|
93
75
|
results_per_readable: DriverOps.ratio(results_read, readable_events),
|
|
94
|
-
flush_calls_per_unit: DriverOps.ratio(flush_calls, units_completed)
|
|
76
|
+
flush_calls_per_unit: DriverOps.ratio(flush_calls, units_completed),
|
|
77
|
+
leaked_watchers: leaked_watchers
|
|
95
78
|
}
|
|
96
79
|
end
|
|
97
80
|
|
|
@@ -99,21 +82,19 @@ module PgPipeline
|
|
|
99
82
|
return true unless available?
|
|
100
83
|
|
|
101
84
|
probe = Request.build("SELECT 1", nil)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
probe.cancel! unless probe.settled?
|
|
116
|
-
end
|
|
85
|
+
submit(probe)
|
|
86
|
+
Runtime.with_timeout(timeout) { probe.wait }
|
|
87
|
+
true
|
|
88
|
+
rescue Runtime::TimeoutError
|
|
89
|
+
DriverOps.abort_timed_out_health_probe(self, probe)
|
|
90
|
+
rescue QueryError, PipelineAbortedError
|
|
91
|
+
true
|
|
92
|
+
rescue ShutdownError, NotDispatchedError
|
|
93
|
+
true
|
|
94
|
+
rescue ConnectionLostError, PG::Error
|
|
95
|
+
false
|
|
96
|
+
ensure
|
|
97
|
+
probe.cancel! if probe && !probe.settled?
|
|
117
98
|
end
|
|
118
99
|
|
|
119
100
|
def graceful_close = DriverOps.graceful_close(self)
|
|
@@ -124,6 +105,10 @@ module PgPipeline
|
|
|
124
105
|
end
|
|
125
106
|
|
|
126
107
|
module DriverOps
|
|
108
|
+
WATCHER_JOIN_TIMEOUT = 2.0
|
|
109
|
+
OWNER_JOIN_TIMEOUT = 5.0
|
|
110
|
+
WATCHER_POLL_INTERVAL = 0.25
|
|
111
|
+
|
|
127
112
|
module_function
|
|
128
113
|
|
|
129
114
|
def ratio(numerator, denominator)
|
|
@@ -155,25 +140,25 @@ module PgPipeline
|
|
|
155
140
|
raise ArgumentError, "#{name} must be an integer >= 1"
|
|
156
141
|
end
|
|
157
142
|
|
|
158
|
-
def start(d
|
|
143
|
+
def start(d)
|
|
159
144
|
raise Error, "driver already started" if d.running
|
|
145
|
+
raise Error, "driver start requires an active Fiber scheduler" unless Fiber.scheduler
|
|
160
146
|
|
|
161
147
|
d.conn.setnonblocking(true)
|
|
162
148
|
d.conn.enter_pipeline_mode
|
|
163
|
-
|
|
164
149
|
d.socket = d.conn.socket_io
|
|
165
150
|
d.accepting = true
|
|
166
151
|
d.running = true
|
|
167
152
|
|
|
168
|
-
d.reader_task =
|
|
169
|
-
d.writer_task =
|
|
170
|
-
d.owner_task =
|
|
153
|
+
d.reader_task = Runtime.spawn(name: :reader) { reader_watcher(d) }
|
|
154
|
+
d.writer_task = Runtime.spawn(name: :writer) { writer_watcher(d) }
|
|
155
|
+
d.owner_task = Runtime.spawn(name: :owner) { owner_loop(d) }
|
|
171
156
|
d
|
|
172
157
|
rescue Exception
|
|
173
158
|
d.accepting = false
|
|
174
159
|
d.running = false
|
|
175
|
-
|
|
176
|
-
|
|
160
|
+
teardown_watchers(d)
|
|
161
|
+
join_owner(d)
|
|
177
162
|
raise
|
|
178
163
|
end
|
|
179
164
|
|
|
@@ -200,7 +185,7 @@ module PgPipeline
|
|
|
200
185
|
d.accepting = false
|
|
201
186
|
d.requests.close(ShutdownError.new("driver is closing"))
|
|
202
187
|
d.events.enqueue(:close)
|
|
203
|
-
d
|
|
188
|
+
join_owner(d)
|
|
204
189
|
nil
|
|
205
190
|
end
|
|
206
191
|
|
|
@@ -210,7 +195,7 @@ module PgPipeline
|
|
|
210
195
|
d.accepting = false
|
|
211
196
|
d.requests.close(not_dispatched_error(error))
|
|
212
197
|
d.events.enqueue([:abort, error])
|
|
213
|
-
|
|
198
|
+
join_owner(d)
|
|
214
199
|
nil
|
|
215
200
|
end
|
|
216
201
|
|
|
@@ -232,7 +217,12 @@ module PgPipeline
|
|
|
232
217
|
end
|
|
233
218
|
|
|
234
219
|
def owner_loop(d)
|
|
235
|
-
|
|
220
|
+
while d.running
|
|
221
|
+
event = d.events.dequeue
|
|
222
|
+
break if event.nil?
|
|
223
|
+
|
|
224
|
+
process_event(d, event)
|
|
225
|
+
end
|
|
236
226
|
rescue StandardError => e
|
|
237
227
|
fatal_close(d, ConnectionLostError.new("driver crashed: #{e.class}: #{e.message}"))
|
|
238
228
|
ensure
|
|
@@ -241,7 +231,6 @@ module PgPipeline
|
|
|
241
231
|
|
|
242
232
|
def process_event(d, event)
|
|
243
233
|
input_changed = false
|
|
244
|
-
|
|
245
234
|
case event
|
|
246
235
|
when :requests
|
|
247
236
|
d.request_event_pending = false
|
|
@@ -275,6 +264,7 @@ module PgPipeline
|
|
|
275
264
|
|
|
276
265
|
def notify_requests(d)
|
|
277
266
|
return if d.request_event_pending
|
|
267
|
+
return unless d.running
|
|
278
268
|
|
|
279
269
|
d.request_event_pending = true
|
|
280
270
|
d.events.enqueue(:requests)
|
|
@@ -282,7 +272,6 @@ module PgPipeline
|
|
|
282
272
|
|
|
283
273
|
def pump_requests(d)
|
|
284
274
|
dispatched = false
|
|
285
|
-
|
|
286
275
|
while d.inflight.size < d.max_in_flight && !d.requests.empty?
|
|
287
276
|
request = d.requests.dequeue
|
|
288
277
|
break unless request
|
|
@@ -363,7 +352,6 @@ module PgPipeline
|
|
|
363
352
|
return unless d.running
|
|
364
353
|
|
|
365
354
|
d.flush_calls += 1
|
|
366
|
-
|
|
367
355
|
if d.conn.sync_flush
|
|
368
356
|
d.needs_flush = false
|
|
369
357
|
else
|
|
@@ -377,6 +365,7 @@ module PgPipeline
|
|
|
377
365
|
|
|
378
366
|
def arm_writer(d)
|
|
379
367
|
return if d.writer_armed
|
|
368
|
+
return unless d.running
|
|
380
369
|
|
|
381
370
|
d.writer_armed = true
|
|
382
371
|
d.writer_commands.enqueue(:wait_writable)
|
|
@@ -404,7 +393,6 @@ module PgPipeline
|
|
|
404
393
|
end
|
|
405
394
|
|
|
406
395
|
status = result.result_status
|
|
407
|
-
|
|
408
396
|
case status
|
|
409
397
|
when PG::PGRES_TUPLES_OK
|
|
410
398
|
ensure_before_query_boundary!(request, status)
|
|
@@ -465,15 +453,11 @@ module PgPipeline
|
|
|
465
453
|
def finish_graceful_close(d)
|
|
466
454
|
d.accepting = false
|
|
467
455
|
d.running = false
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
ensure
|
|
474
|
-
stop_watchers(d)
|
|
475
|
-
safe_close_conn(d)
|
|
476
|
-
end
|
|
456
|
+
d.conn.exit_pipeline_mode
|
|
457
|
+
rescue PG::Error => e
|
|
458
|
+
fail_all(d, ConnectionLostError.new("failed to exit pipeline mode: #{e.message}"))
|
|
459
|
+
ensure
|
|
460
|
+
teardown_watchers(d)
|
|
477
461
|
end
|
|
478
462
|
|
|
479
463
|
def fatal_close(d, error)
|
|
@@ -483,8 +467,8 @@ module PgPipeline
|
|
|
483
467
|
d.running = false
|
|
484
468
|
d.requests.close(not_dispatched_error(error))
|
|
485
469
|
fail_all(d, error)
|
|
486
|
-
|
|
487
|
-
|
|
470
|
+
|
|
471
|
+
teardown_watchers(d)
|
|
488
472
|
end
|
|
489
473
|
|
|
490
474
|
def fail_all(d, error)
|
|
@@ -520,10 +504,32 @@ module PgPipeline
|
|
|
520
504
|
)
|
|
521
505
|
end
|
|
522
506
|
|
|
507
|
+
def watcher_wait_timeout
|
|
508
|
+
scheduler = Fiber.scheduler
|
|
509
|
+
return nil if scheduler&.respond_to?(:fiber_interrupt)
|
|
510
|
+
|
|
511
|
+
WATCHER_POLL_INTERVAL
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def wait_socket_readable(d, timeout)
|
|
515
|
+
loop do
|
|
516
|
+
return false unless d.running
|
|
517
|
+
return true if d.socket.wait_readable(timeout)
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def wait_socket_writable(d, timeout)
|
|
522
|
+
loop do
|
|
523
|
+
return false unless d.running
|
|
524
|
+
return true if d.socket.wait_writable(timeout)
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
523
528
|
def reader_watcher(d)
|
|
529
|
+
timeout = watcher_wait_timeout
|
|
530
|
+
|
|
524
531
|
while d.running
|
|
525
|
-
d
|
|
526
|
-
break unless d.running
|
|
532
|
+
break unless wait_socket_readable(d, timeout)
|
|
527
533
|
|
|
528
534
|
d.events.enqueue(:readable)
|
|
529
535
|
command = d.reader_rearm.dequeue
|
|
@@ -536,11 +542,13 @@ module PgPipeline
|
|
|
536
542
|
end
|
|
537
543
|
|
|
538
544
|
def writer_watcher(d)
|
|
545
|
+
timeout = watcher_wait_timeout
|
|
546
|
+
|
|
539
547
|
while d.running
|
|
540
548
|
command = d.writer_commands.dequeue
|
|
541
549
|
break unless command == :wait_writable && d.running
|
|
550
|
+
break unless wait_socket_writable(d, timeout)
|
|
542
551
|
|
|
543
|
-
d.socket.wait_writable
|
|
544
552
|
d.events.enqueue(:writable) if d.running
|
|
545
553
|
end
|
|
546
554
|
rescue StandardError => e
|
|
@@ -549,21 +557,87 @@ module PgPipeline
|
|
|
549
557
|
end
|
|
550
558
|
end
|
|
551
559
|
|
|
552
|
-
def
|
|
553
|
-
|
|
554
|
-
|
|
560
|
+
def teardown_watchers(d)
|
|
561
|
+
tasks = release_watchers(d)
|
|
562
|
+
close_wait_points(d)
|
|
563
|
+
begin
|
|
564
|
+
d.socket&.close
|
|
565
|
+
rescue StandardError
|
|
566
|
+
nil
|
|
567
|
+
end
|
|
568
|
+
stop_watchers(tasks)
|
|
569
|
+
join_watchers(d, tasks)
|
|
570
|
+
d.socket = nil
|
|
571
|
+
safe_close_conn(d)
|
|
572
|
+
nil
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def release_watchers(d)
|
|
576
|
+
tasks = [d.reader_task, d.writer_task].compact
|
|
555
577
|
d.reader_task = nil
|
|
556
578
|
d.writer_task = nil
|
|
579
|
+
tasks
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def close_wait_points(d)
|
|
583
|
+
[d.reader_rearm, d.writer_commands, d.events].each do |queue|
|
|
584
|
+
queue&.close
|
|
585
|
+
rescue StandardError
|
|
586
|
+
nil
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def stop_watchers(tasks)
|
|
591
|
+
Array(tasks).each do |task|
|
|
592
|
+
task.stop
|
|
593
|
+
rescue Runtime::Cancel, StandardError
|
|
594
|
+
nil
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
nil
|
|
598
|
+
end
|
|
557
599
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
600
|
+
def join_watchers(d, tasks)
|
|
601
|
+
Array(tasks).each do |task|
|
|
602
|
+
task.wait(WATCHER_JOIN_TIMEOUT)
|
|
603
|
+
rescue Runtime::TimeoutError
|
|
604
|
+
d.leaked_watchers += 1
|
|
605
|
+
warn_leaked_task(d, task, WATCHER_JOIN_TIMEOUT)
|
|
606
|
+
rescue Runtime::Cancel, StandardError
|
|
561
607
|
nil
|
|
562
608
|
end
|
|
563
609
|
|
|
564
610
|
nil
|
|
565
611
|
end
|
|
566
612
|
|
|
613
|
+
def join_owner(d)
|
|
614
|
+
owner = d.owner_task
|
|
615
|
+
return if owner.nil?
|
|
616
|
+
return if Fiber.current.equal?(owner.fiber)
|
|
617
|
+
|
|
618
|
+
begin
|
|
619
|
+
owner.wait(OWNER_JOIN_TIMEOUT)
|
|
620
|
+
rescue Runtime::TimeoutError
|
|
621
|
+
d.leaked_watchers += 1
|
|
622
|
+
warn_leaked_task(d, owner, OWNER_JOIN_TIMEOUT)
|
|
623
|
+
rescue Runtime::Cancel, StandardError
|
|
624
|
+
nil
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
nil
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def warn_leaked_task(d, task, timeout)
|
|
631
|
+
return if ENV["PG_PIPELINE_SILENCE_WARNINGS"]
|
|
632
|
+
|
|
633
|
+
warn(
|
|
634
|
+
"pg_pipeline: task #{task.name.inspect} did not exit within " \
|
|
635
|
+
"#{timeout}s and has been leaked (total #{d.leaked_watchers}). " \
|
|
636
|
+
"Closing the socket and, on schedulers without #fiber_interrupt, the " \
|
|
637
|
+
"#{WATCHER_POLL_INTERVAL}s watcher poll did not release the fiber in time."
|
|
638
|
+
)
|
|
639
|
+
end
|
|
640
|
+
|
|
567
641
|
def safe_close_conn(d)
|
|
568
642
|
d.conn.close unless d.conn.finished?
|
|
569
643
|
rescue StandardError
|
data/lib/pg_pipeline/errors.rb
CHANGED
|
@@ -8,6 +8,7 @@ module PgPipeline
|
|
|
8
8
|
class ConnectionLostError < Error; end
|
|
9
9
|
class NotDispatchedError < ConnectionLostError; end
|
|
10
10
|
class IndeterminateResultError < ConnectionLostError; end
|
|
11
|
+
class IndeterminateCommitError < IndeterminateResultError; end
|
|
11
12
|
class ShutdownError < Error; end
|
|
12
13
|
class ProtocolError < Error; end
|
|
13
14
|
class RecursiveCheckoutError < Error; end
|
data/lib/pg_pipeline/pool.rb
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "pg"
|
|
4
|
-
require "async"
|
|
5
|
-
require "async/semaphore"
|
|
6
|
-
require "async/notification"
|
|
7
4
|
|
|
8
5
|
require_relative "errors"
|
|
6
|
+
require_relative "runtime"
|
|
9
7
|
require_relative "connection_driver"
|
|
10
8
|
require_relative "prepared_statement"
|
|
11
9
|
require_relative "server_caps"
|
|
@@ -19,7 +17,9 @@ module PgPipeline
|
|
|
19
17
|
DEFAULT_HEALTH_INTERVAL = 10.0
|
|
20
18
|
DEFAULT_HEALTH_TIMEOUT = 5.0
|
|
21
19
|
DISCARD_SEQUENCES_SERVER_VERSION = 90_400
|
|
22
|
-
CANCEL_SIGNAL =
|
|
20
|
+
CANCEL_SIGNAL = Runtime::Cancel
|
|
21
|
+
SUPERVISOR_JOIN_TIMEOUT = 5.0
|
|
22
|
+
MAX_PAUSE_FAILURES = 3
|
|
23
23
|
|
|
24
24
|
attr_reader :reconnects, :pipeline_size
|
|
25
25
|
|
|
@@ -58,7 +58,8 @@ module PgPipeline
|
|
|
58
58
|
@health_failures = 0
|
|
59
59
|
@supervisor_error = nil
|
|
60
60
|
@supervisor = nil
|
|
61
|
-
@
|
|
61
|
+
@supervisor_wake = Runtime::Notification.new
|
|
62
|
+
@pause_failures = 0
|
|
62
63
|
|
|
63
64
|
@prepared_statements = {}
|
|
64
65
|
@prepared_generation = 0
|
|
@@ -70,30 +71,28 @@ module PgPipeline
|
|
|
70
71
|
@pinned_error = nil
|
|
71
72
|
@pinned_active = 0
|
|
72
73
|
@pinned_owners = Hash.new(0)
|
|
73
|
-
@pinned_idle =
|
|
74
|
+
@pinned_idle = Runtime::Notification.new
|
|
74
75
|
|
|
75
76
|
@started = false
|
|
76
77
|
@closing = false
|
|
77
78
|
@closed = false
|
|
78
79
|
end
|
|
79
80
|
|
|
80
|
-
def start
|
|
81
|
+
def start
|
|
81
82
|
raise Error, "pool already started" if @started
|
|
82
83
|
if @closing || @closed
|
|
83
84
|
raise ShutdownError, "pool is closing or was closed and cannot be restarted; create a new Pool"
|
|
84
85
|
end
|
|
85
|
-
|
|
86
|
-
parent ||= Async::Task.current
|
|
87
|
-
@task_parent = parent
|
|
86
|
+
raise Error, "pool start requires an active Fiber scheduler" unless Fiber.scheduler
|
|
88
87
|
|
|
89
88
|
begin
|
|
90
|
-
@pipeline_size.times { @drivers << start_pipeline_driver
|
|
89
|
+
@pipeline_size.times { @drivers << start_pipeline_driver }
|
|
91
90
|
@driver_backoff = Array.new(@drivers.size, 0.0)
|
|
92
91
|
@driver_attempts = Array.new(@drivers.size, 0)
|
|
93
92
|
@driver_last_health = Array.new(@drivers.size, monotonic)
|
|
94
|
-
@pinned_gate =
|
|
93
|
+
@pinned_gate = Runtime::Semaphore.new(@pinned_size) if @pinned_size.positive?
|
|
95
94
|
@started = true
|
|
96
|
-
@supervisor =
|
|
95
|
+
@supervisor = Runtime.spawn(name: :supervisor) { supervise } if @reconnect || @health_check
|
|
97
96
|
rescue Exception
|
|
98
97
|
cleanup_partial_start
|
|
99
98
|
raise
|
|
@@ -174,9 +173,10 @@ module PgPipeline
|
|
|
174
173
|
raise @pinned_error if @pinned_error
|
|
175
174
|
raise Error, "pinned pool is disabled (pinned_size=0)" if @pinned_size.zero?
|
|
176
175
|
|
|
177
|
-
|
|
176
|
+
owner = Fiber.current
|
|
177
|
+
if @pinned_owners[owner].positive?
|
|
178
178
|
raise RecursiveCheckoutError,
|
|
179
|
-
"nested Client#session/transaction on the same
|
|
179
|
+
"nested Client#session/transaction on the same fiber is not allowed; " \
|
|
180
180
|
"use Transaction#savepoint for nested atomicity"
|
|
181
181
|
end
|
|
182
182
|
|
|
@@ -184,7 +184,6 @@ module PgPipeline
|
|
|
184
184
|
raise @pinned_error if @pinned_error
|
|
185
185
|
raise ShutdownError, "pool is closing" if @closing
|
|
186
186
|
|
|
187
|
-
owner = Async::Task.current
|
|
188
187
|
conn = nil
|
|
189
188
|
@pinned_active += 1
|
|
190
189
|
@pinned_owners[owner] += 1
|
|
@@ -220,6 +219,7 @@ module PgPipeline
|
|
|
220
219
|
reconnects: @reconnects,
|
|
221
220
|
health_failures: @health_failures,
|
|
222
221
|
supervisor_error: @supervisor_error&.message,
|
|
222
|
+
supervisor_alive: supervisor_alive?,
|
|
223
223
|
closing: @closing,
|
|
224
224
|
closed: @closed,
|
|
225
225
|
pinned_error: @pinned_error&.message
|
|
@@ -229,7 +229,7 @@ module PgPipeline
|
|
|
229
229
|
def graceful_close
|
|
230
230
|
return unless @started
|
|
231
231
|
|
|
232
|
-
if @pinned_owners[
|
|
232
|
+
if @pinned_owners[Fiber.current].positive?
|
|
233
233
|
raise Error, "cannot close the pool from inside Client#session/transaction"
|
|
234
234
|
end
|
|
235
235
|
|
|
@@ -284,7 +284,7 @@ module PgPipeline
|
|
|
284
284
|
def supervise
|
|
285
285
|
until @closing
|
|
286
286
|
begin
|
|
287
|
-
reap_and_replace
|
|
287
|
+
reap_and_replace if @reconnect
|
|
288
288
|
health_probe if @health_check
|
|
289
289
|
@supervisor_error = nil
|
|
290
290
|
rescue StandardError => e
|
|
@@ -292,9 +292,29 @@ module PgPipeline
|
|
|
292
292
|
end
|
|
293
293
|
|
|
294
294
|
break if @closing
|
|
295
|
+
break unless supervisor_pause
|
|
296
|
+
end
|
|
297
|
+
end
|
|
295
298
|
|
|
296
|
-
|
|
299
|
+
def supervisor_pause
|
|
300
|
+
@supervisor_wake.wait(supervisor_sleep_interval)
|
|
301
|
+
@pause_failures = 0
|
|
302
|
+
true
|
|
303
|
+
rescue StandardError => e
|
|
304
|
+
@supervisor_error = e
|
|
305
|
+
@pause_failures = (@pause_failures || 0) + 1
|
|
306
|
+
|
|
307
|
+
unless ENV["PG_PIPELINE_SILENCE_WARNINGS"]
|
|
308
|
+
warn("pg_pipeline: supervisor pause failed (#{e.class}: #{e.message}) " \
|
|
309
|
+
"[#{@pause_failures}/#{MAX_PAUSE_FAILURES}]")
|
|
297
310
|
end
|
|
311
|
+
|
|
312
|
+
@pause_failures < MAX_PAUSE_FAILURES
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def supervisor_alive?
|
|
316
|
+
supervisor = @supervisor
|
|
317
|
+
!supervisor.nil? && !supervisor.finished?
|
|
298
318
|
end
|
|
299
319
|
|
|
300
320
|
def supervisor_sleep_interval
|
|
@@ -305,7 +325,7 @@ module PgPipeline
|
|
|
305
325
|
interval.positive? ? interval : @reconnect_interval
|
|
306
326
|
end
|
|
307
327
|
|
|
308
|
-
def reap_and_replace
|
|
328
|
+
def reap_and_replace
|
|
309
329
|
now = monotonic
|
|
310
330
|
ensure_driver_slots!
|
|
311
331
|
|
|
@@ -316,7 +336,7 @@ module PgPipeline
|
|
|
316
336
|
next if now < (@driver_backoff[index] || 0.0)
|
|
317
337
|
|
|
318
338
|
begin
|
|
319
|
-
@drivers[index] = start_pipeline_driver
|
|
339
|
+
@drivers[index] = start_pipeline_driver
|
|
320
340
|
@driver_backoff[index] = 0.0
|
|
321
341
|
@driver_attempts[index] = 0
|
|
322
342
|
@driver_last_health[index] = monotonic
|
|
@@ -339,10 +359,16 @@ module PgPipeline
|
|
|
339
359
|
next if now - (@driver_last_health[index] || 0.0) < @health_interval
|
|
340
360
|
|
|
341
361
|
@driver_last_health[index] = now
|
|
342
|
-
next if driver.health_check(@health_timeout)
|
|
343
362
|
|
|
344
|
-
|
|
345
|
-
|
|
363
|
+
begin
|
|
364
|
+
next if driver.health_check(@health_timeout)
|
|
365
|
+
|
|
366
|
+
@health_failures += 1
|
|
367
|
+
driver.abort!
|
|
368
|
+
rescue StandardError => e
|
|
369
|
+
@health_failures += 1
|
|
370
|
+
@supervisor_error = e
|
|
371
|
+
end
|
|
346
372
|
end
|
|
347
373
|
end
|
|
348
374
|
|
|
@@ -376,10 +402,21 @@ module PgPipeline
|
|
|
376
402
|
def stop_supervisor
|
|
377
403
|
supervisor = @supervisor
|
|
378
404
|
@supervisor = nil
|
|
379
|
-
supervisor
|
|
405
|
+
return nil unless supervisor
|
|
406
|
+
|
|
407
|
+
@supervisor_wake.signal
|
|
408
|
+
|
|
409
|
+
begin
|
|
410
|
+
supervisor.wait(SUPERVISOR_JOIN_TIMEOUT)
|
|
411
|
+
rescue Runtime::TimeoutError
|
|
412
|
+
unless ENV["PG_PIPELINE_SILENCE_WARNINGS"]
|
|
413
|
+
warn("pg_pipeline: supervisor did not exit within #{SUPERVISOR_JOIN_TIMEOUT}s and has been leaked")
|
|
414
|
+
end
|
|
415
|
+
rescue CANCEL_SIGNAL, StandardError
|
|
416
|
+
nil
|
|
417
|
+
end
|
|
418
|
+
|
|
380
419
|
nil
|
|
381
|
-
rescue CANCEL_SIGNAL => e
|
|
382
|
-
e
|
|
383
420
|
rescue StandardError
|
|
384
421
|
nil
|
|
385
422
|
end
|
|
@@ -413,11 +450,11 @@ module PgPipeline
|
|
|
413
450
|
raise Error, "pool not started" unless @started
|
|
414
451
|
end
|
|
415
452
|
|
|
416
|
-
def start_pipeline_driver
|
|
453
|
+
def start_pipeline_driver
|
|
417
454
|
conn = PoolOps.new_connection(@connection_args)
|
|
418
455
|
prepare_registered_statements(conn)
|
|
419
456
|
driver = ConnectionDriver.new(conn, max_pending: @max_pending, max_in_flight: @max_in_flight)
|
|
420
|
-
driver.start
|
|
457
|
+
driver.start
|
|
421
458
|
rescue Exception
|
|
422
459
|
PoolOps.safe_close(conn) if conn
|
|
423
460
|
raise
|
|
@@ -447,28 +484,26 @@ module PgPipeline
|
|
|
447
484
|
end
|
|
448
485
|
|
|
449
486
|
def release_pinned(owner, conn)
|
|
450
|
-
|
|
451
|
-
if
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
@pinned_free << recycled
|
|
462
|
-
end
|
|
487
|
+
if conn
|
|
488
|
+
if @closing
|
|
489
|
+
PoolOps.safe_close(conn)
|
|
490
|
+
else
|
|
491
|
+
recycled = recycle_pinned_connection(conn)
|
|
492
|
+
|
|
493
|
+
if recycled
|
|
494
|
+
if @closing
|
|
495
|
+
PoolOps.safe_close(recycled)
|
|
496
|
+
else
|
|
497
|
+
@pinned_free << recycled
|
|
463
498
|
end
|
|
464
499
|
end
|
|
465
500
|
end
|
|
466
|
-
ensure
|
|
467
|
-
@pinned_active -= 1
|
|
468
|
-
@pinned_owners[owner] -= 1
|
|
469
|
-
@pinned_owners.delete(owner) if @pinned_owners[owner].zero?
|
|
470
|
-
@pinned_idle.signal if @pinned_active.zero?
|
|
471
501
|
end
|
|
502
|
+
ensure
|
|
503
|
+
@pinned_active -= 1
|
|
504
|
+
@pinned_owners[owner] -= 1
|
|
505
|
+
@pinned_owners.delete(owner) if @pinned_owners[owner].zero?
|
|
506
|
+
@pinned_idle.signal if @pinned_active.zero?
|
|
472
507
|
end
|
|
473
508
|
|
|
474
509
|
def recycle_pinned_connection(conn)
|
|
@@ -513,7 +548,6 @@ module PgPipeline
|
|
|
513
548
|
close_free_pinned
|
|
514
549
|
@pinned_gate = nil
|
|
515
550
|
@supervisor = nil
|
|
516
|
-
@task_parent = nil
|
|
517
551
|
@started = false
|
|
518
552
|
@closing = false
|
|
519
553
|
@closed = false
|