async 2.12.1 → 2.16.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
 
@@ -62,43 +71,46 @@ module Async
62
71
  return @busy_time / total_time
63
72
  end
64
73
  end
65
-
66
- def scheduler_close
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.
78
+ def scheduler_close(error = $!)
67
79
  # If the execution context (thread) was handling an exception, we want to exit as quickly as possible:
68
- unless $!
80
+ unless error
69
81
  self.run
70
82
  end
71
83
  ensure
72
84
  self.close
73
85
  end
74
86
 
75
- # Terminate the scheduler. We deliberately ignore interrupts here, as this code can be called from an interrupt, and we don't want to be interrupted while cleaning up.
87
+ # Terminate all child tasks.
76
88
  def terminate
77
- Thread.handle_interrupt(::Interrupt => :never) do
78
- super
89
+ # If that doesn't work, take more serious action:
90
+ @children&.each do |child|
91
+ child.terminate
79
92
  end
93
+
94
+ return @children.nil?
80
95
  end
81
96
 
97
+ # Terminate all child tasks and close the scheduler.
82
98
  # @public Since `stable-v1`.
83
99
  def close
84
- # It's critical to stop all tasks. Otherwise they might be holding on to resources which are never closed/released correctly.
85
- until self.terminate
86
- self.run_once!
100
+ self.run_loop do
101
+ until self.terminate
102
+ self.run_once!
103
+ end
87
104
  end
88
105
 
89
106
  Kernel.raise "Closing scheduler with blocked operations!" if @blocked > 0
90
-
91
- # We depend on GVL for consistency:
92
- # @guard.synchronize do
93
-
107
+ ensure
94
108
  # We want `@selector = nil` to be a visible side effect from this point forward, specifically in `#interrupt` and `#unblock`. If the selector is closed, then we don't want to push any fibers to it.
95
109
  selector = @selector
96
110
  @selector = nil
97
111
 
98
112
  selector&.close
99
113
 
100
- # end
101
-
102
114
  consume
103
115
  end
104
116
 
@@ -108,6 +120,7 @@ module Async
108
120
  @selector.nil?
109
121
  end
110
122
 
123
+ # @returns [String] A description of the scheduler.
111
124
  def to_s
112
125
  "\#<#{self.description} #{@children&.size || 0} children (#{stopped? ? 'stopped' : 'running'})>"
113
126
  end
@@ -135,10 +148,20 @@ module Async
135
148
  @selector.push(fiber)
136
149
  end
137
150
 
138
- def raise(*arguments)
139
- @selector.raise(*arguments)
151
+ # Raise an exception on a specified fiber with the given arguments.
152
+ #
153
+ # This internally schedules the current fiber to be ready, before raising the exception, so that it will later resume execution.
154
+ #
155
+ # @parameter fiber [Fiber] The fiber to raise the exception on.
156
+ # @parameter *arguments [Array] The arguments to pass to the fiber.
157
+ def raise(...)
158
+ @selector.raise(...)
140
159
  end
141
160
 
161
+ # Resume execution of the specified fiber.
162
+ #
163
+ # @parameter fiber [Fiber] The fiber to resume.
164
+ # @parameter arguments [Array] The arguments to pass to the fiber.
142
165
  def resume(fiber, *arguments)
143
166
  @selector.resume(fiber, *arguments)
144
167
  end
@@ -218,7 +241,7 @@ module Async
218
241
  elsif timeout = get_timeout(io)
219
242
  # Otherwise, if we default to the io's timeout, we raise an exception:
220
243
  timer = @timers.after(timeout) do
221
- fiber.raise(::IO::TimeoutError, "Timeout while waiting for IO to become ready!")
244
+ fiber.raise(::IO::TimeoutError, "Timeout (#{timeout}s) while waiting for IO to become ready!")
222
245
  end
223
246
  end
224
247
 
@@ -233,7 +256,7 @@ module Async
233
256
 
234
257
  if timeout = get_timeout(io)
235
258
  timer = @timers.after(timeout) do
236
- fiber.raise(::IO::TimeoutError, "Timeout while waiting for IO to become readable!")
259
+ fiber.raise(::IO::TimeoutError, "Timeout (#{timeout}s) while waiting for IO to become readable!")
237
260
  end
238
261
  end
239
262
 
@@ -248,7 +271,7 @@ module Async
248
271
 
249
272
  if timeout = get_timeout(io)
250
273
  timer = @timers.after(timeout) do
251
- fiber.raise(::IO::TimeoutError, "Timeout while waiting for IO to become writable!")
274
+ fiber.raise(::IO::TimeoutError, "Timeout (#{timeout}s) while waiting for IO to become writable!")
252
275
  end
253
276
  end
254
277
 
@@ -268,28 +291,13 @@ module Async
268
291
  return @selector.process_wait(Fiber.current, pid, flags)
269
292
  end
270
293
 
271
- # Run one iteration of the event loop.
272
- # Does not handle interrupts.
273
- # @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite.
274
- # @returns [Boolean] Whether there is more work to do.
275
- def run_once(timeout = nil)
276
- Kernel::raise "Running scheduler on non-blocking fiber!" unless Fiber.blocking?
277
-
278
- # If we are finished, we stop the task tree and exit:
279
- if self.finished?
280
- return false
281
- end
282
-
283
- return run_once!(timeout)
284
- end
285
-
286
294
  # Run one iteration of the event loop.
287
295
  #
288
296
  # When terminating the event loop, we already know we are finished. So we don't need to check the task tree. This is a logical requirement because `run_once` ignores transient tasks. For example, a single top level transient task is not enough to keep the reactor running, but during termination we must still process it in order to terminate child tasks.
289
297
  #
290
298
  # @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite.
291
299
  # @returns [Boolean] Whether there is more work to do.
292
- private def run_once!(timeout = 0)
300
+ private def run_once!(timeout = nil)
293
301
  start_time = Async::Clock.now
294
302
 
295
303
  interval = @timers.wait_interval
@@ -326,6 +334,25 @@ module Async
326
334
  return true
327
335
  end
328
336
 
337
+ # Run one iteration of the event loop.
338
+ # Does not handle interrupts.
339
+ # @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite.
340
+ # @returns [Boolean] Whether there is more work to do.
341
+ def run_once(timeout = nil)
342
+ Kernel.raise "Running scheduler on non-blocking fiber!" unless Fiber.blocking?
343
+
344
+ if self.finished?
345
+ self.stop
346
+ end
347
+
348
+ # If we are finished, we stop the task tree and exit:
349
+ if @children.nil?
350
+ return false
351
+ end
352
+
353
+ return run_once!(timeout)
354
+ end
355
+
329
356
  # Checks and clears the interrupted state of the scheduler.
330
357
  # @returns [Boolean] Whether the reactor has been interrupted.
331
358
  private def interrupted?
@@ -341,22 +368,22 @@ module Async
341
368
  return false
342
369
  end
343
370
 
344
- # Run the reactor until all tasks are finished. Proxies arguments to {#async} immediately before entering the loop, if a block is provided.
345
- def run(...)
346
- Kernel::raise ClosedError if @selector.nil?
347
-
348
- initial_task = self.async(...) if block_given?
371
+ # Stop all children, including transient children, ignoring any signals.
372
+ def stop
373
+ @children&.each do |child|
374
+ child.stop
375
+ end
376
+ end
377
+
378
+ private def run_loop(&block)
349
379
  interrupt = nil
350
380
 
351
381
  begin
352
382
  # In theory, we could use Exception here to be a little bit safer, but we've only shown the case for SignalException to be a problem, so let's not over-engineer this.
353
383
  Thread.handle_interrupt(::SignalException => :never) do
354
- while true
355
- # If we are interrupted, we need to exit:
356
- break if self.interrupted?
357
-
384
+ until self.interrupted?
358
385
  # If we are finished, we need to exit:
359
- break unless self.run_once
386
+ break unless yield
360
387
  end
361
388
  end
362
389
  rescue Interrupt => interrupt
@@ -368,11 +395,20 @@ module Async
368
395
  end
369
396
 
370
397
  # If the event loop was interrupted, and we finished exiting normally (due to the interrupt), we need to re-raise the interrupt so that the caller can handle it too.
371
- Kernel.raise interrupt if interrupt
398
+ Kernel.raise(interrupt) if interrupt
399
+ end
400
+
401
+ # Run the reactor until all tasks are finished. Proxies arguments to {#async} immediately before entering the loop, if a block is provided.
402
+ def run(...)
403
+ Kernel.raise ClosedError if @selector.nil?
404
+
405
+ initial_task = self.async(...) if block_given?
406
+
407
+ self.run_loop do
408
+ run_once
409
+ end
372
410
 
373
411
  return initial_task
374
- ensure
375
- Console.debug(self) {"Exiting run-loop because #{$! ? $! : 'finished'}."}
376
412
  end
377
413
 
378
414
  # Start an asynchronous task within the specified reactor. The task will be
@@ -385,7 +421,7 @@ module Async
385
421
  # @returns [Task] The task that was scheduled into the reactor.
386
422
  # @deprecated With no replacement.
387
423
  def async(*arguments, **options, &block)
388
- Kernel::raise ClosedError if @selector.nil?
424
+ Kernel.raise ClosedError if @selector.nil?
389
425
 
390
426
  task = Task.new(Task.current? || self, **options, &block)
391
427
 
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
@@ -52,6 +67,13 @@ module Async
52
67
  Fiber.scheduler.transfer
53
68
  end
54
69
 
70
+ # Run the given block of code in a task, asynchronously, in the given scheduler.
71
+ def self.run(scheduler, *arguments, **options, &block)
72
+ self.new(scheduler, **options, &block).tap do |task|
73
+ task.run(*arguments)
74
+ end
75
+ end
76
+
55
77
  # Create a new task.
56
78
  # @parameter reactor [Reactor] the reactor this task will run within.
57
79
  # @parameter parent [Task] the parent task.
@@ -72,14 +94,21 @@ module Async
72
94
  @defer_stop = nil
73
95
  end
74
96
 
97
+ # @returns [Scheduler] The scheduler for this task.
75
98
  def reactor
76
99
  self.root
77
100
  end
78
101
 
102
+ # @returns [Array(Thread::Backtrace::Location) | Nil] The backtrace of the task, if available.
79
103
  def backtrace(*arguments)
80
104
  @fiber&.backtrace(*arguments)
81
105
  end
82
106
 
107
+ # Annotate the task with a description.
108
+ #
109
+ # This will internally try to annotate the fiber if it is running, otherwise it will annotate the task itself.
110
+ #
111
+ # @parameter annotation [String] The description to annotate the task with.
83
112
  def annotate(annotation, &block)
84
113
  if @fiber
85
114
  @fiber.annotate(annotation, &block)
@@ -88,6 +117,7 @@ module Async
88
117
  end
89
118
  end
90
119
 
120
+ # @returns [Object] The annotation of the task.
91
121
  def annotation
92
122
  if @fiber
93
123
  @fiber.annotation
@@ -96,6 +126,7 @@ module Async
96
126
  end
97
127
  end
98
128
 
129
+ # @returns [String] A description of the task and it's current status.
99
130
  def to_s
100
131
  "\#<#{self.description} (#{@status})>"
101
132
  end
@@ -115,10 +146,10 @@ module Async
115
146
  Fiber.scheduler.yield
116
147
  end
117
148
 
118
- # @attr fiber [Fiber] The fiber which is being used for the execution of this task.
149
+ # @attribute [Fiber] The fiber which is being used for the execution of this task.
119
150
  attr :fiber
120
151
 
121
- # Whether the internal fiber is alive, i.e. it
152
+ # @returns [Boolean] Whether the internal fiber is alive, i.e. it is actively executing.
122
153
  def alive?
123
154
  @fiber&.alive?
124
155
  end
@@ -130,38 +161,49 @@ module Async
130
161
  super && @block.nil? && @fiber.nil?
131
162
  end
132
163
 
133
- # Whether the task is running.
134
- # @returns [Boolean]
164
+ # @returns [Boolean] Whether the task is running.
135
165
  def running?
136
166
  @status == :running
137
167
  end
138
168
 
169
+ # @returns [Boolean] Whether the task failed with an exception.
139
170
  def failed?
140
171
  @status == :failed
141
172
  end
142
173
 
143
- # The task has been stopped
174
+ # @returns [Boolean] Whether the task has been stopped.
144
175
  def stopped?
145
176
  @status == :stopped
146
177
  end
147
178
 
148
- # The task has completed execution and generated a result.
179
+ # @returns [Boolean] Whether the task has completed execution and generated a result.
149
180
  def completed?
150
181
  @status == :completed
151
182
  end
152
183
 
153
184
  alias complete? completed?
154
185
 
155
- # @attr status [Symbol] The status of the execution of the fiber, one of `:initialized`, `:running`, `:complete`, `:stopped` or `:failed`.
186
+ # @attribute [Symbol] The status of the execution of the task, one of `:initialized`, `:running`, `:complete`, `:stopped` or `:failed`.
156
187
  attr :status
157
188
 
158
189
  # Begin the execution of the task.
190
+ #
191
+ # @raises [RuntimeError] If the task is already running.
159
192
  def run(*arguments)
160
193
  if @status == :initialized
161
194
  @status = :running
162
195
 
163
196
  schedule do
164
197
  @block.call(self, *arguments)
198
+ rescue => error
199
+ # 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.
200
+ if @finished.nil?
201
+ Console::Event::Failure.for(error).emit(self, "Task may have ended with unhandled exception.", severity: :warn)
202
+ else
203
+ # Console::Event::Failure.for(error).emit(self, severity: :debug)
204
+ end
205
+
206
+ raise
165
207
  end
166
208
  else
167
209
  raise RuntimeError, "Task already running!"
@@ -169,6 +211,9 @@ module Async
169
211
  end
170
212
 
171
213
  # Run an asynchronous task as a child of the current task.
214
+ #
215
+ # @raises [FinishedError] If the task has already finished.
216
+ # @returns [Task] The child task.
172
217
  def async(*arguments, **options, &block)
173
218
  raise FinishedError if self.finished?
174
219
 
@@ -215,15 +260,18 @@ module Async
215
260
  return stopped!
216
261
  end
217
262
 
218
- # If we are deferring stop...
219
- if @defer_stop == false
220
- # Don't stop now... but update the state so we know we need to stop later.
221
- @defer_stop = true
222
- return false
223
- end
224
-
225
263
  # If the fiber is alive, we need to stop it:
226
264
  if @fiber&.alive?
265
+ # As the task is now exiting, we want to ensure the event loop continues to execute until the task finishes.
266
+ self.transient = false
267
+
268
+ # If we are deferring stop...
269
+ if @defer_stop == false
270
+ # Don't stop now... but update the state so we know we need to stop later.
271
+ @defer_stop = true
272
+ return false
273
+ end
274
+
227
275
  if self.current?
228
276
  # If the fiber is current, and later is `true`, we need to schedule the fiber to be stopped later, as it's currently invoking `stop`:
229
277
  if later
@@ -238,7 +286,7 @@ module Async
238
286
  begin
239
287
  # There is a chance that this will stop the fiber that originally called stop. If that happens, the exception handling in `#stopped` will rescue the exception and re-raise it later.
240
288
  Fiber.scheduler.raise(@fiber, Stop)
241
- rescue FiberError
289
+ rescue FiberError => error
242
290
  # In some cases, this can cause a FiberError (it might be resumed already), so we schedule it to be stopped later:
243
291
  Fiber.scheduler.push(Stop::Later.new(self))
244
292
  end
@@ -263,19 +311,23 @@ module Async
263
311
  # - false: defer_stop has been called and we are not stopping.
264
312
  # - true: defer_stop has been called and we will stop when exiting the block.
265
313
  if @defer_stop.nil?
266
- # If we are not deferring stop already, we can defer it now:
267
- @defer_stop = false
268
-
269
314
  begin
315
+ # If we are not deferring stop already, we can defer it now:
316
+ @defer_stop = false
317
+
270
318
  yield
271
319
  rescue Stop
272
320
  # If we are exiting due to a stop, we shouldn't try to invoke stop again:
273
321
  @defer_stop = nil
274
322
  raise
275
323
  ensure
324
+ defer_stop = @defer_stop
325
+
326
+ # We need to ensure the state is reset before we exit the block:
327
+ @defer_stop = nil
328
+
276
329
  # If we were asked to stop, we should do so now:
277
- if @defer_stop
278
- @defer_stop = nil
330
+ if defer_stop
279
331
  raise Stop, "Stopping current task (was deferred)!"
280
332
  end
281
333
  end
@@ -285,19 +337,25 @@ module Async
285
337
  end
286
338
  end
287
339
 
340
+ # @returns [Boolean] Whether stop has been deferred.
341
+ def stop_deferred?
342
+ @defer_stop
343
+ end
344
+
288
345
  # Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
289
346
  # @returns [Task]
290
347
  # @raises[RuntimeError] If task was not {set!} for the current fiber.
291
348
  def self.current
292
- Thread.current[:async_task] or raise RuntimeError, "No async task available!"
349
+ Fiber.current.async_task or raise RuntimeError, "No async task available!"
293
350
  end
294
351
 
295
352
  # Check if there is a task defined for the current fiber.
296
- # @returns [Task | Nil]
353
+ # @returns [Interface(:async) | Nil]
297
354
  def self.current?
298
- Thread.current[:async_task]
355
+ Fiber.current.async_task
299
356
  end
300
357
 
358
+ # @returns [Boolean] Whether this task is the currently executing task.
301
359
  def current?
302
360
  Fiber.current.equal?(@fiber)
303
361
  end
@@ -326,21 +384,10 @@ module Async
326
384
  @status = :completed
327
385
  end
328
386
 
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)
387
+ # State transition into the failed state.
388
+ def failed!(exception = false)
331
389
  @result = exception
332
390
  @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
391
  end
345
392
 
346
393
  def stopped!
@@ -371,30 +418,26 @@ module Async
371
418
 
372
419
  def schedule(&block)
373
420
  @fiber = Fiber.new(annotation: self.annotation) do
374
- set!
375
-
376
421
  begin
377
422
  completed!(yield)
378
- # Console.debug(self) {"Task was completed with #{@children.size} children!"}
379
423
  rescue Stop
380
424
  stopped!
381
425
  rescue StandardError => error
382
- failed!(error, false)
426
+ failed!(error)
383
427
  rescue Exception => exception
384
- failed!(exception, true)
428
+ failed!(exception)
429
+
430
+ # This is a critical failure, we should stop the reactor:
431
+ raise
385
432
  ensure
386
433
  # Console.info(self) {"Task ensure $! = #{$!} with #{@children&.size.inspect} children!"}
387
434
  finish!
388
435
  end
389
436
  end
390
437
 
438
+ @fiber.async_task = self
439
+
391
440
  self.root.resume(@fiber)
392
441
  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
442
  end
400
443
  end
@@ -1,17 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2022, by Samuel Williams.
4
+ # Copyright, 2021-2024, by Samuel Williams.
5
5
 
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,17 +31,23 @@ module Async
22
31
  condition.signal(value)
23
32
  end
24
33
 
34
+ alias value= resolve
35
+
36
+ # Whether the value has been resolved.
37
+ #
38
+ # @returns [Boolean] Whether the value has been resolved.
25
39
  def resolved?
26
40
  @condition.nil?
27
41
  end
28
42
 
29
- def value
43
+ # Wait for the value to be resolved.
44
+ #
45
+ # @returns [Object] The resolved value.
46
+ def wait
30
47
  @condition&.wait
31
48
  return @value
32
49
  end
33
50
 
34
- def wait
35
- self.value
36
- end
51
+ alias value wait
37
52
  end
38
53
  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.16.1"
8
8
  end
data/lib/async/waiter.rb CHANGED
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022, by Samuel Williams.
4
+ # Copyright, 2022-2024, by Samuel Williams.
5
+ # Copyright, 2024, by Patrik Wenger.
5
6
 
6
7
  module Async
7
8
  # 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
9
  class Waiter
10
+ # Create a waiter instance.
11
+ #
12
+ # @parameter parent [Interface(:async) | Nil] The parent task to use for asynchronous operations.
13
+ # @parameter finished [Async::Condition] The condition to signal when a task completes.
9
14
  def initialize(parent: nil, finished: Async::Condition.new)
10
15
  @finished = finished
11
16
  @done = []
@@ -15,8 +20,8 @@ module Async
15
20
 
16
21
  # Execute a child task and add it to the waiter.
17
22
  # @asynchronous Executes the given block concurrently.
18
- def async(parent: (@parent or Task.current), &block)
19
- parent.async do |task|
23
+ def async(parent: (@parent or Task.current), **options, &block)
24
+ parent.async(**options) do |task|
20
25
  yield(task)
21
26
  ensure
22
27
  @done << task