async 2.12.1 → 2.14.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: 1eef4bf800b6d52c0f273289186f9886975237ecdf9885f599617691283ca2e5
4
- data.tar.gz: 14a7e20dc72c178b89a93233a73fd49f182f7aec7926804b9b8bf37e8ceb9f36
3
+ metadata.gz: ffbf235f3425b766c8eedfcdb2b03f86569e43ab1cdda3bd44f74c1b25f516b7
4
+ data.tar.gz: 1d71883b2151b5eb0f23daf27221ac091d352809a0ef1f687f282696cd22ae79
5
5
  SHA512:
6
- metadata.gz: 30176358a5c9efc3df3e525e0ba941f5771bb7619bef28e1b6229a547ad3a24cb5475483a257704c3eea2d9214dcca2e17932994e0859a1c5d4dfc6401b24d48
7
- data.tar.gz: 8b7353cf2ef0884c78a919775b84280d3234c7868c6c3b95aa00ae560a5825a07bd9533eda1afcc9776d1c94b7cc116e549e24144e4bab7c9bb3f086782a7456
6
+ metadata.gz: a00499f9ea43ffee3e6a5cf65dee14f67c5dcd167cb68e1b5da7c11bcef51c06da9c8c2d7a0606f72e5802520f7b9837f128d8425ade3de75111ca16a4cc9c10
7
+ data.tar.gz: f65b60522c85e07a17fdc345944c06e7447095682afea0bd2b7dcbf462deeeb7dcb6053e82eefc96cc259b10d6b0849803b8f22356feb10a21666a5ab134f6d6
checksums.yaml.gz.sig CHANGED
Binary file
@@ -11,6 +11,7 @@ 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
+ # Create a new condition.
14
15
  def initialize
15
16
  @waiting = List.new
16
17
  end
@@ -39,12 +40,16 @@ module Async
39
40
  end
40
41
  end
41
42
 
42
- # Is any fiber waiting on this notification?
43
- # @returns [Boolean]
43
+ # @deprecated Replaced by {#waiting?}
44
44
  def empty?
45
45
  @waiting.empty?
46
46
  end
47
47
 
48
+ # @returns [Boolean] Is any fiber waiting on this notification?
49
+ def waiting?
50
+ @waiting.size > 0
51
+ end
52
+
48
53
  # Signal to a given task that it should resume operations.
49
54
  # @parameter value [Object | Nil] The value to return to the waiting fibers.
50
55
  def signal(value = nil)
data/lib/async/idler.rb CHANGED
@@ -4,13 +4,28 @@
4
4
  # Copyright, 2024, by Samuel Williams.
5
5
 
6
6
  module Async
7
+ # A load balancing mechanism that can be used process work when the system is idle.
7
8
  class Idler
9
+ # Create a new idler.
10
+ # @public Since `stable-v2`.
11
+ #
12
+ # @parameter maximum_load [Numeric] The maximum load before we start shedding work.
13
+ # @parameter backoff [Numeric] The initial backoff time, used for delaying work.
14
+ # @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
8
15
  def initialize(maximum_load = 0.8, backoff: 0.01, parent: nil)
9
16
  @maximum_load = maximum_load
10
17
  @backoff = backoff
11
18
  @parent = parent
12
19
  end
13
20
 
21
+ # Wait until the system is idle, then execute the given block in a new task.
22
+ #
23
+ # @asynchronous Executes the given block concurrently.
24
+ #
25
+ # @parameter arguments [Array] The arguments to pass to the block.
26
+ # @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
27
+ # @parameter options [Hash] The options to pass to the task.
28
+ # @yields {|task| ...} When the system is idle, the block will be executed in a new task.
14
29
  def async(*arguments, parent: (@parent or Task.current), **options, &block)
15
30
  wait
16
31
 
@@ -18,6 +33,9 @@ module Async
18
33
  parent.async(*arguments, **options, &block)
19
34
  end
20
35
 
36
+ # Wait until the system is idle, according to the maximum load specified.
37
+ #
38
+ # If the scheduler is overloaded, this method will sleep for an exponentially increasing amount of time.
21
39
  def wait
22
40
  scheduler = Fiber.scheduler
23
41
  backoff = nil
data/lib/async/list.rb CHANGED
@@ -13,7 +13,7 @@ module Async
13
13
  @size = 0
14
14
  end
15
15
 
16
- # Print a short summary of the list.
16
+ # @returns [String] A short summary of the list.
17
17
  def to_s
18
18
  sprintf("#<%s:0x%x size=%d>", self.class.name, object_id, @size)
19
19
  end
@@ -36,12 +36,13 @@ module Async
36
36
  return items
37
37
  end
38
38
 
39
- # Points at the end of the list.
39
+ # @attribute [Node | Nil] Points at the end of the list.
40
40
  attr_accessor :head
41
41
 
42
- # Points at the start of the list.
42
+ # @attribute [Node | Nil] Points at the start of the list.
43
43
  attr_accessor :tail
44
44
 
45
+ # @attribute [Integer] The number of nodes in the list.
45
46
  attr :size
46
47
 
47
48
  # A callback that is invoked when an item is added to the list.
@@ -64,6 +65,7 @@ module Async
64
65
  return added(node)
65
66
  end
66
67
 
68
+ # Prepend a node to the start of the list.
67
69
  def prepend(node)
68
70
  if node.head
69
71
  raise ArgumentError, "Node is already in a list!"
@@ -224,6 +226,7 @@ module Async
224
226
  return nil
225
227
  end
226
228
 
229
+ # Shift the first node off the list, if it is not empty.
227
230
  def shift
228
231
  if node = first
229
232
  remove!(node)
data/lib/async/node.rb CHANGED
@@ -12,6 +12,7 @@ require_relative 'list'
12
12
  module Async
13
13
  # A list of children tasks.
14
14
  class Children < List
15
+ # Create an empty list of children tasks.
15
16
  def initialize
16
17
  super
17
18
  @transient_count = 0
@@ -73,7 +74,7 @@ module Async
73
74
  end
74
75
  end
75
76
 
76
- # @returns [Node] the root node in the hierarchy.
77
+ # @returns [Node] The root node in the hierarchy.
77
78
  def root
78
79
  @parent&.root || self
79
80
  end
@@ -87,10 +88,10 @@ module Async
87
88
  # @attribute [Node] The parent node.
88
89
  attr :parent
89
90
 
90
- # @attribute children [Children | Nil] Optional list of children.
91
+ # @attribute [Children | Nil] Optional list of children.
91
92
  attr :children
92
93
 
93
- # A useful identifier for the current node.
94
+ # @attribute [String | Nil] A useful identifier for the current node.
94
95
  attr :annotation
95
96
 
96
97
  # Whether this node has any children.
@@ -109,6 +110,9 @@ module Async
109
110
  @transient
110
111
  end
111
112
 
113
+ # Annotate the node with a description.
114
+ #
115
+ # @parameter annotation [String] The description to annotate the node with.
112
116
  def annotate(annotation)
113
117
  if block_given?
114
118
  begin
@@ -123,6 +127,9 @@ module Async
123
127
  end
124
128
  end
125
129
 
130
+ # A description of the node, including the annotation and object name.
131
+ #
132
+ # @returns [String] The description of the node.
126
133
  def description
127
134
  @object_name ||= "#{self.class}:#{format '%#018x', object_id}#{@transient ? ' transient' : nil}"
128
135
 
@@ -135,10 +142,14 @@ module Async
135
142
  end
136
143
  end
137
144
 
145
+ # Provides a backtrace for nodes that have an active execution context.
146
+ #
147
+ # @returns [Array(Thread::Backtrace::Locations) | Nil] The backtrace of the node, if available.
138
148
  def backtrace(*arguments)
139
149
  nil
140
150
  end
141
151
 
152
+ # @returns [String] A description of the node.
142
153
  def to_s
143
154
  "\#<#{self.description}>"
144
155
  end
@@ -255,10 +266,15 @@ module Async
255
266
  end
256
267
  end
257
268
 
269
+ # Whether the node has been stopped.
258
270
  def stopped?
259
271
  @children.nil?
260
272
  end
261
273
 
274
+ # Print the hierarchy of the task tree from the given node.
275
+ #
276
+ # @parameter out [IO] The output stream to write to.
277
+ # @parameter backtrace [Boolean] Whether to print the backtrace of each node.
262
278
  def print_hierarchy(out = $stdout, backtrace: true)
263
279
  self.traverse do |node, level|
264
280
  indent = "\t" * level
@@ -29,5 +29,7 @@ module Async
29
29
  end
30
30
  end
31
31
  end
32
+
33
+ private_constant :Signal
32
34
  end
33
35
  end
data/lib/async/queue.rb CHANGED
@@ -9,68 +9,104 @@ require_relative 'notification'
9
9
 
10
10
  module Async
11
11
  # A queue which allows items to be processed in order.
12
+ #
13
+ # It has a compatible interface with {Notification} and {Condition}, except that it's multi-value.
14
+ #
12
15
  # @public Since `stable-v1`.
13
- class Queue < Notification
14
- def initialize(parent: nil)
15
- super()
16
-
16
+ class Queue
17
+ # Create a new queue.
18
+ #
19
+ # @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
20
+ # @parameter available [Notification] The notification to use for signaling when items are available.
21
+ def initialize(parent: nil, available: Notification.new)
17
22
  @items = []
18
23
  @parent = parent
24
+ @available = available
19
25
  end
20
26
 
27
+ # @attribute [Array] The items in the queue.
21
28
  attr :items
22
29
 
30
+ # @returns [Integer] The number of items in the queue.
23
31
  def size
24
32
  @items.size
25
33
  end
26
-
34
+
35
+ # @returns [Boolean] Whether the queue is empty.
27
36
  def empty?
28
37
  @items.empty?
29
38
  end
30
39
 
40
+ # Add an item to the queue.
31
41
  def <<(item)
32
42
  @items << item
33
43
 
34
- self.signal unless self.empty?
44
+ @available.signal unless self.empty?
35
45
  end
36
46
 
47
+ # Add multiple items to the queue.
37
48
  def enqueue(*items)
38
49
  @items.concat(items)
39
50
 
40
- self.signal unless self.empty?
51
+ @available.signal unless self.empty?
41
52
  end
42
53
 
54
+ # Remove and return the next item from the queue.
43
55
  def dequeue
44
56
  while @items.empty?
45
- self.wait
57
+ @available.wait
46
58
  end
47
59
 
48
60
  @items.shift
49
61
  end
50
62
 
51
- def async(parent: (@parent or Task.current), &block)
63
+ # Process each item in the queue.
64
+ #
65
+ # @asynchronous Executes the given block concurrently for each item.
66
+ #
67
+ # @parameter arguments [Array] The arguments to pass to the block.
68
+ # @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
69
+ # @parameter options [Hash] The options to pass to the task.
70
+ # @yields {|task| ...} When the system is idle, the block will be executed in a new task.
71
+ def async(parent: (@parent or Task.current), **options, &block)
52
72
  while item = self.dequeue
53
- parent.async(item, &block)
73
+ parent.async(item, **options, &block)
54
74
  end
55
75
  end
56
76
 
77
+ # Enumerate each item in the queue.
57
78
  def each
58
79
  while item = self.dequeue
59
80
  yield item
60
81
  end
61
82
  end
83
+
84
+ # Signal the queue with a value, the same as {#enqueue}.
85
+ def signal(value)
86
+ self.enqueue(value)
87
+ end
88
+
89
+ # Wait for an item to be available, the same as {#dequeue}.
90
+ def wait
91
+ self.dequeue
92
+ end
62
93
  end
63
94
 
95
+ # A queue which limits the number of items that can be enqueued.
64
96
  # @public Since `stable-v1`.
65
97
  class LimitedQueue < Queue
66
- def initialize(limit = 1, **options)
98
+ # Create a new limited queue.
99
+ #
100
+ # @parameter limit [Integer] The maximum number of items that can be enqueued.
101
+ # @parameter full [Notification] The notification to use for signaling when the queue is full.
102
+ def initialize(limit = 1, full: Notification.new, **options)
67
103
  super(**options)
68
104
 
69
105
  @limit = limit
70
-
71
- @full = Notification.new
106
+ @full = full
72
107
  end
73
108
 
109
+ # @attribute [Integer] The maximum number of items that can be enqueued.
74
110
  attr :limit
75
111
 
76
112
  # @returns [Boolean] Whether trying to enqueue an item would block.
@@ -78,6 +114,11 @@ module Async
78
114
  @items.size >= @limit
79
115
  end
80
116
 
117
+ # Add an item to the queue.
118
+ #
119
+ # If the queue is full, this method will block until there is space available.
120
+ #
121
+ # @parameter item [Object] The item to add to the queue.
81
122
  def <<(item)
82
123
  while limited?
83
124
  @full.wait
@@ -86,7 +127,12 @@ module Async
86
127
  super
87
128
  end
88
129
 
89
- def enqueue *items
130
+ # Add multiple items to the queue.
131
+ #
132
+ # If the queue is full, this method will block until there is space available.
133
+ #
134
+ # @parameter items [Array] The items to add to the queue.
135
+ def enqueue(*items)
90
136
  while !items.empty?
91
137
  while limited?
92
138
  @full.wait
@@ -95,10 +141,15 @@ module Async
95
141
  available = @limit - @items.size
96
142
  @items.concat(items.shift(available))
97
143
 
98
- self.signal unless self.empty?
144
+ @available.signal unless self.empty?
99
145
  end
100
146
  end
101
147
 
148
+ # Remove and return the next item from the queue.
149
+ #
150
+ # If the queue is empty, this method will block until an item is available.
151
+ #
152
+ # @returns [Object] The next item in the queue.
102
153
  def dequeue
103
154
  item = super
104
155
 
data/lib/async/reactor.rb CHANGED
@@ -15,12 +15,14 @@ module Async
15
15
  Async(...)
16
16
  end
17
17
 
18
+ # Initialize the reactor and assign it to the current Fiber scheduler.
18
19
  def initialize(...)
19
20
  super
20
21
 
21
22
  Fiber.set_scheduler(self)
22
23
  end
23
24
 
25
+ # Close the reactor and remove it from the current Fiber scheduler.
24
26
  def scheduler_close
25
27
  self.close
26
28
  end
@@ -16,7 +16,11 @@ require 'resolv'
16
16
  module Async
17
17
  # Handles scheduling of fibers. Implements the fiber scheduler interface.
18
18
  class Scheduler < Node
19
+ # Raised when an operation is attempted on a closed scheduler.
19
20
  class ClosedError < RuntimeError
21
+ # Create a new error.
22
+ #
23
+ # @parameter message [String] The error message.
20
24
  def initialize(message = "Scheduler is closed!")
21
25
  super
22
26
  end
@@ -28,6 +32,11 @@ module Async
28
32
  true
29
33
  end
30
34
 
35
+ # Create a new scheduler.
36
+ #
37
+ # @public Since `stable-v1`.
38
+ # @parameter parent [Node | Nil] The parent node to use for task hierarchy.
39
+ # @parameter selector [IO::Event::Selector] The selector to use for event handling.
31
40
  def initialize(parent = nil, selector: nil)
32
41
  super(parent)
33
42
 
@@ -63,6 +72,9 @@ module Async
63
72
  end
64
73
  end
65
74
 
75
+ # Invoked when the fiber scheduler is being closed.
76
+ #
77
+ # Executes the run loop until all tasks are finished, then closes the scheduler.
66
78
  def scheduler_close
67
79
  # If the execution context (thread) was handling an exception, we want to exit as quickly as possible:
68
80
  unless $!
@@ -79,6 +91,7 @@ module Async
79
91
  end
80
92
  end
81
93
 
94
+ # Terminate all child tasks and close the scheduler.
82
95
  # @public Since `stable-v1`.
83
96
  def close
84
97
  # It's critical to stop all tasks. Otherwise they might be holding on to resources which are never closed/released correctly.
@@ -108,6 +121,7 @@ module Async
108
121
  @selector.nil?
109
122
  end
110
123
 
124
+ # @returns [String] A description of the scheduler.
111
125
  def to_s
112
126
  "\#<#{self.description} #{@children&.size || 0} children (#{stopped? ? 'stopped' : 'running'})>"
113
127
  end
@@ -135,10 +149,20 @@ module Async
135
149
  @selector.push(fiber)
136
150
  end
137
151
 
138
- def raise(*arguments)
139
- @selector.raise(*arguments)
152
+ # Raise an exception on a specified fiber with the given arguments.
153
+ #
154
+ # This internally schedules the current fiber to be ready, before raising the exception, so that it will later resume execution.
155
+ #
156
+ # @parameter fiber [Fiber] The fiber to raise the exception on.
157
+ # @parameter *arguments [Array] The arguments to pass to the fiber.
158
+ def raise(...)
159
+ @selector.raise(...)
140
160
  end
141
161
 
162
+ # Resume execution of the specified fiber.
163
+ #
164
+ # @parameter fiber [Fiber] The fiber to resume.
165
+ # @parameter arguments [Array] The arguments to pass to the fiber.
142
166
  def resume(fiber, *arguments)
143
167
  @selector.resume(fiber, *arguments)
144
168
  end
data/lib/async/task.rb CHANGED
@@ -13,18 +13,26 @@ require 'console/event/failure'
13
13
  require_relative 'node'
14
14
  require_relative 'condition'
15
15
 
16
+ Fiber.attr_accessor :async_task
17
+
16
18
  module Async
17
19
  # Raised when a task is explicitly stopped.
18
20
  class Stop < Exception
21
+ # Used to defer stopping the current task until later.
19
22
  class Later
23
+ # Create a new stop later operation.
24
+ #
25
+ # @parameter task [Task] The task to stop later.
20
26
  def initialize(task)
21
27
  @task = task
22
28
  end
23
29
 
30
+ # @returns [Boolean] Whether the task is alive.
24
31
  def alive?
25
32
  true
26
33
  end
27
34
 
35
+ # Transfer control to the operation - this will stop the task.
28
36
  def transfer
29
37
  @task.stop
30
38
  end
@@ -34,6 +42,9 @@ module Async
34
42
  # Raised if a timeout occurs on a specific Fiber. Handled gracefully by `Task`.
35
43
  # @public Since `stable-v1`.
36
44
  class TimeoutError < StandardError
45
+ # Create a new timeout error.
46
+ #
47
+ # @parameter message [String] The error message.
37
48
  def initialize(message = "execution expired")
38
49
  super
39
50
  end
@@ -41,7 +52,11 @@ module Async
41
52
 
42
53
  # @public Since `stable-v1`.
43
54
  class Task < Node
55
+ # Raised when a child task is created within a task that has finished execution.
44
56
  class FinishedError < RuntimeError
57
+ # Create a new finished error.
58
+ #
59
+ # @parameter message [String] The error message.
45
60
  def initialize(message = "Cannot create child task within a task that has finished execution!")
46
61
  super
47
62
  end
@@ -72,14 +87,21 @@ module Async
72
87
  @defer_stop = nil
73
88
  end
74
89
 
90
+ # @returns [Scheduler] The scheduler for this task.
75
91
  def reactor
76
92
  self.root
77
93
  end
78
94
 
95
+ # @returns [Array(Thread::Backtrace::Location) | Nil] The backtrace of the task, if available.
79
96
  def backtrace(*arguments)
80
97
  @fiber&.backtrace(*arguments)
81
98
  end
82
99
 
100
+ # Annotate the task with a description.
101
+ #
102
+ # This will internally try to annotate the fiber if it is running, otherwise it will annotate the task itself.
103
+ #
104
+ # @parameter annotation [String] The description to annotate the task with.
83
105
  def annotate(annotation, &block)
84
106
  if @fiber
85
107
  @fiber.annotate(annotation, &block)
@@ -88,6 +110,7 @@ module Async
88
110
  end
89
111
  end
90
112
 
113
+ # @returns [Object] The annotation of the task.
91
114
  def annotation
92
115
  if @fiber
93
116
  @fiber.annotation
@@ -96,6 +119,7 @@ module Async
96
119
  end
97
120
  end
98
121
 
122
+ # @returns [String] A description of the task and it's current status.
99
123
  def to_s
100
124
  "\#<#{self.description} (#{@status})>"
101
125
  end
@@ -115,10 +139,10 @@ module Async
115
139
  Fiber.scheduler.yield
116
140
  end
117
141
 
118
- # @attr fiber [Fiber] The fiber which is being used for the execution of this task.
142
+ # @attribute [Fiber] The fiber which is being used for the execution of this task.
119
143
  attr :fiber
120
144
 
121
- # Whether the internal fiber is alive, i.e. it
145
+ # @returns [Boolean] Whether the internal fiber is alive, i.e. it is actively executing.
122
146
  def alive?
123
147
  @fiber&.alive?
124
148
  end
@@ -130,38 +154,49 @@ module Async
130
154
  super && @block.nil? && @fiber.nil?
131
155
  end
132
156
 
133
- # Whether the task is running.
134
- # @returns [Boolean]
157
+ # @returns [Boolean] Whether the task is running.
135
158
  def running?
136
159
  @status == :running
137
160
  end
138
161
 
162
+ # @returns [Boolean] Whether the task failed with an exception.
139
163
  def failed?
140
164
  @status == :failed
141
165
  end
142
166
 
143
- # The task has been stopped
167
+ # @returns [Boolean] Whether the task has been stopped.
144
168
  def stopped?
145
169
  @status == :stopped
146
170
  end
147
171
 
148
- # The task has completed execution and generated a result.
172
+ # @returns [Boolean] Whether the task has completed execution and generated a result.
149
173
  def completed?
150
174
  @status == :completed
151
175
  end
152
176
 
153
177
  alias complete? completed?
154
178
 
155
- # @attr status [Symbol] The status of the execution of the fiber, one of `:initialized`, `:running`, `:complete`, `:stopped` or `:failed`.
179
+ # @attribute [Symbol] The status of the execution of the fiber, one of `:initialized`, `:running`, `:complete`, `:stopped` or `:failed`.
156
180
  attr :status
157
181
 
158
182
  # Begin the execution of the task.
183
+ #
184
+ # @raises [RuntimeError] If the task is already running.
159
185
  def run(*arguments)
160
186
  if @status == :initialized
161
187
  @status = :running
162
188
 
163
189
  schedule do
164
190
  @block.call(self, *arguments)
191
+ rescue => error
192
+ # I'm not completely happy with this overhead, but the alternative is to not log anything which makes debugging extremely difficult. Maybe we can introduce a debug wrapper which adds extra logging.
193
+ if @finished.nil?
194
+ Console::Event::Failure.for(error).emit("Task may have ended with unhandled exception.", severity: :warn)
195
+ # else
196
+ # Console::Event::Failure.for(error).emit(self, severity: :debug)
197
+ end
198
+
199
+ raise
165
200
  end
166
201
  else
167
202
  raise RuntimeError, "Task already running!"
@@ -169,6 +204,9 @@ module Async
169
204
  end
170
205
 
171
206
  # Run an asynchronous task as a child of the current task.
207
+ #
208
+ # @raises [FinishedError] If the task has already finished.
209
+ # @returns [Task] The child task.
172
210
  def async(*arguments, **options, &block)
173
211
  raise FinishedError if self.finished?
174
212
 
@@ -289,15 +327,16 @@ module Async
289
327
  # @returns [Task]
290
328
  # @raises[RuntimeError] If task was not {set!} for the current fiber.
291
329
  def self.current
292
- Thread.current[:async_task] or raise RuntimeError, "No async task available!"
330
+ Fiber.current.async_task or raise RuntimeError, "No async task available!"
293
331
  end
294
332
 
295
333
  # Check if there is a task defined for the current fiber.
296
- # @returns [Task | Nil]
334
+ # @returns [Interface(:async) | Nil]
297
335
  def self.current?
298
- Thread.current[:async_task]
336
+ Fiber.current.async_task
299
337
  end
300
338
 
339
+ # @returns [Boolean] Whether this task is the currently executing task.
301
340
  def current?
302
341
  Fiber.current.equal?(@fiber)
303
342
  end
@@ -326,21 +365,10 @@ module Async
326
365
  @status = :completed
327
366
  end
328
367
 
329
- # This is a very tricky aspect of tasks to get right. I've modelled it after `Thread` but it's slightly different in that the exception can propagate back up through the reactor. If the user writes code which raises an exception, that exception should always be visible, i.e. cause a failure. If it's not visible, such code fails silently and can be very difficult to debug.
330
- def failed!(exception = false, propagate = true)
368
+ # State transition into the failed state.
369
+ def failed!(exception = false)
331
370
  @result = exception
332
371
  @status = :failed
333
-
334
- if exception
335
- if propagate
336
- raise exception
337
- elsif @finished.nil?
338
- # If no one has called wait, we log this as a warning:
339
- Console::Event::Failure.for(exception).emit(self, "Task may have ended with unhandled exception.", severity: :warn)
340
- else
341
- Console::Event::Failure.for(exception).emit(self, severity: :debug)
342
- end
343
- end
344
372
  end
345
373
 
346
374
  def stopped!
@@ -371,30 +399,26 @@ module Async
371
399
 
372
400
  def schedule(&block)
373
401
  @fiber = Fiber.new(annotation: self.annotation) do
374
- set!
375
-
376
402
  begin
377
403
  completed!(yield)
378
- # Console.debug(self) {"Task was completed with #{@children.size} children!"}
379
404
  rescue Stop
380
405
  stopped!
381
406
  rescue StandardError => error
382
- failed!(error, false)
407
+ failed!(error)
383
408
  rescue Exception => exception
384
- failed!(exception, true)
409
+ failed!(exception)
410
+
411
+ # This is a critical failure, we should stop the reactor:
412
+ raise
385
413
  ensure
386
414
  # Console.info(self) {"Task ensure $! = #{$!} with #{@children&.size.inspect} children!"}
387
415
  finish!
388
416
  end
389
417
  end
390
418
 
419
+ @fiber.async_task = self
420
+
391
421
  self.root.resume(@fiber)
392
422
  end
393
-
394
- # Set the current fiber's `:async_task` to this task.
395
- def set!
396
- # This is actually fiber-local:
397
- Thread.current[:async_task] = self
398
- end
399
423
  end
400
424
  end
@@ -6,12 +6,21 @@
6
6
  require_relative 'condition'
7
7
 
8
8
  module Async
9
+ # A synchronization primitive that allows one task to wait for another task to resolve a value.
9
10
  class Variable
11
+ # Create a new variable.
12
+ #
13
+ # @parameter condition [Condition] The condition to use for synchronization.
10
14
  def initialize(condition = Condition.new)
11
15
  @condition = condition
12
16
  @value = nil
13
17
  end
14
18
 
19
+ # Resolve the value.
20
+ #
21
+ # Signals all waiting tasks.
22
+ #
23
+ # @parameter value [Object] The value to resolve.
15
24
  def resolve(value = true)
16
25
  @value = value
17
26
  condition = @condition
@@ -22,15 +31,22 @@ module Async
22
31
  condition.signal(value)
23
32
  end
24
33
 
34
+ # Whether the value has been resolved.
35
+ #
36
+ # @returns [Boolean] Whether the value has been resolved.
25
37
  def resolved?
26
38
  @condition.nil?
27
39
  end
28
40
 
41
+ # Wait for the value to be resolved.
42
+ #
43
+ # @returns [Object] The resolved value.
29
44
  def value
30
45
  @condition&.wait
31
46
  return @value
32
47
  end
33
48
 
49
+ # Alias for {#value}.
34
50
  def wait
35
51
  self.value
36
52
  end
data/lib/async/version.rb CHANGED
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2017-2024, by Samuel Williams.
5
5
 
6
6
  module Async
7
- VERSION = "2.12.1"
7
+ VERSION = "2.14.1"
8
8
  end
data/lib/async/waiter.rb CHANGED
@@ -6,6 +6,10 @@
6
6
  module Async
7
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
8
  class Waiter
9
+ # Create a waiter instance.
10
+ #
11
+ # @parameter parent [Interface(:async) | Nil] The parent task to use for asynchronous operations.
12
+ # @parameter finished [Async::Condition] The condition to signal when a task completes.
9
13
  def initialize(parent: nil, finished: Async::Condition.new)
10
14
  @finished = finished
11
15
  @done = []
@@ -15,8 +19,8 @@ module Async
15
19
 
16
20
  # Execute a child task and add it to the waiter.
17
21
  # @asynchronous Executes the given block concurrently.
18
- def async(parent: (@parent or Task.current), &block)
19
- parent.async do |task|
22
+ def async(parent: (@parent or Task.current), **options, &block)
23
+ parent.async(**options) do |task|
20
24
  yield(task)
21
25
  ensure
22
26
  @done << task
data/lib/async/wrapper.rb CHANGED
@@ -23,6 +23,7 @@ module Async
23
23
 
24
24
  attr_accessor :reactor
25
25
 
26
+ # Dup the underlying IO.
26
27
  def dup
27
28
  self.class.new(@io.dup)
28
29
  end
@@ -51,11 +52,12 @@ module Async
51
52
  @io.to_io.wait(::IO::READABLE|::IO::WRITABLE|::IO::PRIORITY, timeout) or raise TimeoutError
52
53
  end
53
54
 
54
- # Close the io and monitor.
55
+ # Close the underlying IO.
55
56
  def close
56
57
  @io.close
57
58
  end
58
59
 
60
+ # Whether the underlying IO is closed.
59
61
  def closed?
60
62
  @io.closed?
61
63
  end
data/lib/async.rb CHANGED
@@ -10,5 +10,6 @@ require_relative "async/reactor"
10
10
  require_relative "kernel/async"
11
11
  require_relative "kernel/sync"
12
12
 
13
+ # Asynchronous programming framework.
13
14
  module Async
14
15
  end
data/readme.md CHANGED
@@ -1,4 +1,4 @@
1
- # ![Async](logo.svg)
1
+ # ![Async](assets/logo.webp)
2
2
 
3
3
  Async is a composable asynchronous I/O framework for Ruby based on [io-event](https://github.com/socketry/io-event).
4
4
 
@@ -19,20 +19,17 @@ Async is a composable asynchronous I/O framework for Ruby based on [io-event](ht
19
19
 
20
20
  Please see the [project documentation](https://socketry.github.io/async/) for more details.
21
21
 
22
- - [Getting Started](https://socketry.github.io/async/guides/getting-started/index) - This guide shows how to add
23
- async to your project and run code asynchronously.
22
+ - [Getting Started](https://socketry.github.io/async/guides/getting-started/index) - This guide shows how to add async to your project and run code asynchronously.
24
23
 
25
- - [Asynchronous Tasks](https://socketry.github.io/async/guides/asynchronous-tasks/index) - This guide explains how
26
- asynchronous tasks work and how to use them.
24
+ - [Asynchronous Tasks](https://socketry.github.io/async/guides/asynchronous-tasks/index) - This guide explains how asynchronous tasks work and how to use them.
27
25
 
28
- - [Event Loop](https://socketry.github.io/async/guides/event-loop/index) - This guide gives an overview of how the
29
- event loop is implemented.
26
+ - [Event Loop](https://socketry.github.io/async/guides/event-loop/index) - This guide gives an overview of how the event loop is implemented.
30
27
 
31
- - [Compatibility](https://socketry.github.io/async/guides/compatibility/index) - This guide gives an overview of the
32
- compatibility of Async with Ruby and other frameworks.
28
+ - [Compatibility](https://socketry.github.io/async/guides/compatibility/index) - This guide gives an overview of the compatibility of Async with Ruby and other frameworks.
33
29
 
34
- - [Best Practices](https://socketry.github.io/async/guides/best-practices/index) - This guide gives an overview of
35
- best practices for using Async.
30
+ - [Best Practices](https://socketry.github.io/async/guides/best-practices/index) - This guide gives an overview of best practices for using Async.
31
+
32
+ - [Debugging](https://socketry.github.io/async/guides/debugging/index) - This guide explains how to debug issues with programs that use Async.
36
33
 
37
34
  ## Contributing
38
35
 
@@ -54,16 +51,9 @@ This project is governed by the [Contributor Covenant](https://www.contributor-c
54
51
 
55
52
  ## See Also
56
53
 
57
- - [async-io](https://github.com/socketry/async-io) — Asynchronous networking and sockets.
58
54
  - [async-http](https://github.com/socketry/async-http) — Asynchronous HTTP client/server.
59
- - [async-process](https://github.com/socketry/async-process) — Asynchronous process spawning/waiting.
60
55
  - [async-websocket](https://github.com/socketry/async-websocket) — Asynchronous client and server websockets.
61
56
  - [async-dns](https://github.com/socketry/async-dns) — Asynchronous DNS resolver and server.
62
- - [async-rspec](https://github.com/socketry/async-rspec) — Shared contexts for running async specs.
63
-
64
- ### Projects Using Async
65
-
66
- - [ciri](https://github.com/ciri-ethereum/ciri) — An Ethereum implementation written in Ruby.
67
57
  - [falcon](https://github.com/socketry/falcon) — A rack compatible server built on top of `async-http`.
68
58
  - [rubydns](https://github.com/ioquatix/rubydns) — An easy to use Ruby DNS server.
69
59
  - [slack-ruby-bot](https://github.com/slack-ruby/slack-ruby-bot) — A client for making slack bots.
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.12.1
4
+ version: 2.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -62,7 +62,7 @@ cert_chain:
62
62
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
63
63
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
64
64
  -----END CERTIFICATE-----
65
- date: 2024-06-23 00:00:00.000000000 Z
65
+ date: 2024-07-15 00:00:00.000000000 Z
66
66
  dependencies:
67
67
  - !ruby/object:Gem::Dependency
68
68
  name: console
metadata.gz.sig CHANGED
Binary file