pg_pipeline 0.2.5 → 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.
@@ -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 = Async::Cancel
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
- @task_parent = nil
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 = Async::Notification.new
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(parent: nil)
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(parent) }
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 = Async::Semaphore.new(@pinned_size) if @pinned_size.positive?
93
+ @pinned_gate = Runtime::Semaphore.new(@pinned_size) if @pinned_size.positive?
95
94
  @started = true
96
- @supervisor = parent.async { supervise } if @reconnect || @health_check
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
- if @pinned_owners[Async::Task.current].positive?
176
+ owner = Fiber.current
177
+ if @pinned_owners[owner].positive?
178
178
  raise RecursiveCheckoutError,
179
- "nested Client#session/transaction on the same task is not allowed; " \
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[Async::Task.current].positive?
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(@task_parent) if @reconnect
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
- sleep(supervisor_sleep_interval)
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(parent)
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(parent)
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
- @health_failures += 1
345
- driver.abort!
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&.stop
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(parent)
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(parent: parent)
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
- begin
451
- if conn
452
- if @closing
453
- PoolOps.safe_close(conn)
454
- else
455
- recycled = recycle_pinned_connection(conn)
456
-
457
- if recycled
458
- if @closing
459
- PoolOps.safe_close(recycled)
460
- else
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
@@ -255,13 +255,11 @@ module PgPipeline
255
255
  req.waiter = waiter
256
256
  req.waiter_scheduler = scheduler
257
257
 
258
- begin
259
- scheduler.block(req, nil) until req.settled
260
- ensure
261
- if req.waiter.equal?(waiter)
262
- req.waiter = nil
263
- req.waiter_scheduler = nil
264
- 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
265
263
  end
266
264
  end
267
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
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgPipeline
4
+ module Runtime
5
+ class Task
6
+ attr_reader :fiber, :name, :error
7
+
8
+ def self.spawn(name: nil, &block)
9
+ raise ArgumentError, "block required" unless block
10
+
11
+ task = new(name: name)
12
+ task.__send__(:start, &block)
13
+ task
14
+ end
15
+
16
+ def initialize(name: nil)
17
+ @fiber, @scheduler, @blocker, @result, @error = nil, nil, nil, nil, nil
18
+ @done, @cancelled = false, false
19
+
20
+ @name = name
21
+ @waiters = []
22
+ end
23
+
24
+ def wait(timeout = nil)
25
+ Runtime.with_timeout(timeout) { join }
26
+ raise @error if @error
27
+
28
+ @result
29
+ end
30
+
31
+ def stop
32
+ return false if @done
33
+
34
+ @cancelled = true
35
+
36
+ return true if interrupt_fiber
37
+ return true if release_blocker
38
+
39
+ false
40
+ end
41
+
42
+ def cancelled? = @cancelled
43
+
44
+ def raise_if_cancelled!
45
+ raise Cancel, "task stopped" if @cancelled
46
+ end
47
+
48
+ def enter_block(waiter)
49
+ @blocker = waiter
50
+ end
51
+
52
+ def exit_block
53
+ @blocker = nil
54
+ end
55
+
56
+ def finished? = @done
57
+
58
+ private
59
+
60
+ def start(&block)
61
+ @scheduler = Runtime.scheduler!
62
+ this = self
63
+
64
+ scheduled = Fiber.schedule do
65
+ this.__send__(:adopt_fiber, Fiber.current)
66
+ Thread.current[Runtime::CURRENT_TASK_KEY] = this
67
+ begin
68
+ this.__send__(:complete, block.call, nil)
69
+ rescue Cancel
70
+ this.__send__(:complete, nil, nil)
71
+ rescue Exception => e
72
+ this.__send__(:complete, nil, e)
73
+ end
74
+ end
75
+
76
+ @fiber ||= scheduled
77
+ end
78
+
79
+ def adopt_fiber(fiber)
80
+ @fiber ||= fiber
81
+ end
82
+
83
+ def interrupt_fiber
84
+ fiber = @fiber
85
+ scheduler = @scheduler
86
+ return false unless fiber&.alive?
87
+ return false unless scheduler&.respond_to?(:fiber_interrupt)
88
+
89
+ result = scheduler.fiber_interrupt(fiber, Cancel.new("task stopped"))
90
+ result != false
91
+ rescue FiberError
92
+ false
93
+ end
94
+
95
+ def release_blocker
96
+ waiter = @blocker or return false
97
+ Runtime.wake(waiter, waiter[:blocker])
98
+ waiter[:fiber].alive?
99
+ end
100
+
101
+ def complete(result, error)
102
+ return if @done
103
+
104
+ @result = result
105
+ @error = error
106
+ @done = true
107
+ wake_waiters
108
+ end
109
+
110
+ def join
111
+ return if @done
112
+
113
+ Runtime.with_waiter(self, @waiters) do |waiter|
114
+ Runtime.park(self, waiter) { @done }
115
+ end
116
+ end
117
+
118
+ def wake_waiters
119
+ pending, @waiters = @waiters, []
120
+ pending.each { |waiter| Runtime.wake_dequeued(waiter, self) }
121
+ end
122
+ end
123
+ end
124
+ end