async 2.2.1 → 2.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74fa09c162a2702195d38adcb921dec7c9b53563a81b5d7cc117bf3a3f1402f3
4
- data.tar.gz: 37e7fae58da9e1a0ca928a1c9f34b376da95b8cdfcb89949fb9200dfb7253351
3
+ metadata.gz: 042a45792628d56b89e02200e38d6d00fb16c5311b95c3dae7d52607aef8108a
4
+ data.tar.gz: 6e7b3271d484ee3aabe395996ca987b1e0043d6d8710b3b706688993df43c916
5
5
  SHA512:
6
- metadata.gz: 9b17a23c9c0d7affeb9c75d8c1058fb0c25d2c8ebac6568ebf5834885f6379dc799c3bc9f92699441cc8cfd387cadbce993c52ca5b576fcedc0e1d68a4fd9c21
7
- data.tar.gz: 80a3a72ab5e9c49af4018a34564c7a7d72314dd9c640f306a4c7eab8b99fa9682a55135eb0b67614c370a4526908832b91cf94436089f4b48670a12d8b4a2896
6
+ metadata.gz: 49ea21f4f81b9e566119265b834ecbfb4de8db83788370e18d3d27b14e7a2ec972960f67301139dd61111f13843325264412bf096b5b33cafff910630b1663cb
7
+ data.tar.gz: 151eee11c70bf263c72cae52142168449ba38cd68a00ad302c2c9fcf53088de65784f35193971628e6c1ddb9d7efab9677359040ed19e13cde1f846670fd9b3e
checksums.yaml.gz.sig CHANGED
Binary file
data/lib/async/barrier.md CHANGED
@@ -7,6 +7,7 @@ require 'async'
7
7
  require 'async/barrier'
8
8
 
9
9
  Sync do
10
+ Console.logger.info("Barrier Example: sleep sort.")
10
11
  barrier = Async::Barrier.new
11
12
 
12
13
  # Generate an array of 10 numbers:
@@ -31,6 +32,7 @@ end
31
32
  ### Output
32
33
 
33
34
  ~~~
34
- 0.0s info: Sorted [ec=0x104] [pid=50291]
35
- | [0, 0, 0, 0, 1, 2, 2, 3, 6, 6]
35
+ 0.0s info: Barrier Example: sleep sort.
36
+ 9.0s info: Sorted
37
+ | [3, 3, 3, 4, 4, 5, 5, 5, 8, 9]
36
38
  ~~~
data/lib/async/barrier.rb CHANGED
@@ -3,35 +3,47 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2019-2022, by Samuel Williams.
5
5
 
6
+ require_relative 'list'
6
7
  require_relative 'task'
7
8
 
8
9
  module Async
9
- # A synchronization primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore}.
10
+ # 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
+ #
10
12
  # @public Since `stable-v1`.
11
13
  class Barrier
12
14
  # Initialize the barrier.
13
15
  # @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks.
14
16
  # @public Since `stable-v1`.
15
17
  def initialize(parent: nil)
16
- @tasks = []
18
+ @tasks = List.new
17
19
 
18
20
  @parent = parent
19
21
  end
20
22
 
21
- # All tasks which have been invoked into the barrier.
22
- attr :tasks
23
+ class TaskNode < List::Node
24
+ def initialize(task)
25
+ @task = task
26
+ end
27
+
28
+ attr :task
29
+ end
23
30
 
24
- # The number of tasks currently held by the barrier.
31
+ private_constant :TaskNode
32
+
33
+ # Number of tasks being held by the barrier.
25
34
  def size
26
35
  @tasks.size
27
36
  end
28
37
 
38
+ # All tasks which have been invoked into the barrier.
39
+ attr :tasks
40
+
29
41
  # Execute a child task and add it to the barrier.
30
42
  # @asynchronous Executes the given block concurrently.
31
43
  def async(*arguments, parent: (@parent or Task.current), **options, &block)
32
44
  task = parent.async(*arguments, **options, &block)
33
45
 
34
- @tasks << task
46
+ @tasks.append(TaskNode.new(task))
35
47
 
36
48
  return task
37
49
  end
@@ -42,21 +54,15 @@ module Async
42
54
  @tasks.empty?
43
55
  end
44
56
 
45
- # Wait for all tasks.
57
+ # 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.
46
58
  # @asynchronous Will wait for tasks to finish executing.
47
59
  def wait
48
- # TODO: This would be better with linked list.
49
- while @tasks.any?
50
- task = @tasks.first
51
-
60
+ @tasks.each do |waiting|
61
+ task = waiting.task
52
62
  begin
53
63
  task.wait
54
64
  ensure
55
- # We don't know for sure that the exception was due to the task completion.
56
- unless task.running?
57
- # Remove the task from the waiting list if it's finished:
58
- @tasks.shift if @tasks.first == task
59
- end
65
+ @tasks.remove?(waiting) unless task.alive?
60
66
  end
61
67
  end
62
68
  end
@@ -64,9 +70,9 @@ module Async
64
70
  # Stop all tasks held by the barrier.
65
71
  # @asynchronous May wait for tasks to finish executing.
66
72
  def stop
67
- # We have to be careful to avoid enumerating tasks while adding/removing to it:
68
- tasks = @tasks.dup
69
- tasks.each(&:stop)
73
+ @tasks.each do |waiting|
74
+ waiting.task.stop
75
+ end
70
76
  end
71
77
  end
72
78
  end
@@ -25,7 +25,7 @@ end
25
25
  ### Output
26
26
 
27
27
  ~~~
28
- 0.0s info: Waiting for condition... [ec=0x3c] [pid=47943]
29
- 1.0s info: Signalling condition... [ec=0x64] [pid=47943]
30
- 1.0s info: Condition was signalled: Hello World [ec=0x3c] [pid=47943]
28
+ 0.0s info: Waiting for condition...
29
+ 1.0s info: Signalling condition...
30
+ 1.0s info: Condition was signalled: Hello World
31
31
  ~~~
@@ -5,41 +5,38 @@
5
5
  # Copyright, 2017, by Kent Gruber.
6
6
 
7
7
  require 'fiber'
8
- require_relative 'node'
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
12
  # @public Since `stable-v1`.
13
13
  class Condition
14
14
  def initialize
15
- @waiting = []
15
+ @waiting = List.new
16
16
  end
17
17
 
18
- Queue = Struct.new(:fiber) do
19
- def transfer(*arguments)
20
- fiber&.transfer(*arguments)
18
+ class FiberNode < List::Node
19
+ def initialize(fiber)
20
+ @fiber = fiber
21
21
  end
22
22
 
23
- def alive?
24
- fiber&.alive?
23
+ def transfer(*arguments)
24
+ @fiber.transfer(*arguments)
25
25
  end
26
26
 
27
- def nullify
28
- self.fiber = nil
27
+ def alive?
28
+ @fiber.alive?
29
29
  end
30
30
  end
31
31
 
32
- private_constant :Queue
32
+ private_constant :FiberNode
33
33
 
34
34
  # Queue up the current fiber and wait on yielding the task.
35
35
  # @returns [Object]
36
36
  def wait
37
- queue = Queue.new(Fiber.current)
38
- @waiting << queue
39
-
40
- Fiber.scheduler.transfer
41
- ensure
42
- queue.nullify
37
+ @waiting.stack(FiberNode.new(Fiber.current)) do
38
+ Fiber.scheduler.transfer
39
+ end
43
40
  end
44
41
 
45
42
  # Is any fiber waiting on this notification?
@@ -51,8 +48,9 @@ module Async
51
48
  # Signal to a given task that it should resume operations.
52
49
  # @parameter value [Object | Nil] The value to return to the waiting fibers.
53
50
  def signal(value = nil)
54
- waiting = @waiting
55
- @waiting = []
51
+ return if @waiting.empty?
52
+
53
+ waiting = self.exchange
56
54
 
57
55
  waiting.each do |fiber|
58
56
  Fiber.scheduler.resume(fiber, value) if fiber.alive?
@@ -60,5 +58,13 @@ module Async
60
58
 
61
59
  return nil
62
60
  end
61
+
62
+ protected
63
+
64
+ def exchange
65
+ waiting = @waiting
66
+ @waiting = List.new
67
+ return waiting
68
+ end
63
69
  end
64
70
  end
data/lib/async/list.rb ADDED
@@ -0,0 +1,308 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2022, by Samuel Williams.
5
+
6
+ module Async
7
+ # A general doublely linked list. This is used internally by {Async::Barrier} and {Async::Condition} to manage child tasks.
8
+ class List
9
+ # Initialize a new, empty, list.
10
+ def initialize
11
+ @head = self
12
+ @tail = self
13
+ @size = 0
14
+ end
15
+
16
+ # Print a short summary of the list.
17
+ def to_s
18
+ sprintf("#<%s:0x%x size=%d>", self.class.name, object_id, @size)
19
+ end
20
+
21
+ alias inspect to_s
22
+
23
+ # Fast, safe, unbounded accumulation of children.
24
+ def to_a
25
+ items = []
26
+ current = self
27
+
28
+ while current.tail != self
29
+ unless current.tail.is_a?(Iterator)
30
+ items << current.tail
31
+ end
32
+
33
+ current = current.tail
34
+ end
35
+
36
+ return items
37
+ end
38
+
39
+ # Points at the end of the list.
40
+ attr_accessor :head
41
+
42
+ # Points at the start of the list.
43
+ attr_accessor :tail
44
+
45
+ attr :size
46
+
47
+ # A callback that is invoked when an item is added to the list.
48
+ def added(node)
49
+ @size += 1
50
+ return node
51
+ end
52
+
53
+ # Append a node to the end of the list.
54
+ def append(node)
55
+ if node.head
56
+ raise ArgumentError, "Node is already in a list!"
57
+ end
58
+
59
+ node.tail = self
60
+ @head.tail = node
61
+ node.head = @head
62
+ @head = node
63
+
64
+ return added(node)
65
+ end
66
+
67
+ def prepend(node)
68
+ if node.head
69
+ raise ArgumentError, "Node is already in a list!"
70
+ end
71
+
72
+ node.head = self
73
+ @tail.head = node
74
+ node.tail = @tail
75
+ @tail = node
76
+
77
+ return added(node)
78
+ end
79
+
80
+ # Add the node, yield, and the remove the node.
81
+ # @yields {|node| ...} Yields the node.
82
+ # @returns [Object] Returns the result of the block.
83
+ def stack(node, &block)
84
+ append(node)
85
+ return yield(node)
86
+ ensure
87
+ remove!(node)
88
+ end
89
+
90
+ # A callback that is invoked when an item is removed from the list.
91
+ def removed(node)
92
+ @size -= 1
93
+ return node
94
+ end
95
+
96
+ # Remove the node if it is in a list.
97
+ #
98
+ # You should be careful to only remove nodes that are part of this list.
99
+ #
100
+ # @returns [Node] Returns the node if it was removed, otherwise nil.
101
+ def remove?(node)
102
+ if node.head
103
+ return remove!(node)
104
+ end
105
+
106
+ return nil
107
+ end
108
+
109
+ # Remove the node. If it was already removed, this will raise an error.
110
+ #
111
+ # You should be careful to only remove nodes that are part of this list.
112
+ #
113
+ # @raises [ArgumentError] If the node is not part of this list.
114
+ # @returns [Node] Returns the node if it was removed, otherwise nil.
115
+ def remove(node)
116
+ # One downside of this interface is we don't actually check if the node is part of the list defined by `self`. This means that there is a potential for a node to be removed from a different list using this method, which in can throw off book-keeping when lists track size, etc.
117
+ unless node.head
118
+ raise ArgumentError, "Node is not in a list!"
119
+ end
120
+
121
+ remove!(node)
122
+ end
123
+
124
+ private def remove!(node)
125
+ node.head.tail = node.tail
126
+ node.tail.head = node.head
127
+
128
+ # This marks the node as being removed, and causes remove to fail if called a 2nd time.
129
+ node.head = nil
130
+ # node.tail = nil
131
+
132
+ return removed(node)
133
+ end
134
+
135
+ # @returns [Boolean] Returns true if the list is empty.
136
+ def empty?
137
+ @size == 0
138
+ end
139
+
140
+ # def validate!(node = nil)
141
+ # previous = self
142
+ # current = @tail
143
+ # found = node.equal?(self)
144
+
145
+ # while true
146
+ # break if current.equal?(self)
147
+
148
+ # if current.head != previous
149
+ # raise "Invalid previous linked list node!"
150
+ # end
151
+
152
+ # if current.is_a?(List) and !current.equal?(self)
153
+ # raise "Invalid list in list node!"
154
+ # end
155
+
156
+ # if node
157
+ # found ||= current.equal?(node)
158
+ # end
159
+
160
+ # previous = current
161
+ # current = current.tail
162
+ # end
163
+
164
+ # if node and !found
165
+ # raise "Node not found in list!"
166
+ # end
167
+ # end
168
+
169
+ # Iterate over each node in the linked list. It is generally safe to remove the current node, any previous node or any future node during iteration.
170
+ #
171
+ # @yields {|node| ...} Yields each node in the list.
172
+ # @returns [List] Returns self.
173
+ def each(&block)
174
+ return to_enum unless block_given?
175
+
176
+ Iterator.each(self, &block)
177
+
178
+ return self
179
+ end
180
+
181
+ # Determine whether the given node is included in the list.
182
+ #
183
+ # @parameter needle [Node] The node to search for.
184
+ # @returns [Boolean] Returns true if the node is in the list.
185
+ def include?(needle)
186
+ self.each do |item|
187
+ return true if needle.equal?(item)
188
+ end
189
+
190
+ return false
191
+ end
192
+
193
+ # @returns [Node] Returns the first node in the list, if it is not empty.
194
+ def first
195
+ # validate!
196
+
197
+ current = @tail
198
+
199
+ while !current.equal?(self)
200
+ if current.is_a?(Iterator)
201
+ current = current.tail
202
+ else
203
+ return current
204
+ end
205
+ end
206
+
207
+ return nil
208
+ end
209
+
210
+ # @returns [Node] Returns the last node in the list, if it is not empty.
211
+ def last
212
+ # validate!
213
+
214
+ current = @head
215
+
216
+ while !current.equal?(self)
217
+ if current.is_a?(Iterator)
218
+ current = current.head
219
+ else
220
+ return current
221
+ end
222
+ end
223
+
224
+ return nil
225
+ end
226
+
227
+ def shift
228
+ if node = first
229
+ remove!(node)
230
+ end
231
+ end
232
+
233
+ # A linked list Node.
234
+ class Node
235
+ attr_accessor :head
236
+ attr_accessor :tail
237
+
238
+ alias inspect to_s
239
+ end
240
+
241
+ class Iterator < Node
242
+ def initialize(list)
243
+ @list = list
244
+
245
+ # Insert the iterator as the first item in the list:
246
+ @tail = list.tail
247
+ @tail.head = self
248
+ list.tail = self
249
+ @head = list
250
+ end
251
+
252
+ def remove!
253
+ @head.tail = @tail
254
+ @tail.head = @head
255
+ @head = nil
256
+ @tail = nil
257
+ @list = nil
258
+ end
259
+
260
+ def move_next
261
+ # Move to the next item (which could be an iterator or the end):
262
+ @tail.head = @head
263
+ @head.tail = @tail
264
+ @head = @tail
265
+ @tail = @tail.tail
266
+ @head.tail = self
267
+ @tail.head = self
268
+ end
269
+
270
+ def move_current
271
+ while true
272
+ # Are we at the end of the list?
273
+ if @tail.equal?(@list)
274
+ return nil
275
+ end
276
+
277
+ if @tail.is_a?(Iterator)
278
+ move_next
279
+ else
280
+ return @tail
281
+ end
282
+ end
283
+ end
284
+
285
+ def each
286
+ while current = move_current
287
+ yield current
288
+
289
+ if current.equal?(@tail)
290
+ move_next
291
+ end
292
+ end
293
+ end
294
+
295
+ def self.each(list, &block)
296
+ return if list.empty?
297
+
298
+ iterator = Iterator.new(list)
299
+
300
+ iterator.each(&block)
301
+ ensure
302
+ iterator&.remove!
303
+ end
304
+ end
305
+
306
+ private_constant :Iterator
307
+ end
308
+ end
data/lib/async/node.rb CHANGED
@@ -5,138 +5,48 @@
5
5
  # Copyright, 2017, by Kent Gruber.
6
6
  # Copyright, 2022, by Shannon Skipper.
7
7
 
8
+ require_relative 'list'
9
+
8
10
  module Async
9
- # A double linked list used for managing tasks.
10
- class List
11
- def initialize
12
- # The list behaves like a list node, so @tail points to the next item (the first one) and head points to the previous item (the last one). This may be slightly confusing but it makes the interface more natural.
13
- @head = nil
14
- @tail = nil
15
- @size = 0
16
- end
17
-
18
- attr :size
19
-
20
- attr_accessor :head
21
- attr_accessor :tail
22
-
23
- # Inserts an item at the end of the list.
24
- def insert(item)
25
- unless @tail
26
- @tail = item
27
- @head = item
28
-
29
- # Consistency:
30
- item.head = nil
31
- item.tail = nil
32
- else
33
- @head.tail = item
34
- item.head = @head
35
-
36
- # Consistency:
37
- item.tail = nil
38
-
39
- @head = item
40
- end
41
-
42
- @size += 1
43
-
44
- return self
45
- end
46
-
47
- def delete(item)
48
- if @tail.equal?(item)
49
- @tail = @tail.tail
50
- else
51
- item.head.tail = item.tail
52
- end
53
-
54
- if @head.equal?(item)
55
- @head = @head.head
56
- else
57
- item.tail.head = item.head
58
- end
59
-
60
- item.head = nil
61
- item.tail = nil
62
-
63
- @size -= 1
64
-
65
- return self
66
- end
67
-
68
- def each(&block)
69
- return to_enum unless block_given?
70
-
71
- current = self
72
- while node = current.tail
73
- yield node
74
-
75
- # If the node has deleted itself or any subsequent node, it will no longer be the next node, so don't use it for continued traversal:
76
- if current.tail.equal?(node)
77
- current = node
78
- end
79
- end
80
- end
81
-
82
- def include?(needle)
83
- self.each do |item|
84
- return true if needle.equal?(item)
85
- end
86
-
87
- return false
88
- end
89
-
90
- def first
91
- @tail
92
- end
93
-
94
- def last
95
- @head
96
- end
97
-
98
- def empty?
99
- @tail.nil?
100
- end
101
-
102
- def nil?
103
- @tail.nil?
104
- end
105
- end
106
-
107
- private_constant :List
108
-
109
11
  # A list of children tasks.
110
12
  class Children < List
111
13
  def initialize
112
14
  super
113
-
114
15
  @transient_count = 0
115
16
  end
116
17
 
117
- # Does this node have (direct) transient children?
18
+ # Some children may be marked as transient. Transient children do not prevent the parent from finishing.
19
+ # @returns [Boolean] Whether the node has transient children.
118
20
  def transients?
119
21
  @transient_count > 0
120
22
  end
121
23
 
122
- def insert(item)
123
- if item.transient?
24
+ # Whether all children are considered finished. Ignores transient children.
25
+ def finished?
26
+ @size == @transient_count
27
+ end
28
+
29
+ # Whether the children is empty, preserved for compatibility.
30
+ def nil?
31
+ empty?
32
+ end
33
+
34
+ private
35
+
36
+ def added(node)
37
+ if node.transient?
124
38
  @transient_count += 1
125
39
  end
126
40
 
127
- super
41
+ return super
128
42
  end
129
43
 
130
- def delete(item)
131
- if item.transient?
44
+ def removed(node)
45
+ if node.transient?
132
46
  @transient_count -= 1
133
47
  end
134
48
 
135
- super
136
- end
137
-
138
- def finished?
139
- @size == @transient_count
49
+ return super
140
50
  end
141
51
  end
142
52
 
@@ -181,12 +91,18 @@ module Async
181
91
  # A useful identifier for the current node.
182
92
  attr :annotation
183
93
 
184
- # Whether there are children?
94
+ # Whether this node has any children.
95
+ # @returns [Boolean]
185
96
  def children?
186
- @children != nil && !@children.empty?
97
+ @children && !@children.empty?
187
98
  end
188
99
 
189
- # Is this node transient?
100
+ # Represents whether a node is transient. Transient nodes are not considered
101
+ # when determining if a node is finished. This is useful for tasks which are
102
+ # internal to an object rather than explicit user concurrency. For example,
103
+ # a child task which is pruning a connection pool is transient, because it
104
+ # is not directly related to the parent task, and should not prevent the
105
+ # parent task from finishing.
190
106
  def transient?
191
107
  @transient
192
108
  end
@@ -225,13 +141,14 @@ module Async
225
141
  alias inspect to_s
226
142
 
227
143
  # Change the parent of this node.
228
- # @parameter parent [Node | Nil] the parent to attach to, or nil to detach.
144
+ #
145
+ # @parameter parent [Node | Nil] The parent to attach to, or nil to detach.
229
146
  # @returns [Node] Itself.
230
147
  def parent=(parent)
231
148
  return if @parent.equal?(parent)
232
149
 
233
150
  if @parent
234
- @parent.delete_child(self)
151
+ @parent.remove_child(self)
235
152
  @parent = nil
236
153
  end
237
154
 
@@ -242,23 +159,23 @@ module Async
242
159
  return self
243
160
  end
244
161
 
245
- protected def set_parent parent
162
+ protected def set_parent(parent)
246
163
  @parent = parent
247
164
  end
248
165
 
249
- protected def add_child child
166
+ protected def add_child(child)
250
167
  @children ||= Children.new
251
- @children.insert(child)
168
+ @children.append(child)
252
169
  child.set_parent(self)
253
170
  end
254
171
 
255
- protected def delete_child(child)
256
- @children.delete(child)
172
+ protected def remove_child(child)
173
+ @children.remove(child)
257
174
  child.set_parent(nil)
258
175
  end
259
176
 
260
- # Whether the node can be consumed safely. By default, checks if the
261
- # children set is empty.
177
+ # Whether the node can be consumed (deleted) safely. By default, checks if the children set is empty.
178
+ #
262
179
  # @returns [Boolean]
263
180
  def finished?
264
181
  @children.nil? || @children.finished?
@@ -268,15 +185,14 @@ module Async
268
185
  # the parent.
269
186
  def consume
270
187
  if parent = @parent and finished?
271
- parent.delete_child(self)
188
+ parent.remove_child(self)
272
189
 
190
+ # If we have children, then we need to move them to our the parent if they are not finished:
273
191
  if @children
274
- @children.each do |child|
192
+ while child = @children.shift
275
193
  if child.finished?
276
- delete_child(child)
194
+ child.set_parent(nil)
277
195
  else
278
- # In theory we don't need to do this... because we are throwing away the list. However, if you don't correctly update the list when moving the child to the parent, it foobars the enumeration, and subsequent nodes will be skipped, or in the worst case you might start enumerating the parents nodes.
279
- delete_child(child)
280
196
  parent.add_child(child)
281
197
  end
282
198
  end
@@ -288,18 +204,25 @@ module Async
288
204
  end
289
205
  end
290
206
 
291
- # Traverse the tree.
207
+ # Traverse the task tree.
208
+ #
209
+ # @returns [Enumerator] An enumerator which will traverse the tree if no block is given.
292
210
  # @yields {|node, level| ...} The node and the level relative to the given root.
293
- def traverse(level = 0, &block)
211
+ def traverse(&block)
212
+ return enum_for(:traverse) unless block_given?
213
+
214
+ self.traverse_recurse(&block)
215
+ end
216
+
217
+ protected def traverse_recurse(level = 0, &block)
294
218
  yield self, level
295
219
 
296
220
  @children&.each do |child|
297
- child.traverse(level + 1, &block)
221
+ child.traverse_recurse(level + 1, &block)
298
222
  end
299
223
  end
300
224
 
301
- # Immediately terminate all children tasks, including transient tasks.
302
- # Internally invokes `stop(false)` on all children.
225
+ # Immediately terminate all children tasks, including transient tasks. Internally invokes `stop(false)` on all children. This should be considered a last ditch effort and is used when closing the scheduler.
303
226
  def terminate
304
227
  # Attempt to stop the current task immediately, and all children:
305
228
  stop(false)
@@ -310,8 +233,8 @@ module Async
310
233
  end
311
234
  end
312
235
 
313
- # Attempt to stop the current node immediately, including all non-transient children.
314
- # Invokes {#stop_children} to stop all children.
236
+ # Attempt to stop the current node immediately, including all non-transient children. Invokes {#stop_children} to stop all children.
237
+ #
315
238
  # @parameter later [Boolean] Whether to defer stopping until some point in the future.
316
239
  def stop(later = false)
317
240
  # The implementation of this method may defer calling `stop_children`.
@@ -13,9 +13,7 @@ module Async
13
13
  def signal(value = nil, task: Task.current)
14
14
  return if @waiting.empty?
15
15
 
16
- Fiber.scheduler.push Signal.new(@waiting, value)
17
-
18
- @waiting = []
16
+ Fiber.scheduler.push Signal.new(self.exchange, value)
19
17
 
20
18
  return nil
21
19
  end
data/lib/async/reactor.rb CHANGED
@@ -21,6 +21,10 @@ module Async
21
21
  Fiber.set_scheduler(self)
22
22
  end
23
23
 
24
+ def scheduler_close
25
+ self.close
26
+ end
27
+
24
28
  public :sleep
25
29
  end
26
30
  end
@@ -34,6 +34,12 @@ module Async
34
34
  @timers = ::Timers::Group.new
35
35
  end
36
36
 
37
+ def scheduler_close
38
+ self.run
39
+ ensure
40
+ self.close
41
+ end
42
+
37
43
  # @public Since `stable-v1`.
38
44
  def close
39
45
  # This is a critical step. Because tasks could be stored as instance variables, and since the reactor is (probably) going out of scope, we need to ensure they are stopped. Otherwise, the tasks will belong to a reactor that will never run again and are not stopped.
@@ -3,6 +3,8 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2018-2022, by Samuel Williams.
5
5
 
6
+ require_relative 'list'
7
+
6
8
  module Async
7
9
  # A synchronization primitive, which limits access to a given resource.
8
10
  # @public Since `stable-v1`.
@@ -12,7 +14,7 @@ module Async
12
14
  def initialize(limit = 1, parent: nil)
13
15
  @count = 0
14
16
  @limit = limit
15
- @waiting = []
17
+ @waiting = List.new
16
18
 
17
19
  @parent = parent
18
20
  end
@@ -73,26 +75,34 @@ module Async
73
75
  def release
74
76
  @count -= 1
75
77
 
76
- while (@limit - @count) > 0 and fiber = @waiting.shift
77
- if fiber.alive?
78
- Fiber.scheduler.resume(fiber)
79
- end
78
+ while (@limit - @count) > 0 and node = @waiting.first
79
+ node.resume
80
80
  end
81
81
  end
82
82
 
83
83
  private
84
84
 
85
+ class FiberNode < List::Node
86
+ def initialize(fiber)
87
+ @fiber = fiber
88
+ end
89
+
90
+ def resume
91
+ if @fiber.alive?
92
+ Fiber.scheduler.resume(@fiber)
93
+ end
94
+ end
95
+ end
96
+
97
+ private_constant :FiberNode
98
+
85
99
  # Wait until the semaphore becomes available.
86
100
  def wait
87
- fiber = Fiber.current
101
+ return unless blocking?
88
102
 
89
- if blocking?
90
- @waiting << fiber
103
+ @waiting.stack(FiberNode.new(Fiber.current)) do
91
104
  Fiber.scheduler.transfer while blocking?
92
105
  end
93
- rescue Exception
94
- @waiting.delete(fiber)
95
- raise
96
106
  end
97
107
  end
98
108
  end
data/lib/async/task.rb CHANGED
@@ -111,7 +111,10 @@ module Async
111
111
  end
112
112
  end
113
113
 
114
+ # Run an asynchronous task as a child of the current task.
114
115
  def async(*arguments, **options, &block)
116
+ raise "Cannot create child task within a task that has finished execution!" if self.finished?
117
+
115
118
  task = Task.new(self, **options, &block)
116
119
 
117
120
  task.run(*arguments)
@@ -119,26 +122,28 @@ module Async
119
122
  return task
120
123
  end
121
124
 
122
- # Retrieve the current result of the task. Will cause the caller to wait until result is available.
123
- # @raises[RuntimeError] If the task's fiber is the current fiber.
125
+ # Retrieve the current result of the task. Will cause the caller to wait until result is available. If the result was an exception, raise that exception.
126
+ #
127
+ # Conceptually speaking, waiting on a task should return a result, and if it throws an exception, this is certainly an exceptional case that should represent a failure in your program, not an expected outcome. In other words, you should not design your programs to expect exceptions from `#wait` as a normal flow control, and prefer to catch known exceptions within the task itself and return a result that captures the intention of the failure, e.g. a `TimeoutError` might simply return `nil` or `false` to indicate that the operation did not generate a valid result (as a timeout was an expected outcome of the internal operation in this case).
128
+ #
129
+ # @raises [RuntimeError] If the task's fiber is the current fiber.
124
130
  # @returns [Object] The final expression/result of the task's block.
125
131
  def wait
126
- raise "Cannot wait on own fiber" if Fiber.current.equal?(@fiber)
132
+ raise "Cannot wait on own fiber!" if Fiber.current.equal?(@fiber)
127
133
 
128
134
  if running?
129
135
  @finished ||= Condition.new
130
136
  @finished.wait
131
137
  end
132
138
 
133
- case @result
134
- when Exception
139
+ if @result.is_a?(Exception)
135
140
  raise @result
136
141
  else
137
142
  return @result
138
143
  end
139
144
  end
140
145
 
141
- # Access the result of the task without waiting. May be nil if the task is not completed.
146
+ # Access the result of the task without waiting. May be nil if the task is not completed. Does not raise exceptions.
142
147
  attr :result
143
148
 
144
149
  # Stop the task and all of its children.
@@ -258,7 +263,7 @@ module Async
258
263
  self.root.resume(@fiber)
259
264
  end
260
265
 
261
- # Finish the current task, and all bound bound IO objects.
266
+ # Finish the current task, moving any children to the parent.
262
267
  def finish!
263
268
  # Allow the fiber to be recycled.
264
269
  @fiber = nil
@@ -268,7 +273,7 @@ module Async
268
273
 
269
274
  # If this task was being used as a future, signal completion here:
270
275
  if @finished
271
- @finished.signal(@result)
276
+ @finished.signal(self)
272
277
  end
273
278
  end
274
279
 
data/lib/async/version.rb CHANGED
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2017-2022, by Samuel Williams.
5
5
 
6
6
  module Async
7
- VERSION = "2.2.1"
7
+ VERSION = "2.3.1"
8
8
  end
@@ -0,0 +1,50 @@
1
+ A synchronization primitive, which allows you to wait for tasks to complete in order of completion. This is useful for implementing a task pool, where you want to wait for the first task to complete, and then cancel the rest.
2
+
3
+ If you try to wait for more things than you have added, you will deadlock.
4
+
5
+ ## Example
6
+
7
+ ~~~ ruby
8
+ require 'async'
9
+ require 'async/semaphore'
10
+ require 'async/barrier'
11
+ require 'async/waiter'
12
+
13
+ Sync do
14
+ barrier = Async::Barrier.new
15
+ waiter = Async::Waiter.new(parent: barrier)
16
+ semaphore = Async::Semaphore.new(2, parent: waiter)
17
+
18
+ # Sleep sort the numbers:
19
+ generator = Async do
20
+ while true
21
+ semaphore.async do |task|
22
+ number = rand(1..10)
23
+ sleep(number)
24
+ end
25
+ end
26
+ end
27
+
28
+ numbers = []
29
+
30
+ 4.times do
31
+ # Wait for all the numbers to be sorted:
32
+ numbers << waiter.wait
33
+ end
34
+
35
+ # Don't generate any more numbers:
36
+ generator.stop
37
+
38
+ # Stop all tasks which we don't care about:
39
+ barrier.stop
40
+
41
+ Console.logger.info("Smallest", numbers)
42
+ end
43
+ ~~~
44
+
45
+ ### Output
46
+
47
+ ~~~
48
+ 0.0s info: Smallest
49
+ | [3, 3, 1, 2]
50
+ ~~~
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2022, by Samuel Williams.
5
+
6
+ module Async
7
+ # A composable synchronization primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore} and/or {Barrier}.
8
+ class Waiter
9
+ def initialize(parent: nil, finished: Async::Condition.new)
10
+ @finished = finished
11
+ @done = []
12
+
13
+ @parent = parent
14
+ end
15
+
16
+ # Execute a child task and add it to the waiter.
17
+ # @asynchronous Executes the given block concurrently.
18
+ def async(parent: (@parent or Task.current), &block)
19
+ parent.async do |task|
20
+ yield(task)
21
+ ensure
22
+ @done << task
23
+ @finished.signal
24
+ end
25
+ end
26
+
27
+ # Wait for the first `count` tasks to complete.
28
+ # @parameter count [Integer | Nil] The number of tasks to wait for.
29
+ # @returns [Array(Async::Task)] If an integer is given, the tasks which have completed.
30
+ # @returns [Async::Task] Otherwise, the first task to complete.
31
+ def first(count = nil)
32
+ minimum = count || 1
33
+
34
+ while @done.size < minimum
35
+ @finished.wait
36
+ end
37
+
38
+ return @done.shift(*count)
39
+ end
40
+
41
+ # Wait for the first `count` tasks to complete.
42
+ # @parameter count [Integer | Nil] The number of tasks to wait for.
43
+ def wait(count = nil)
44
+ if count
45
+ first(count).map(&:wait)
46
+ else
47
+ first.wait
48
+ end
49
+ end
50
+ end
51
+ end
data/lib/kernel/async.rb CHANGED
@@ -25,6 +25,7 @@ module Kernel
25
25
  if current = ::Async::Task.current?
26
26
  return current.async(...)
27
27
  else
28
+ # This calls Fiber.set_scheduler(self):
28
29
  reactor = ::Async::Reactor.new
29
30
 
30
31
  begin
data/lib/kernel/sync.rb CHANGED
@@ -19,6 +19,7 @@ module Kernel
19
19
  if task = ::Async::Task.current?
20
20
  yield task
21
21
  else
22
+ # This calls Fiber.set_scheduler(self):
22
23
  reactor = Async::Reactor.new
23
24
 
24
25
  begin
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.1
4
+ version: 2.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -54,7 +54,7 @@ cert_chain:
54
54
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
55
55
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
56
56
  -----END CERTIFICATE-----
57
- date: 2022-10-13 00:00:00.000000000 Z
57
+ date: 2022-12-28 00:00:00.000000000 Z
58
58
  dependencies:
59
59
  - !ruby/object:Gem::Dependency
60
60
  name: console
@@ -76,14 +76,14 @@ dependencies:
76
76
  requirements:
77
77
  - - "~>"
78
78
  - !ruby/object:Gem::Version
79
- version: 1.1.0
79
+ version: '1.1'
80
80
  type: :runtime
81
81
  prerelease: false
82
82
  version_requirements: !ruby/object:Gem::Requirement
83
83
  requirements:
84
84
  - - "~>"
85
85
  - !ruby/object:Gem::Version
86
- version: 1.1.0
86
+ version: '1.1'
87
87
  - !ruby/object:Gem::Dependency
88
88
  name: timers
89
89
  requirement: !ruby/object:Gem::Requirement
@@ -174,28 +174,42 @@ dependencies:
174
174
  requirements:
175
175
  - - "~>"
176
176
  - !ruby/object:Gem::Version
177
- version: '0.10'
177
+ version: 0.18.3
178
178
  type: :development
179
179
  prerelease: false
180
180
  version_requirements: !ruby/object:Gem::Requirement
181
181
  requirements:
182
182
  - - "~>"
183
183
  - !ruby/object:Gem::Version
184
- version: '0.10'
184
+ version: 0.18.3
185
185
  - !ruby/object:Gem::Dependency
186
- name: rspec
186
+ name: sus
187
187
  requirement: !ruby/object:Gem::Requirement
188
188
  requirements:
189
189
  - - "~>"
190
190
  - !ruby/object:Gem::Version
191
- version: '3.6'
191
+ version: '0.15'
192
192
  type: :development
193
193
  prerelease: false
194
194
  version_requirements: !ruby/object:Gem::Requirement
195
195
  requirements:
196
196
  - - "~>"
197
197
  - !ruby/object:Gem::Version
198
- version: '3.6'
198
+ version: '0.15'
199
+ - !ruby/object:Gem::Dependency
200
+ name: sus-fixtures-async
201
+ requirement: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ type: :development
207
+ prerelease: false
208
+ version_requirements: !ruby/object:Gem::Requirement
209
+ requirements:
210
+ - - ">="
211
+ - !ruby/object:Gem::Version
212
+ version: '0'
199
213
  description:
200
214
  email:
201
215
  executables: []
@@ -208,6 +222,7 @@ files:
208
222
  - lib/async/clock.rb
209
223
  - lib/async/condition.md
210
224
  - lib/async/condition.rb
225
+ - lib/async/list.rb
211
226
  - lib/async/node.rb
212
227
  - lib/async/notification.rb
213
228
  - lib/async/queue.rb
@@ -218,6 +233,8 @@ files:
218
233
  - lib/async/task.rb
219
234
  - lib/async/variable.rb
220
235
  - lib/async/version.rb
236
+ - lib/async/waiter.md
237
+ - lib/async/waiter.rb
221
238
  - lib/async/wrapper.rb
222
239
  - lib/kernel/async.rb
223
240
  - lib/kernel/sync.rb
@@ -242,7 +259,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
242
259
  - !ruby/object:Gem::Version
243
260
  version: '0'
244
261
  requirements: []
245
- rubygems_version: 3.3.7
262
+ rubygems_version: 3.4.1
246
263
  signing_key:
247
264
  specification_version: 4
248
265
  summary: A concurrency framework for Ruby.
metadata.gz.sig CHANGED
Binary file