async 2.2.0 → 2.3.0

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: e757a8f806a7c54fc04c17385029c6f6804ca6db4d78ae9a090933b37ce7ba3c
4
- data.tar.gz: 7baa64c5e0ce26f31a9ca6e487a139781879e98c4feaf55353484eed906e9e02
3
+ metadata.gz: d60db4d677d9cb4ed92f654f19ca17a4dab336002ba44290a80545b1f25e9154
4
+ data.tar.gz: 9e84c874ef477cd0de6a86022c87a3fe92ae9276ddd3c4f4ccfd9d6600be0672
5
5
  SHA512:
6
- metadata.gz: 5d063676cf2926c9bb56daec3bef63b47f87f44ba49420d1206efc57a5432beeb21f6aead24c6508f105a55e485f38e6c9f161e3757633a2848840b1804c20f1
7
- data.tar.gz: f90fb391a58f0ad5de8dfcac70fe3c24b4a4dc3172157b4ff8467455fda81dd0321f82a34a4dda6bb630a18ae945b70cf33416a19497c8b377d5de428b8c9459
6
+ metadata.gz: 7dd4367c1abb5cc79bf635a34b5819522e7f8363c385f69c995fe23d6fd4d7f6fc6a07784c789afbad0ff1ff6726d6d11899dd9b0a87ae582e9f78711c0ab346
7
+ data.tar.gz: 0cbef57bb4896a06b11a95f318ad8a0d87435b70f591abc73bb8c7bf76773a341aba47399ef59a813f3d87f32f57112d070081db8598d4d58967f13ac26b4c61
checksums.yaml.gz.sig CHANGED
Binary file
data/lib/async/barrier.md CHANGED
@@ -31,6 +31,6 @@ end
31
31
  ### Output
32
32
 
33
33
  ~~~
34
- 0.0s info: Sorted [ec=0x104] [pid=50291]
35
- | [0, 0, 0, 0, 1, 2, 2, 3, 6, 6]
34
+ 0.0s info: Sorted
35
+ | [0, 0, 0, 0, 1, 2, 2, 3, 6, 6]
36
36
  ~~~
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,181 @@
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
+ "#<#{self.class.name} size=#{@size}>"
19
+ end
20
+
21
+ alias inspect to_s
22
+
23
+ # Points at the end of the list.
24
+ attr_accessor :head
25
+
26
+ # Points at the start of the list.
27
+ attr_accessor :tail
28
+
29
+ attr :size
30
+
31
+ # A callback that is invoked when an item is added to the list.
32
+ def added(node)
33
+ @size += 1
34
+ return node
35
+ end
36
+
37
+ # Append a node to the end of the list.
38
+ def append(node)
39
+ if node.head
40
+ raise ArgumentError, "Node is already in a list!"
41
+ end
42
+
43
+ node.tail = self
44
+ @head.tail = node
45
+ node.head = @head
46
+ @head = node
47
+
48
+ return added(node)
49
+ end
50
+
51
+ def prepend(node)
52
+ if node.head
53
+ raise ArgumentError, "Node is already in a list!"
54
+ end
55
+
56
+ node.head = self
57
+ @tail.head = node
58
+ node.tail = @tail
59
+ @tail = node
60
+
61
+ return added(node)
62
+ end
63
+
64
+ # Add the node, yield, and the remove the node.
65
+ # @yields {|node| ...} Yields the node.
66
+ # @returns [Object] Returns the result of the block.
67
+ def stack(node, &block)
68
+ append(node)
69
+ return yield(node)
70
+ ensure
71
+ remove!(node)
72
+ end
73
+
74
+ # A callback that is invoked when an item is removed from the list.
75
+ def removed(node)
76
+ @size -= 1
77
+ return node
78
+ end
79
+
80
+ # Remove the node if it is in a list.
81
+ #
82
+ # You should be careful to only remove nodes that are part of this list.
83
+ #
84
+ # @returns [Node] Returns the node if it was removed, otherwise nil.
85
+ def remove?(node)
86
+ if node.head
87
+ return remove!(node)
88
+ end
89
+
90
+ return nil
91
+ end
92
+
93
+ # Remove the node. If it was already removed, this will raise an error.
94
+ #
95
+ # You should be careful to only remove nodes that are part of this list.
96
+ #
97
+ # @raises [ArgumentError] If the node is not part of this list.
98
+ # @returns [Node] Returns the node if it was removed, otherwise nil.
99
+ def remove(node)
100
+ # 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.
101
+ unless node.head
102
+ raise ArgumentError, "Node is not in a list!"
103
+ end
104
+
105
+ remove!(node)
106
+ end
107
+
108
+ private def remove!(node)
109
+ node.head.tail = node.tail
110
+ node.tail.head = node.head
111
+
112
+ # This marks the node as being removed, and causes remove to fail if called a 2nd time.
113
+ node.head = nil
114
+ # node.tail = nil
115
+
116
+ return removed(node)
117
+ end
118
+
119
+ # @returns [Boolean] Returns true if the list is empty.
120
+ def empty?
121
+ @tail.equal?(self)
122
+ end
123
+
124
+ # 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.
125
+ #
126
+ # @yields {|node| ...} Yields each node in the list.
127
+ # @returns [List] Returns self.
128
+ def each
129
+ return to_enum unless block_given?
130
+
131
+ current = self
132
+
133
+ while true
134
+ node = current.tail
135
+ # binding.irb if node.nil? && !node.equal?(self)
136
+ break if node.equal?(self)
137
+
138
+ yield node
139
+
140
+ # 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:
141
+ if current.tail.equal?(node)
142
+ current = node
143
+ end
144
+ end
145
+
146
+ return self
147
+ end
148
+
149
+ # Determine whether the given node is included in the list.
150
+ #
151
+ # @parameter needle [Node] The node to search for.
152
+ # @returns [Boolean] Returns true if the node is in the list.
153
+ def include?(needle)
154
+ self.each do |item|
155
+ return true if needle.equal?(item)
156
+ end
157
+
158
+ return false
159
+ end
160
+
161
+ # @returns [Node] Returns the first node in the list, if it is not empty.
162
+ def first
163
+ unless @tail.equal?(self)
164
+ @tail
165
+ end
166
+ end
167
+
168
+ # @returns [Node] Returns the last node in the list, if it is not empty.
169
+ def last
170
+ unless @head.equal?(self)
171
+ @head
172
+ end
173
+ end
174
+ end
175
+
176
+ # A linked list Node.
177
+ class List::Node
178
+ attr_accessor :head
179
+ attr_accessor :tail
180
+ end
181
+ 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,15 @@ 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
 
273
190
  if @children
274
191
  @children.each do |child|
275
192
  if child.finished?
276
- delete_child(child)
193
+ remove_child(child)
277
194
  else
278
195
  # 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)
196
+ remove_child(child)
280
197
  parent.add_child(child)
281
198
  end
282
199
  end
@@ -288,18 +205,25 @@ module Async
288
205
  end
289
206
  end
290
207
 
291
- # Traverse the tree.
208
+ # Traverse the task tree.
209
+ #
210
+ # @returns [Enumerator] An enumerator which will traverse the tree if no block is given.
292
211
  # @yields {|node, level| ...} The node and the level relative to the given root.
293
- def traverse(level = 0, &block)
212
+ def traverse(&block)
213
+ return enum_for(:traverse) unless block_given?
214
+
215
+ self.traverse_recurse(&block)
216
+ end
217
+
218
+ protected def traverse_recurse(level = 0, &block)
294
219
  yield self, level
295
220
 
296
221
  @children&.each do |child|
297
- child.traverse(level + 1, &block)
222
+ child.traverse_recurse(level + 1, &block)
298
223
  end
299
224
  end
300
225
 
301
- # Immediately terminate all children tasks, including transient tasks.
302
- # Internally invokes `stop(false)` on all children.
226
+ # 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
227
  def terminate
304
228
  # Attempt to stop the current task immediately, and all children:
305
229
  stop(false)
@@ -310,8 +234,8 @@ module Async
310
234
  end
311
235
  end
312
236
 
313
- # Attempt to stop the current node immediately, including all non-transient children.
314
- # Invokes {#stop_children} to stop all children.
237
+ # Attempt to stop the current node immediately, including all non-transient children. Invokes {#stop_children} to stop all children.
238
+ #
315
239
  # @parameter later [Boolean] Whether to defer stopping until some point in the future.
316
240
  def stop(later = false)
317
241
  # 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.
@@ -160,7 +166,7 @@ module Async
160
166
  timer&.cancel
161
167
  end
162
168
 
163
- if IO::Event::Support.buffer?
169
+ if ::IO::Event::Support.buffer?
164
170
  def io_read(io, buffer, length, offset = 0)
165
171
  @selector.io_read(Fiber.current, io, buffer, length, offset)
166
172
  end
@@ -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.
@@ -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.0"
7
+ VERSION = "2.3.0"
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.0
4
+ version: 2.3.0
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-04 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
metadata.gz.sig CHANGED
Binary file