async 2.17.0 → 2.32.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
- checksums.yaml.gz.sig +0 -0
- data/context/best-practices.md +188 -0
- data/context/debugging.md +63 -0
- data/context/getting-started.md +177 -0
- data/context/index.yaml +29 -0
- data/context/scheduler.md +109 -0
- data/context/tasks.md +448 -0
- data/context/thread-safety.md +651 -0
- data/lib/async/barrier.md +1 -2
- data/lib/async/barrier.rb +35 -12
- data/lib/async/clock.rb +11 -2
- data/lib/async/condition.md +1 -1
- data/lib/async/condition.rb +18 -34
- data/lib/async/console.rb +42 -0
- data/lib/async/deadline.rb +70 -0
- data/lib/async/idler.rb +2 -1
- data/lib/async/limited_queue.rb +13 -0
- data/lib/async/list.rb +16 -8
- data/lib/async/node.rb +5 -3
- data/lib/async/notification.rb +13 -9
- data/lib/async/priority_queue.rb +253 -0
- data/lib/async/promise.rb +188 -0
- data/lib/async/queue.rb +70 -82
- data/lib/async/reactor.rb +4 -2
- data/lib/async/scheduler.rb +233 -54
- data/lib/async/semaphore.rb +3 -3
- data/lib/async/stop.rb +82 -0
- data/lib/async/task.rb +111 -81
- data/lib/async/timeout.rb +88 -0
- data/lib/async/variable.rb +15 -4
- data/lib/async/version.rb +2 -2
- data/lib/async/waiter.rb +6 -1
- data/lib/kernel/async.rb +1 -1
- data/lib/kernel/sync.rb +14 -5
- data/lib/metrics/provider/async/task.rb +20 -0
- data/lib/metrics/provider/async.rb +6 -0
- data/lib/traces/provider/async/barrier.rb +17 -0
- data/lib/traces/provider/async/task.rb +40 -0
- data/lib/traces/provider/async.rb +7 -0
- data/license.md +8 -1
- data/readme.md +50 -7
- data/releases.md +357 -0
- data.tar.gz.sig +0 -0
- metadata +61 -20
- metadata.gz.sig +0 -0
- data/lib/async/waiter.md +0 -50
- data/lib/async/wrapper.rb +0 -65
data/lib/async/barrier.rb
CHANGED
@@ -1,21 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2019-
|
4
|
+
# Copyright, 2019-2025, by Samuel Williams.
|
5
5
|
|
6
|
-
require_relative
|
7
|
-
require_relative
|
6
|
+
require_relative "list"
|
7
|
+
require_relative "task"
|
8
|
+
require_relative "queue"
|
8
9
|
|
9
10
|
module Async
|
10
11
|
# A general purpose synchronisation primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore}.
|
11
12
|
#
|
12
|
-
# @public Since
|
13
|
+
# @public Since *Async v1*.
|
13
14
|
class Barrier
|
14
15
|
# Initialize the barrier.
|
15
16
|
# @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks.
|
16
|
-
# @public Since
|
17
|
+
# @public Since *Async v1*.
|
17
18
|
def initialize(parent: nil)
|
18
19
|
@tasks = List.new
|
20
|
+
@finished = Queue.new
|
19
21
|
|
20
22
|
@parent = parent
|
21
23
|
end
|
@@ -41,11 +43,17 @@ module Async
|
|
41
43
|
# Execute a child task and add it to the barrier.
|
42
44
|
# @asynchronous Executes the given block concurrently.
|
43
45
|
def async(*arguments, parent: (@parent or Task.current), **options, &block)
|
44
|
-
|
46
|
+
raise "Barrier is stopped!" if @finished.closed?
|
45
47
|
|
46
|
-
|
48
|
+
waiting = nil
|
47
49
|
|
48
|
-
|
50
|
+
parent.async(*arguments, **options) do |task, *arguments|
|
51
|
+
waiting = TaskNode.new(task)
|
52
|
+
@tasks.append(waiting)
|
53
|
+
block.call(task, *arguments)
|
54
|
+
ensure
|
55
|
+
@finished.signal(waiting) unless @finished.closed?
|
56
|
+
end
|
49
57
|
end
|
50
58
|
|
51
59
|
# Whether there are any tasks being held by the barrier.
|
@@ -55,14 +63,27 @@ module Async
|
|
55
63
|
end
|
56
64
|
|
57
65
|
# Wait for all tasks to complete by invoking {Task#wait} on each waiting task, which may raise an error. As long as the task has completed, it will be removed from the barrier.
|
66
|
+
#
|
67
|
+
# @yields {|task| ...} If a block is given, the unwaited task is yielded. You must invoke {Task#wait} yourself. In addition, you may `break` if you have captured enough results.
|
68
|
+
#
|
58
69
|
# @asynchronous Will wait for tasks to finish executing.
|
59
70
|
def wait
|
60
|
-
|
71
|
+
while !@tasks.empty?
|
72
|
+
# Wait for a task to finish (we get the task node):
|
73
|
+
return unless waiting = @finished.wait
|
74
|
+
|
75
|
+
# Remove the task as it is now finishing:
|
76
|
+
@tasks.remove?(waiting)
|
77
|
+
|
78
|
+
# Get the task:
|
61
79
|
task = waiting.task
|
62
|
-
|
80
|
+
|
81
|
+
# If a block is given, the user can implement their own behaviour:
|
82
|
+
if block_given?
|
83
|
+
yield task
|
84
|
+
else
|
85
|
+
# Wait for it to either complete or raise an error:
|
63
86
|
task.wait
|
64
|
-
ensure
|
65
|
-
@tasks.remove?(waiting) unless task.alive?
|
66
87
|
end
|
67
88
|
end
|
68
89
|
end
|
@@ -73,6 +94,8 @@ module Async
|
|
73
94
|
@tasks.each do |waiting|
|
74
95
|
waiting.task.stop
|
75
96
|
end
|
97
|
+
|
98
|
+
@finished.close
|
76
99
|
end
|
77
100
|
end
|
78
101
|
end
|
data/lib/async/clock.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
module Async
|
7
7
|
# A convenient wrapper around the internal monotonic clock.
|
8
|
-
# @public Since
|
8
|
+
# @public Since *Async v1*.
|
9
9
|
class Clock
|
10
10
|
# Get the current elapsed monotonic time.
|
11
11
|
def self.now
|
@@ -61,5 +61,14 @@ module Async
|
|
61
61
|
|
62
62
|
return total
|
63
63
|
end
|
64
|
+
|
65
|
+
# Reset the total elapsed time. If the clock is currently running, reset the start time to now.
|
66
|
+
def reset!
|
67
|
+
@total = 0
|
68
|
+
|
69
|
+
if @started
|
70
|
+
@started = Clock.now
|
71
|
+
end
|
72
|
+
end
|
64
73
|
end
|
65
74
|
end
|
data/lib/async/condition.md
CHANGED
data/lib/async/condition.rb
CHANGED
@@ -1,75 +1,59 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2017-
|
4
|
+
# Copyright, 2017-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2017, by Kent Gruber.
|
6
6
|
|
7
|
-
require
|
8
|
-
require_relative
|
7
|
+
require "fiber"
|
8
|
+
require_relative "list"
|
9
9
|
|
10
10
|
module Async
|
11
11
|
# A synchronization primitive, which allows fibers to wait until a particular condition is (edge) triggered.
|
12
|
-
# @public Since
|
12
|
+
# @public Since *Async v1*.
|
13
13
|
class Condition
|
14
14
|
# Create a new condition.
|
15
15
|
def initialize
|
16
|
-
@
|
16
|
+
@ready = ::Thread::Queue.new
|
17
17
|
end
|
18
18
|
|
19
|
-
class FiberNode < List::Node
|
20
|
-
def initialize(fiber)
|
21
|
-
@fiber = fiber
|
22
|
-
end
|
23
|
-
|
24
|
-
def transfer(*arguments)
|
25
|
-
@fiber.transfer(*arguments)
|
26
|
-
end
|
27
|
-
|
28
|
-
def alive?
|
29
|
-
@fiber.alive?
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
private_constant :FiberNode
|
34
|
-
|
35
19
|
# Queue up the current fiber and wait on yielding the task.
|
36
20
|
# @returns [Object]
|
37
21
|
def wait
|
38
|
-
@
|
39
|
-
Fiber.scheduler.transfer
|
40
|
-
end
|
22
|
+
@ready.pop
|
41
23
|
end
|
42
24
|
|
43
|
-
# @
|
25
|
+
# @returns [Boolean] If there are no fibers waiting on this condition.
|
44
26
|
def empty?
|
45
|
-
@
|
27
|
+
@ready.num_waiting.zero?
|
46
28
|
end
|
47
29
|
|
48
30
|
# @returns [Boolean] Is any fiber waiting on this notification?
|
49
31
|
def waiting?
|
50
|
-
|
32
|
+
!self.empty?
|
51
33
|
end
|
52
34
|
|
53
35
|
# Signal to a given task that it should resume operations.
|
54
36
|
# @parameter value [Object | Nil] The value to return to the waiting fibers.
|
55
37
|
def signal(value = nil)
|
56
|
-
return if
|
38
|
+
return if empty?
|
57
39
|
|
58
|
-
|
40
|
+
ready = self.exchange
|
59
41
|
|
60
|
-
|
61
|
-
|
42
|
+
ready.num_waiting.times do
|
43
|
+
ready.push(value)
|
62
44
|
end
|
63
45
|
|
46
|
+
ready.close
|
47
|
+
|
64
48
|
return nil
|
65
49
|
end
|
66
50
|
|
67
51
|
protected
|
68
52
|
|
69
53
|
def exchange
|
70
|
-
|
71
|
-
@
|
72
|
-
return
|
54
|
+
ready = @ready
|
55
|
+
@ready = ::Thread::Queue.new
|
56
|
+
return ready
|
73
57
|
end
|
74
58
|
end
|
75
59
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2024, by Samuel Williams.
|
5
|
+
|
6
|
+
module Async
|
7
|
+
# Shims for the console gem, redirecting warnings and above to `Kernel#warn`.
|
8
|
+
#
|
9
|
+
# If you require this file, the `async` library will not depend on the `console` gem.
|
10
|
+
#
|
11
|
+
# That includes any gems that sit within the `Async` namespace.
|
12
|
+
#
|
13
|
+
# This is an experimental feature.
|
14
|
+
module Console
|
15
|
+
# Log a message at the debug level. The shim is silent.
|
16
|
+
def self.debug(...)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Log a message at the info level. The shim is silent.
|
20
|
+
def self.info(...)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Log a message at the warn level. The shim redirects to `Kernel#warn`.
|
24
|
+
def self.warn(*arguments, exception: nil, **options)
|
25
|
+
if exception
|
26
|
+
super(*arguments, exception.full_message, **options)
|
27
|
+
else
|
28
|
+
super(*arguments, **options)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Log a message at the error level. The shim redirects to `Kernel#warn`.
|
33
|
+
def self.error(...)
|
34
|
+
self.warn(...)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Log a message at the fatal level. The shim redirects to `Kernel#warn`.
|
38
|
+
def self.fatal(...)
|
39
|
+
self.warn(...)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative "clock"
|
7
|
+
|
8
|
+
# @namespace
|
9
|
+
module Async
|
10
|
+
# Represents a deadline timeout with decrementing remaining time.
|
11
|
+
# Includes an efficient representation for zero (non-blocking) timeouts.
|
12
|
+
# @public Since *Async v2.31*.
|
13
|
+
class Deadline
|
14
|
+
# Singleton module for immediate timeouts (zero or negative).
|
15
|
+
# Avoids object allocation for fast path (non-blocking) timeouts.
|
16
|
+
module Zero
|
17
|
+
# Check if the deadline has expired.
|
18
|
+
# @returns [Boolean] Always returns true since zero timeouts are immediately expired.
|
19
|
+
def self.expired?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
# Get the remaining time.
|
24
|
+
# @returns [Integer] Always returns 0 since zero timeouts have no remaining time.
|
25
|
+
def self.remaining
|
26
|
+
0
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create a deadline for the given timeout.
|
31
|
+
# @parameter timeout [Numeric | Nil] The timeout duration, or nil for no timeout.
|
32
|
+
# @returns [Deadline | Nil] A deadline instance, Zero singleton, or nil.
|
33
|
+
def self.start(timeout)
|
34
|
+
if timeout.nil?
|
35
|
+
nil
|
36
|
+
elsif timeout <= 0
|
37
|
+
Zero
|
38
|
+
else
|
39
|
+
self.new(timeout)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Create a new deadline with the specified remaining time.
|
44
|
+
# @parameter remaining [Numeric] The initial remaining time.
|
45
|
+
def initialize(remaining)
|
46
|
+
@remaining = remaining
|
47
|
+
@start = Clock.now
|
48
|
+
end
|
49
|
+
|
50
|
+
# Get the remaining time, updating internal state.
|
51
|
+
# Each call to this method advances the internal clock and reduces
|
52
|
+
# the remaining time by the elapsed duration since the last call.
|
53
|
+
# @returns [Numeric] The remaining time (may be negative if expired).
|
54
|
+
def remaining
|
55
|
+
now = Clock.now
|
56
|
+
delta = now - @start
|
57
|
+
@start = now
|
58
|
+
|
59
|
+
@remaining -= delta
|
60
|
+
|
61
|
+
return @remaining
|
62
|
+
end
|
63
|
+
|
64
|
+
# Check if the deadline has expired.
|
65
|
+
# @returns [Boolean] True if no time remains.
|
66
|
+
def expired?
|
67
|
+
self.remaining <= 0
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/async/idler.rb
CHANGED
@@ -7,7 +7,8 @@ module Async
|
|
7
7
|
# A load balancing mechanism that can be used process work when the system is idle.
|
8
8
|
class Idler
|
9
9
|
# Create a new idler.
|
10
|
-
#
|
10
|
+
#
|
11
|
+
# @public Since *Async v2*.
|
11
12
|
#
|
12
13
|
# @parameter maximum_load [Numeric] The maximum load before we start shedding work.
|
13
14
|
# @parameter backoff [Numeric] The initial backoff time, used for delaying work.
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
5
|
+
|
6
|
+
# The implementation lives in `queue.rb` but later we may move it here for better autoload/inference.
|
7
|
+
require_relative "queue"
|
8
|
+
|
9
|
+
module Async
|
10
|
+
class LimitedQueue < Queue
|
11
|
+
singleton_class.remove_method(:new)
|
12
|
+
end
|
13
|
+
end
|
data/lib/async/list.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2022-
|
4
|
+
# Copyright, 2022-2025, by Samuel Williams.
|
5
|
+
# Copyright, 2025, by Shopify Inc.
|
5
6
|
|
6
7
|
module Async
|
7
8
|
# A general doublely linked list. This is used internally by {Async::Barrier} and {Async::Condition} to manage child tasks.
|
@@ -18,6 +19,7 @@ module Async
|
|
18
19
|
sprintf("#<%s:0x%x size=%d>", self.class.name, object_id, @size)
|
19
20
|
end
|
20
21
|
|
22
|
+
# @returns [String] A short summary of the list.
|
21
23
|
alias inspect to_s
|
22
24
|
|
23
25
|
# Fast, safe, unbounded accumulation of children.
|
@@ -134,7 +136,7 @@ module Async
|
|
134
136
|
return removed(node)
|
135
137
|
end
|
136
138
|
|
137
|
-
# @returns [Boolean]
|
139
|
+
# @returns [Boolean] True if the list is empty.
|
138
140
|
def empty?
|
139
141
|
@size == 0
|
140
142
|
end
|
@@ -143,26 +145,26 @@ module Async
|
|
143
145
|
# previous = self
|
144
146
|
# current = @tail
|
145
147
|
# found = node.equal?(self)
|
146
|
-
|
148
|
+
|
147
149
|
# while true
|
148
150
|
# break if current.equal?(self)
|
149
|
-
|
151
|
+
|
150
152
|
# if current.head != previous
|
151
153
|
# raise "Invalid previous linked list node!"
|
152
154
|
# end
|
153
|
-
|
155
|
+
|
154
156
|
# if current.is_a?(List) and !current.equal?(self)
|
155
157
|
# raise "Invalid list in list node!"
|
156
158
|
# end
|
157
|
-
|
159
|
+
|
158
160
|
# if node
|
159
161
|
# found ||= current.equal?(node)
|
160
162
|
# end
|
161
|
-
|
163
|
+
|
162
164
|
# previous = current
|
163
165
|
# current = current.tail
|
164
166
|
# end
|
165
|
-
|
167
|
+
|
166
168
|
# if node and !found
|
167
169
|
# raise "Node not found in list!"
|
168
170
|
# end
|
@@ -238,6 +240,12 @@ module Async
|
|
238
240
|
attr_accessor :head
|
239
241
|
attr_accessor :tail
|
240
242
|
|
243
|
+
# @returns [String] A string representation of the node.
|
244
|
+
def to_s
|
245
|
+
sprintf("#<%s:0x%x>", self.class.name, object_id)
|
246
|
+
end
|
247
|
+
|
248
|
+
# @returns [String] A string representation of the node.
|
241
249
|
alias inspect to_s
|
242
250
|
end
|
243
251
|
|
data/lib/async/node.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2017-
|
4
|
+
# Copyright, 2017-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2017, by Kent Gruber.
|
6
6
|
# Copyright, 2022, by Shannon Skipper.
|
7
|
+
# Copyright, 2025, by Shopify Inc.
|
7
8
|
|
8
|
-
require
|
9
|
+
require "fiber/annotation"
|
9
10
|
|
10
|
-
require_relative
|
11
|
+
require_relative "list"
|
11
12
|
|
12
13
|
module Async
|
13
14
|
# A list of children tasks.
|
@@ -180,6 +181,7 @@ module Async
|
|
180
181
|
"\#<#{self.description}>"
|
181
182
|
end
|
182
183
|
|
184
|
+
# @returns [String] A description of the node.
|
183
185
|
alias inspect to_s
|
184
186
|
|
185
187
|
# Change the parent of this node.
|
data/lib/async/notification.rb
CHANGED
@@ -1,32 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
|
6
|
-
require_relative
|
6
|
+
require_relative "condition"
|
7
7
|
|
8
8
|
module Async
|
9
9
|
# A synchronization primitive, which allows fibers to wait until a notification is received. Does not block the task which signals the notification. Waiting tasks are resumed on next iteration of the reactor.
|
10
|
-
# @public Since
|
10
|
+
# @public Since *Async v1*.
|
11
11
|
class Notification < Condition
|
12
12
|
# Signal to a given task that it should resume operations.
|
13
|
-
|
14
|
-
|
13
|
+
#
|
14
|
+
# @returns [Boolean] if a task was signalled.
|
15
|
+
def signal(value = nil)
|
16
|
+
return false if empty?
|
15
17
|
|
16
18
|
Fiber.scheduler.push Signal.new(self.exchange, value)
|
17
19
|
|
18
|
-
return
|
20
|
+
return true
|
19
21
|
end
|
20
22
|
|
21
|
-
Signal = Struct.new(:
|
23
|
+
Signal = Struct.new(:ready, :value) do
|
22
24
|
def alive?
|
23
25
|
true
|
24
26
|
end
|
25
27
|
|
26
28
|
def transfer
|
27
|
-
|
28
|
-
|
29
|
+
ready.num_waiting.times do
|
30
|
+
ready.push(value)
|
29
31
|
end
|
32
|
+
|
33
|
+
ready.close
|
30
34
|
end
|
31
35
|
end
|
32
36
|
|