pg_pipeline 0.2.4 → 0.3.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 +4 -4
- data/CHANGELOG.md +85 -0
- data/DESIGN.md +40 -0
- data/README.md +105 -1
- data/lib/pg_pipeline/bounded_queue.rb +10 -12
- data/lib/pg_pipeline/client.rb +14 -21
- data/lib/pg_pipeline/connection_driver.rb +215 -109
- data/lib/pg_pipeline/pool.rb +111 -61
- data/lib/pg_pipeline/prepared_statement.rb +1 -1
- data/lib/pg_pipeline/request.rb +69 -11
- 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.rb +4 -2
- data/lib/pg_pipeline/session_guard.rb +40 -24
- data/lib/pg_pipeline/version.rb +1 -1
- data/lib/pg_pipeline.rb +1 -0
- metadata +26 -19
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
|
|
|
@@ -53,11 +53,13 @@ module PgPipeline
|
|
|
53
53
|
@driver_attempts = []
|
|
54
54
|
@driver_last_health = []
|
|
55
55
|
@rr = 0
|
|
56
|
+
@rr_slot = [0]
|
|
56
57
|
@reconnects = 0
|
|
57
58
|
@health_failures = 0
|
|
58
59
|
@supervisor_error = nil
|
|
59
60
|
@supervisor = nil
|
|
60
|
-
@
|
|
61
|
+
@supervisor_wake = Runtime::Notification.new
|
|
62
|
+
@pause_failures = 0
|
|
61
63
|
|
|
62
64
|
@prepared_statements = {}
|
|
63
65
|
@prepared_generation = 0
|
|
@@ -69,30 +71,28 @@ module PgPipeline
|
|
|
69
71
|
@pinned_error = nil
|
|
70
72
|
@pinned_active = 0
|
|
71
73
|
@pinned_owners = Hash.new(0)
|
|
72
|
-
@pinned_idle =
|
|
74
|
+
@pinned_idle = Runtime::Notification.new
|
|
73
75
|
|
|
74
76
|
@started = false
|
|
75
77
|
@closing = false
|
|
76
78
|
@closed = false
|
|
77
79
|
end
|
|
78
80
|
|
|
79
|
-
def start
|
|
81
|
+
def start
|
|
80
82
|
raise Error, "pool already started" if @started
|
|
81
83
|
if @closing || @closed
|
|
82
84
|
raise ShutdownError, "pool is closing or was closed and cannot be restarted; create a new Pool"
|
|
83
85
|
end
|
|
84
|
-
|
|
85
|
-
parent ||= Async::Task.current
|
|
86
|
-
@task_parent = parent
|
|
86
|
+
raise Error, "pool start requires an active Fiber scheduler" unless Fiber.scheduler
|
|
87
87
|
|
|
88
88
|
begin
|
|
89
|
-
@pipeline_size.times { @drivers << start_pipeline_driver
|
|
89
|
+
@pipeline_size.times { @drivers << start_pipeline_driver }
|
|
90
90
|
@driver_backoff = Array.new(@drivers.size, 0.0)
|
|
91
91
|
@driver_attempts = Array.new(@drivers.size, 0)
|
|
92
92
|
@driver_last_health = Array.new(@drivers.size, monotonic)
|
|
93
|
-
@pinned_gate =
|
|
93
|
+
@pinned_gate = Runtime::Semaphore.new(@pinned_size) if @pinned_size.positive?
|
|
94
94
|
@started = true
|
|
95
|
-
@supervisor =
|
|
95
|
+
@supervisor = Runtime.spawn(name: :supervisor) { supervise } if @reconnect || @health_check
|
|
96
96
|
rescue Exception
|
|
97
97
|
cleanup_partial_start
|
|
98
98
|
raise
|
|
@@ -106,7 +106,8 @@ module PgPipeline
|
|
|
106
106
|
def pipeline_driver
|
|
107
107
|
ensure_available!
|
|
108
108
|
|
|
109
|
-
driver
|
|
109
|
+
driver = PoolOps.select_driver_into(@drivers, @rr, @rr_slot)
|
|
110
|
+
@rr = @rr_slot[0]
|
|
110
111
|
raise NotDispatchedError, "no live pipeline connections; request was not dispatched" unless driver
|
|
111
112
|
|
|
112
113
|
driver
|
|
@@ -172,9 +173,10 @@ module PgPipeline
|
|
|
172
173
|
raise @pinned_error if @pinned_error
|
|
173
174
|
raise Error, "pinned pool is disabled (pinned_size=0)" if @pinned_size.zero?
|
|
174
175
|
|
|
175
|
-
|
|
176
|
+
owner = Fiber.current
|
|
177
|
+
if @pinned_owners[owner].positive?
|
|
176
178
|
raise RecursiveCheckoutError,
|
|
177
|
-
"nested Client#session/transaction on the same
|
|
179
|
+
"nested Client#session/transaction on the same fiber is not allowed; " \
|
|
178
180
|
"use Transaction#savepoint for nested atomicity"
|
|
179
181
|
end
|
|
180
182
|
|
|
@@ -182,7 +184,6 @@ module PgPipeline
|
|
|
182
184
|
raise @pinned_error if @pinned_error
|
|
183
185
|
raise ShutdownError, "pool is closing" if @closing
|
|
184
186
|
|
|
185
|
-
owner = Async::Task.current
|
|
186
187
|
conn = nil
|
|
187
188
|
@pinned_active += 1
|
|
188
189
|
@pinned_owners[owner] += 1
|
|
@@ -218,6 +219,7 @@ module PgPipeline
|
|
|
218
219
|
reconnects: @reconnects,
|
|
219
220
|
health_failures: @health_failures,
|
|
220
221
|
supervisor_error: @supervisor_error&.message,
|
|
222
|
+
supervisor_alive: supervisor_alive?,
|
|
221
223
|
closing: @closing,
|
|
222
224
|
closed: @closed,
|
|
223
225
|
pinned_error: @pinned_error&.message
|
|
@@ -227,7 +229,7 @@ module PgPipeline
|
|
|
227
229
|
def graceful_close
|
|
228
230
|
return unless @started
|
|
229
231
|
|
|
230
|
-
if @pinned_owners[
|
|
232
|
+
if @pinned_owners[Fiber.current].positive?
|
|
231
233
|
raise Error, "cannot close the pool from inside Client#session/transaction"
|
|
232
234
|
end
|
|
233
235
|
|
|
@@ -282,7 +284,7 @@ module PgPipeline
|
|
|
282
284
|
def supervise
|
|
283
285
|
until @closing
|
|
284
286
|
begin
|
|
285
|
-
reap_and_replace
|
|
287
|
+
reap_and_replace if @reconnect
|
|
286
288
|
health_probe if @health_check
|
|
287
289
|
@supervisor_error = nil
|
|
288
290
|
rescue StandardError => e
|
|
@@ -290,9 +292,29 @@ module PgPipeline
|
|
|
290
292
|
end
|
|
291
293
|
|
|
292
294
|
break if @closing
|
|
295
|
+
break unless supervisor_pause
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
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
|
|
293
306
|
|
|
294
|
-
|
|
307
|
+
unless ENV["PG_PIPELINE_SILENCE_WARNINGS"]
|
|
308
|
+
warn("pg_pipeline: supervisor pause failed (#{e.class}: #{e.message}) " \
|
|
309
|
+
"[#{@pause_failures}/#{MAX_PAUSE_FAILURES}]")
|
|
295
310
|
end
|
|
311
|
+
|
|
312
|
+
@pause_failures < MAX_PAUSE_FAILURES
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def supervisor_alive?
|
|
316
|
+
supervisor = @supervisor
|
|
317
|
+
!supervisor.nil? && !supervisor.finished?
|
|
296
318
|
end
|
|
297
319
|
|
|
298
320
|
def supervisor_sleep_interval
|
|
@@ -303,7 +325,7 @@ module PgPipeline
|
|
|
303
325
|
interval.positive? ? interval : @reconnect_interval
|
|
304
326
|
end
|
|
305
327
|
|
|
306
|
-
def reap_and_replace
|
|
328
|
+
def reap_and_replace
|
|
307
329
|
now = monotonic
|
|
308
330
|
ensure_driver_slots!
|
|
309
331
|
|
|
@@ -314,7 +336,7 @@ module PgPipeline
|
|
|
314
336
|
next if now < (@driver_backoff[index] || 0.0)
|
|
315
337
|
|
|
316
338
|
begin
|
|
317
|
-
@drivers[index] = start_pipeline_driver
|
|
339
|
+
@drivers[index] = start_pipeline_driver
|
|
318
340
|
@driver_backoff[index] = 0.0
|
|
319
341
|
@driver_attempts[index] = 0
|
|
320
342
|
@driver_last_health[index] = monotonic
|
|
@@ -337,10 +359,16 @@ module PgPipeline
|
|
|
337
359
|
next if now - (@driver_last_health[index] || 0.0) < @health_interval
|
|
338
360
|
|
|
339
361
|
@driver_last_health[index] = now
|
|
340
|
-
next if driver.health_check(@health_timeout)
|
|
341
362
|
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
344
372
|
end
|
|
345
373
|
end
|
|
346
374
|
|
|
@@ -374,10 +402,21 @@ module PgPipeline
|
|
|
374
402
|
def stop_supervisor
|
|
375
403
|
supervisor = @supervisor
|
|
376
404
|
@supervisor = nil
|
|
377
|
-
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
|
+
|
|
378
419
|
nil
|
|
379
|
-
rescue CANCEL_SIGNAL => e
|
|
380
|
-
e
|
|
381
420
|
rescue StandardError
|
|
382
421
|
nil
|
|
383
422
|
end
|
|
@@ -411,11 +450,11 @@ module PgPipeline
|
|
|
411
450
|
raise Error, "pool not started" unless @started
|
|
412
451
|
end
|
|
413
452
|
|
|
414
|
-
def start_pipeline_driver
|
|
453
|
+
def start_pipeline_driver
|
|
415
454
|
conn = PoolOps.new_connection(@connection_args)
|
|
416
455
|
prepare_registered_statements(conn)
|
|
417
456
|
driver = ConnectionDriver.new(conn, max_pending: @max_pending, max_in_flight: @max_in_flight)
|
|
418
|
-
driver.start
|
|
457
|
+
driver.start
|
|
419
458
|
rescue Exception
|
|
420
459
|
PoolOps.safe_close(conn) if conn
|
|
421
460
|
raise
|
|
@@ -445,28 +484,26 @@ module PgPipeline
|
|
|
445
484
|
end
|
|
446
485
|
|
|
447
486
|
def release_pinned(owner, conn)
|
|
448
|
-
|
|
449
|
-
if
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
@pinned_free << recycled
|
|
460
|
-
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
|
|
461
498
|
end
|
|
462
499
|
end
|
|
463
500
|
end
|
|
464
|
-
ensure
|
|
465
|
-
@pinned_active -= 1
|
|
466
|
-
@pinned_owners[owner] -= 1
|
|
467
|
-
@pinned_owners.delete(owner) if @pinned_owners[owner].zero?
|
|
468
|
-
@pinned_idle.signal if @pinned_active.zero?
|
|
469
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?
|
|
470
507
|
end
|
|
471
508
|
|
|
472
509
|
def recycle_pinned_connection(conn)
|
|
@@ -511,7 +548,6 @@ module PgPipeline
|
|
|
511
548
|
close_free_pinned
|
|
512
549
|
@pinned_gate = nil
|
|
513
550
|
@supervisor = nil
|
|
514
|
-
@task_parent = nil
|
|
515
551
|
@started = false
|
|
516
552
|
@closing = false
|
|
517
553
|
@closed = false
|
|
@@ -557,28 +593,42 @@ module PgPipeline
|
|
|
557
593
|
raise ArgumentError, "#{name} must be an integer >= 0"
|
|
558
594
|
end
|
|
559
595
|
|
|
560
|
-
def
|
|
596
|
+
def select_driver_into(drivers, rr, slot)
|
|
561
597
|
size = drivers.length
|
|
562
|
-
|
|
598
|
+
if size.zero?
|
|
599
|
+
slot[0] = rr
|
|
600
|
+
return nil
|
|
601
|
+
end
|
|
563
602
|
|
|
603
|
+
start = rr % size
|
|
564
604
|
best = nil
|
|
565
|
-
best_index =
|
|
566
|
-
best_load =
|
|
605
|
+
best_index = 0
|
|
606
|
+
best_load = 0
|
|
607
|
+
offset = 0
|
|
567
608
|
|
|
568
|
-
|
|
569
|
-
index =
|
|
609
|
+
while offset < size
|
|
610
|
+
index = start + offset
|
|
611
|
+
index -= size if index >= size
|
|
570
612
|
driver = drivers[index]
|
|
613
|
+
offset += 1
|
|
571
614
|
next unless driver.available?
|
|
572
615
|
|
|
573
616
|
load = driver.load
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
617
|
+
next unless best.nil? || load < best_load
|
|
618
|
+
|
|
619
|
+
best = driver
|
|
620
|
+
best_index = index
|
|
621
|
+
best_load = load
|
|
579
622
|
end
|
|
580
623
|
|
|
581
|
-
best ?
|
|
624
|
+
slot[0] = best ? (best_index + 1) % size : rr
|
|
625
|
+
best
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def select_driver(drivers, rr)
|
|
629
|
+
slot = [rr]
|
|
630
|
+
driver = select_driver_into(drivers, rr, slot)
|
|
631
|
+
[driver, slot[0]]
|
|
582
632
|
end
|
|
583
633
|
|
|
584
634
|
def new_connection(connection_args)
|
data/lib/pg_pipeline/request.rb
CHANGED
|
@@ -22,10 +22,16 @@ module PgPipeline
|
|
|
22
22
|
@waiter_scheduler = nil
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def self.build(sql, params)
|
|
26
|
+
request = allocate
|
|
27
|
+
request.__send__(:init_query, sql, params)
|
|
28
|
+
request
|
|
29
|
+
end
|
|
30
|
+
|
|
25
31
|
def self.prepare(statement) = PrepareRequest.new(statement)
|
|
26
32
|
|
|
27
33
|
def self.prepared_query(statement, params: nil)
|
|
28
|
-
PreparedQueryRequest.
|
|
34
|
+
PreparedQueryRequest.build(statement, params)
|
|
29
35
|
end
|
|
30
36
|
|
|
31
37
|
def operation = :query
|
|
@@ -43,6 +49,23 @@ module PgPipeline
|
|
|
43
49
|
def query_boundary_seen? = @query_boundary_seen
|
|
44
50
|
def cancelled? = @cancelled
|
|
45
51
|
def settled? = @settled
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def init_query(sql, params)
|
|
56
|
+
@sql = RequestOps.snapshot_sql(sql)
|
|
57
|
+
@params = RequestOps.snapshot_params(params)
|
|
58
|
+
@state = :new
|
|
59
|
+
@cancelled = false
|
|
60
|
+
@settled = false
|
|
61
|
+
@result_seen = false
|
|
62
|
+
@query_boundary_seen = false
|
|
63
|
+
@result = nil
|
|
64
|
+
@error = nil
|
|
65
|
+
@waiter = nil
|
|
66
|
+
@waiter_scheduler = nil
|
|
67
|
+
self
|
|
68
|
+
end
|
|
46
69
|
end
|
|
47
70
|
|
|
48
71
|
class PrepareRequest < Request
|
|
@@ -65,7 +88,21 @@ module PgPipeline
|
|
|
65
88
|
@statement_name = RequestOps.snapshot_name(statement.physical_name)
|
|
66
89
|
end
|
|
67
90
|
|
|
91
|
+
def self.build(statement, params)
|
|
92
|
+
request = allocate
|
|
93
|
+
request.__send__(:init_prepared, statement, params)
|
|
94
|
+
request
|
|
95
|
+
end
|
|
96
|
+
|
|
68
97
|
def operation = :prepared_query
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def init_prepared(statement, params)
|
|
102
|
+
init_query(statement.sql, params)
|
|
103
|
+
@statement_name = RequestOps.snapshot_name(statement.physical_name)
|
|
104
|
+
self
|
|
105
|
+
end
|
|
69
106
|
end
|
|
70
107
|
|
|
71
108
|
module RequestOps
|
|
@@ -76,11 +113,34 @@ module PgPipeline
|
|
|
76
113
|
value.frozen? ? value : value.dup.freeze
|
|
77
114
|
end
|
|
78
115
|
|
|
116
|
+
EMPTY_PARAMS = [].freeze
|
|
117
|
+
|
|
79
118
|
def snapshot_params(params)
|
|
80
|
-
|
|
81
|
-
raise ArgumentError, "params must be an Array" unless
|
|
119
|
+
return EMPTY_PARAMS if params.nil?
|
|
120
|
+
raise ArgumentError, "params must be an Array" unless params.is_a?(Array)
|
|
121
|
+
return EMPTY_PARAMS if params.empty?
|
|
122
|
+
|
|
123
|
+
return params if params.frozen? && immutable_values?(params)
|
|
82
124
|
|
|
83
|
-
|
|
125
|
+
params.map { |value| snapshot_value(value) }.freeze
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def immutable_values?(values)
|
|
129
|
+
index = 0
|
|
130
|
+
size = values.size
|
|
131
|
+
while index < size
|
|
132
|
+
value = values[index]
|
|
133
|
+
case value
|
|
134
|
+
when Integer, Float, Symbol, NilClass, TrueClass, FalseClass
|
|
135
|
+
nil
|
|
136
|
+
when String
|
|
137
|
+
return false unless value.frozen?
|
|
138
|
+
else
|
|
139
|
+
return false
|
|
140
|
+
end
|
|
141
|
+
index += 1
|
|
142
|
+
end
|
|
143
|
+
true
|
|
84
144
|
end
|
|
85
145
|
|
|
86
146
|
def snapshot_value(value)
|
|
@@ -195,13 +255,11 @@ module PgPipeline
|
|
|
195
255
|
req.waiter = waiter
|
|
196
256
|
req.waiter_scheduler = scheduler
|
|
197
257
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
req.waiter_scheduler = nil
|
|
204
|
-
end
|
|
258
|
+
scheduler.block(req, nil) until req.settled
|
|
259
|
+
ensure
|
|
260
|
+
if waiter && req.waiter.equal?(waiter)
|
|
261
|
+
req.waiter = nil
|
|
262
|
+
req.waiter_scheduler = nil
|
|
205
263
|
end
|
|
206
264
|
end
|
|
207
265
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgPipeline
|
|
4
|
+
module Runtime
|
|
5
|
+
class Notification
|
|
6
|
+
def initialize
|
|
7
|
+
@waiters = []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def wait(timeout = nil)
|
|
11
|
+
Runtime.with_waiter(self, @waiters) do |waiter|
|
|
12
|
+
Runtime.with_timeout(timeout) { Runtime.park(self, waiter) { false } }
|
|
13
|
+
true
|
|
14
|
+
rescue TimeoutError
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def signal
|
|
20
|
+
waiter = @waiters.shift or return false
|
|
21
|
+
Runtime.wake_dequeued(waiter, self)
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def signal_all
|
|
26
|
+
pending, @waiters = @waiters, []
|
|
27
|
+
pending.each { |waiter| Runtime.wake_dequeued(waiter, self) }
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgPipeline
|
|
4
|
+
module Runtime
|
|
5
|
+
class Queue
|
|
6
|
+
def initialize
|
|
7
|
+
@queue = ::Thread::Queue.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def enqueue(item)
|
|
11
|
+
@queue.push(item)
|
|
12
|
+
item
|
|
13
|
+
rescue ClosedQueueError
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def dequeue
|
|
18
|
+
@queue.pop
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def close
|
|
22
|
+
@queue.close
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def closed? = @queue.closed?
|
|
27
|
+
def empty? = @queue.empty?
|
|
28
|
+
def size = @queue.size
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgPipeline
|
|
4
|
+
module Runtime
|
|
5
|
+
class Semaphore
|
|
6
|
+
def initialize(limit)
|
|
7
|
+
@limit = Integer(limit)
|
|
8
|
+
raise ArgumentError, "limit must be >= 0" if @limit.negative?
|
|
9
|
+
|
|
10
|
+
@available = @limit
|
|
11
|
+
@waiting = WaitList.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def acquire
|
|
15
|
+
wait
|
|
16
|
+
return @available unless block_given?
|
|
17
|
+
|
|
18
|
+
begin
|
|
19
|
+
yield
|
|
20
|
+
ensure
|
|
21
|
+
release
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def release
|
|
26
|
+
if (node = @waiting.shift)
|
|
27
|
+
node.grant!
|
|
28
|
+
node.resume
|
|
29
|
+
else
|
|
30
|
+
@available += 1
|
|
31
|
+
end
|
|
32
|
+
@available
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def wait
|
|
38
|
+
return (@available -= 1) if @available.positive?
|
|
39
|
+
|
|
40
|
+
task = Runtime.current_task
|
|
41
|
+
node = FiberNode.new(Fiber.current, self)
|
|
42
|
+
@waiting.push(node)
|
|
43
|
+
task&.enter_block(node.waiter)
|
|
44
|
+
|
|
45
|
+
begin
|
|
46
|
+
until node.granted?
|
|
47
|
+
node.suspend
|
|
48
|
+
abandon_wait!(node, task) if task&.cancelled?
|
|
49
|
+
end
|
|
50
|
+
abandon_wait!(node, task) if task&.cancelled?
|
|
51
|
+
ensure
|
|
52
|
+
task&.exit_block
|
|
53
|
+
@waiting.remove(node) unless node.granted? || node.list.nil?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def abandon_wait!(node, task)
|
|
58
|
+
node.granted? ? release : @waiting.remove(node)
|
|
59
|
+
task.raise_if_cancelled!
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class WaitList
|
|
63
|
+
attr_reader :size
|
|
64
|
+
|
|
65
|
+
def initialize
|
|
66
|
+
@head = @tail = nil
|
|
67
|
+
@size = 0
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def first = @head
|
|
71
|
+
def empty? = @head.nil?
|
|
72
|
+
|
|
73
|
+
def push(node)
|
|
74
|
+
raise ArgumentError, "node already queued" if node.list
|
|
75
|
+
|
|
76
|
+
node.list = self
|
|
77
|
+
node.prev = @tail
|
|
78
|
+
node.next = nil
|
|
79
|
+
@tail ? (@tail.next = node) : (@head = node)
|
|
80
|
+
@tail = node
|
|
81
|
+
@size += 1
|
|
82
|
+
node
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def shift
|
|
86
|
+
node = @head or return nil
|
|
87
|
+
remove(node)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def remove(node)
|
|
91
|
+
return nil unless node.list.equal?(self)
|
|
92
|
+
|
|
93
|
+
node.prev ? (node.prev.next = node.next) : (@head = node.next)
|
|
94
|
+
node.next ? (node.next.prev = node.prev) : (@tail = node.prev)
|
|
95
|
+
node.list = node.prev = node.next = nil
|
|
96
|
+
@size -= 1
|
|
97
|
+
node
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class FiberNode
|
|
102
|
+
attr_accessor :list, :prev, :next
|
|
103
|
+
attr_reader :fiber, :waiter, :blocker
|
|
104
|
+
|
|
105
|
+
def initialize(fiber, blocker)
|
|
106
|
+
@fiber = fiber
|
|
107
|
+
@blocker = blocker
|
|
108
|
+
@list = @prev = @next = nil
|
|
109
|
+
@granted = false
|
|
110
|
+
@scheduler = Fiber.scheduler
|
|
111
|
+
@waiter = {fiber: fiber, scheduler: @scheduler, ready: false, blocker: blocker, queued: true}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def granted? = @granted
|
|
115
|
+
|
|
116
|
+
def grant!
|
|
117
|
+
@granted = true
|
|
118
|
+
@waiter[:ready] = true
|
|
119
|
+
@waiter[:queued] = false
|
|
120
|
+
self
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def suspend
|
|
124
|
+
@scheduler.block(@blocker, nil)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def resume
|
|
128
|
+
fiber = @fiber
|
|
129
|
+
return unless fiber.alive?
|
|
130
|
+
|
|
131
|
+
@scheduler.unblock(@blocker, fiber)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private_constant :WaitList, :FiberNode
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|