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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/README.md +54 -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 +128 -71
- 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/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
|
|
|
@@ -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
|
data/lib/pg_pipeline/request.rb
CHANGED
|
@@ -255,13 +255,11 @@ module PgPipeline
|
|
|
255
255
|
req.waiter = waiter
|
|
256
256
|
req.waiter_scheduler = scheduler
|
|
257
257
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|