async 2.17.0 → 2.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/context/best-practices.md +188 -0
  4. data/context/debugging.md +63 -0
  5. data/context/getting-started.md +177 -0
  6. data/context/index.yaml +29 -0
  7. data/context/scheduler.md +109 -0
  8. data/context/tasks.md +448 -0
  9. data/context/thread-safety.md +651 -0
  10. data/lib/async/barrier.md +1 -2
  11. data/lib/async/barrier.rb +35 -12
  12. data/lib/async/clock.rb +11 -2
  13. data/lib/async/condition.md +1 -1
  14. data/lib/async/condition.rb +18 -34
  15. data/lib/async/console.rb +42 -0
  16. data/lib/async/deadline.rb +70 -0
  17. data/lib/async/idler.rb +2 -1
  18. data/lib/async/limited_queue.rb +13 -0
  19. data/lib/async/list.rb +16 -8
  20. data/lib/async/node.rb +5 -3
  21. data/lib/async/notification.rb +13 -9
  22. data/lib/async/priority_queue.rb +253 -0
  23. data/lib/async/promise.rb +188 -0
  24. data/lib/async/queue.rb +70 -82
  25. data/lib/async/reactor.rb +4 -2
  26. data/lib/async/scheduler.rb +233 -54
  27. data/lib/async/semaphore.rb +3 -3
  28. data/lib/async/stop.rb +82 -0
  29. data/lib/async/task.rb +111 -81
  30. data/lib/async/timeout.rb +88 -0
  31. data/lib/async/variable.rb +15 -4
  32. data/lib/async/version.rb +2 -2
  33. data/lib/async/waiter.rb +6 -1
  34. data/lib/kernel/async.rb +1 -1
  35. data/lib/kernel/sync.rb +14 -5
  36. data/lib/metrics/provider/async/task.rb +20 -0
  37. data/lib/metrics/provider/async.rb +6 -0
  38. data/lib/traces/provider/async/barrier.rb +17 -0
  39. data/lib/traces/provider/async/task.rb +40 -0
  40. data/lib/traces/provider/async.rb +7 -0
  41. data/license.md +8 -1
  42. data/readme.md +50 -7
  43. data/releases.md +357 -0
  44. data.tar.gz.sig +0 -0
  45. metadata +61 -20
  46. metadata.gz.sig +0 -0
  47. data/lib/async/waiter.md +0 -50
  48. data/lib/async/wrapper.rb +0 -65
data/lib/async/task.rb CHANGED
@@ -1,46 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2024, by Samuel Williams.
4
+ # Copyright, 2017-2025, by Samuel Williams.
5
5
  # Copyright, 2017, by Kent Gruber.
6
6
  # Copyright, 2017, by Devin Christensen.
7
7
  # Copyright, 2020, by Patrik Wenger.
8
8
  # Copyright, 2023, by Math Ieu.
9
+ # Copyright, 2025, by Shigeru Nakajima.
10
+ # Copyright, 2025, by Shopify Inc.
9
11
 
10
- require 'fiber'
11
- require 'console/event/failure'
12
+ require "fiber"
13
+ require "console"
12
14
 
13
- require_relative 'node'
14
- require_relative 'condition'
15
+ require_relative "node"
16
+ require_relative "condition"
17
+ require_relative "promise"
18
+ require_relative "stop"
15
19
 
16
20
  Fiber.attr_accessor :async_task
17
21
 
18
22
  module Async
19
- # Raised when a task is explicitly stopped.
20
- class Stop < Exception
21
- # Used to defer stopping the current task until later.
22
- class Later
23
- # Create a new stop later operation.
24
- #
25
- # @parameter task [Task] The task to stop later.
26
- def initialize(task)
27
- @task = task
28
- end
29
-
30
- # @returns [Boolean] Whether the task is alive.
31
- def alive?
32
- true
33
- end
34
-
35
- # Transfer control to the operation - this will stop the task.
36
- def transfer
37
- @task.stop
38
- end
39
- end
40
- end
41
-
42
23
  # Raised if a timeout occurs on a specific Fiber. Handled gracefully by `Task`.
43
- # @public Since `stable-v1`.
24
+ # @public Since *Async v1*.
44
25
  class TimeoutError < StandardError
45
26
  # Create a new timeout error.
46
27
  #
@@ -50,7 +31,7 @@ module Async
50
31
  end
51
32
  end
52
33
 
53
- # @public Since `stable-v1`.
34
+ # @public Since *Async v1*.
54
35
  class Task < Node
55
36
  # Raised when a child task is created within a task that has finished execution.
56
37
  class FinishedError < RuntimeError
@@ -64,6 +45,8 @@ module Async
64
45
 
65
46
  # @deprecated With no replacement.
66
47
  def self.yield
48
+ warn("`Async::Task.yield` is deprecated with no replacement.", uplevel: 1, category: :deprecated) if $VERBOSE
49
+
67
50
  Fiber.scheduler.transfer
68
51
  end
69
52
 
@@ -82,14 +65,24 @@ module Async
82
65
 
83
66
  # These instance variables are critical to the state of the task.
84
67
  # In the initialized state, the @block should be set, but the @fiber should be nil.
85
- # In the running state, the @fiber should be set.
68
+ # In the running state, the @fiber should be set, and @block should be nil.
86
69
  # In a finished state, the @block should be nil, and the @fiber should be nil.
87
70
  @block = block
88
71
  @fiber = nil
89
72
 
90
- @status = :initialized
91
- @result = nil
92
- @finished = finished
73
+ @promise = Promise.new
74
+
75
+ # Handle finished: parameter for backward compatibility:
76
+ case finished
77
+ when false
78
+ # `finished: false` suppresses warnings for expected task failures:
79
+ @promise.suppress_warnings!
80
+ when nil
81
+ # `finished: nil` is the default, no special handling:
82
+ else
83
+ # All other `finished:` values are deprecated:
84
+ warn("finished: argument with non-false value is deprecated and will be removed.", uplevel: 1, category: :deprecated) if $VERBOSE
85
+ end
93
86
 
94
87
  @defer_stop = nil
95
88
  end
@@ -128,11 +121,13 @@ module Async
128
121
 
129
122
  # @returns [String] A description of the task and it's current status.
130
123
  def to_s
131
- "\#<#{self.description} (#{@status})>"
124
+ "\#<#{self.description} (#{self.status})>"
132
125
  end
133
126
 
134
127
  # @deprecated Prefer {Kernel#sleep} except when compatibility with `stable-v1` is required.
135
128
  def sleep(duration = nil)
129
+ Kernel.warn("`Async::Task#sleep` is deprecated, use `Kernel#sleep` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
130
+
136
131
  super
137
132
  end
138
133
 
@@ -163,44 +158,57 @@ module Async
163
158
 
164
159
  # @returns [Boolean] Whether the task is running.
165
160
  def running?
166
- @status == :running
161
+ self.alive?
167
162
  end
168
163
 
169
164
  # @returns [Boolean] Whether the task failed with an exception.
170
165
  def failed?
171
- @status == :failed
166
+ @promise.failed?
172
167
  end
173
168
 
174
169
  # @returns [Boolean] Whether the task has been stopped.
175
170
  def stopped?
176
- @status == :stopped
171
+ @promise.cancelled?
177
172
  end
178
173
 
179
174
  # @returns [Boolean] Whether the task has completed execution and generated a result.
180
175
  def completed?
181
- @status == :completed
176
+ @promise.completed?
182
177
  end
183
178
 
184
- alias complete? completed?
179
+ # Alias for {#completed?}.
180
+ def complete?
181
+ self.completed?
182
+ end
185
183
 
186
184
  # @attribute [Symbol] The status of the execution of the task, one of `:initialized`, `:running`, `:complete`, `:stopped` or `:failed`.
187
- attr :status
185
+ def status
186
+ case @promise.resolved
187
+ when :cancelled
188
+ :stopped
189
+ when :failed
190
+ :failed
191
+ when :completed
192
+ :completed
193
+ when nil
194
+ self.running? ? :running : :initialized
195
+ end
196
+ end
188
197
 
189
198
  # Begin the execution of the task.
190
199
  #
191
200
  # @raises [RuntimeError] If the task is already running.
192
201
  def run(*arguments)
193
- if @status == :initialized
194
- @status = :running
202
+ # Move from initialized to running by clearing @block
203
+ if block = @block
204
+ @block = nil
195
205
 
196
206
  schedule do
197
- @block.call(self, *arguments)
207
+ block.call(self, *arguments)
198
208
  rescue => error
199
209
  # 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)
210
+ unless @promise.waiting?
211
+ warn(self, "Task may have ended with unhandled exception.", exception: error)
204
212
  end
205
213
 
206
214
  raise
@@ -212,6 +220,10 @@ module Async
212
220
 
213
221
  # Run an asynchronous task as a child of the current task.
214
222
  #
223
+ # @public Since *Async v1*.
224
+ # @asynchronous May context switch immediately to the new task.
225
+ #
226
+ # @yields {|task| ...} in the context of the new task.
215
227
  # @raises [FinishedError] If the task has already finished.
216
228
  # @returns [Task] The child task.
217
229
  def async(*arguments, **options, &block)
@@ -219,6 +231,13 @@ module Async
219
231
 
220
232
  task = Task.new(self, **options, &block)
221
233
 
234
+ # When calling an async block, we deterministically execute it until the first blocking operation. We don't *have* to do this - we could schedule it for later execution, but it's useful to:
235
+ #
236
+ # - Fail at the point of the method call where possible.
237
+ # - Execute determinstically where possible.
238
+ # - Avoid scheduler overhead if no blocking operation is performed.
239
+ #
240
+ # There are different strategies (greedy vs non-greedy). We are currently using a greedy strategy.
222
241
  task.run(*arguments)
223
242
 
224
243
  return task
@@ -230,31 +249,42 @@ module Async
230
249
  #
231
250
  # @raises [RuntimeError] If the task's fiber is the current fiber.
232
251
  # @returns [Object] The final expression/result of the task's block.
252
+ # @asynchronous This method is thread-safe.
233
253
  def wait
234
254
  raise "Cannot wait on own fiber!" if Fiber.current.equal?(@fiber)
235
255
 
236
- # `finish!` will set both of these to nil before signaling the condition:
237
- if @block || @fiber
238
- @finished ||= Condition.new
239
- @finished.wait
240
- end
241
-
242
- if @status == :failed
243
- raise @result
244
- else
245
- return @result
256
+ # Wait for the task to complete - Promise handles all the complexity:
257
+ begin
258
+ @promise.wait
259
+ rescue Promise::Cancel
260
+ # For backward compatibility, stopped tasks return nil
261
+ return nil
246
262
  end
247
263
  end
248
264
 
249
265
  # Access the result of the task without waiting. May be nil if the task is not completed. Does not raise exceptions.
250
- attr :result
266
+ def result
267
+ value = @promise.value
268
+ # For backward compatibility, return nil for stopped tasks
269
+ if @promise.cancelled?
270
+ nil
271
+ else
272
+ value
273
+ end
274
+ end
251
275
 
252
276
  # Stop the task and all of its children.
253
277
  #
254
278
  # If `later` is false, it means that `stop` has been invoked directly. When `later` is true, it means that `stop` is invoked by `stop_children` or some other indirect mechanism. In that case, if we encounter the "current" fiber, we can't stop it right away, as it's currently performing `#stop`. Stopping it immediately would interrupt the current stop traversal, so we need to schedule the stop to occur later.
255
279
  #
256
280
  # @parameter later [Boolean] Whether to stop the task later, or immediately.
257
- def stop(later = false)
281
+ # @parameter cause [Exception] The cause of the stop operation.
282
+ def stop(later = false, cause: $!)
283
+ # If no cause is given, we generate one from the current call stack:
284
+ unless cause
285
+ cause = Stop::Cause.for("Stopping task!")
286
+ end
287
+
258
288
  if self.stopped?
259
289
  # If the task is already stopped, a `stop` state transition re-enters the same state which is a no-op. However, we will also attempt to stop any running children too. This can happen if the children did not stop correctly the first time around. Doing this should probably be considered a bug, but it's better to be safe than sorry.
260
290
  return stopped!
@@ -268,7 +298,7 @@ module Async
268
298
  # If we are deferring stop...
269
299
  if @defer_stop == false
270
300
  # Don't stop now... but update the state so we know we need to stop later.
271
- @defer_stop = true
301
+ @defer_stop = cause
272
302
  return false
273
303
  end
274
304
 
@@ -276,19 +306,19 @@ module Async
276
306
  # 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`:
277
307
  if later
278
308
  # If the fiber is the current fiber and we want to stop it later, schedule it:
279
- Fiber.scheduler.push(Stop::Later.new(self))
309
+ Fiber.scheduler.push(Stop::Later.new(self, cause))
280
310
  else
281
311
  # Otherwise, raise the exception directly:
282
- raise Stop, "Stopping current task!"
312
+ raise Stop, "Stopping current task!", cause: cause
283
313
  end
284
314
  else
285
315
  # If the fiber is not curent, we can raise the exception directly:
286
316
  begin
287
317
  # 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.
288
- Fiber.scheduler.raise(@fiber, Stop)
289
- rescue FiberError => error
318
+ Fiber.scheduler.raise(@fiber, Stop, cause: cause)
319
+ rescue FiberError
290
320
  # In some cases, this can cause a FiberError (it might be resumed already), so we schedule it to be stopped later:
291
- Fiber.scheduler.push(Stop::Later.new(self))
321
+ Fiber.scheduler.push(Stop::Later.new(self, cause))
292
322
  end
293
323
  end
294
324
  else
@@ -304,7 +334,7 @@ module Async
304
334
  # If stop is invoked a second time, it will be immediately executed.
305
335
  #
306
336
  # @yields {} The block of code to execute.
307
- # @public Since `stable-v1`.
337
+ # @public Since *Async v1*.
308
338
  def defer_stop
309
339
  # Tri-state variable for controlling stop:
310
340
  # - nil: defer_stop has not been called.
@@ -328,7 +358,7 @@ module Async
328
358
 
329
359
  # If we were asked to stop, we should do so now:
330
360
  if defer_stop
331
- raise Stop, "Stopping current task (was deferred)!"
361
+ raise Stop, "Stopping current task (was deferred)!", cause: defer_stop
332
362
  end
333
363
  end
334
364
  else
@@ -339,7 +369,7 @@ module Async
339
369
 
340
370
  # @returns [Boolean] Whether stop has been deferred.
341
371
  def stop_deferred?
342
- @defer_stop
372
+ !!@defer_stop
343
373
  end
344
374
 
345
375
  # Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
@@ -362,6 +392,10 @@ module Async
362
392
 
363
393
  private
364
394
 
395
+ def warn(...)
396
+ Console.warn(...)
397
+ end
398
+
365
399
  # Finish the current task, moving any children to the parent.
366
400
  def finish!
367
401
  # Don't hold references to the fiber or block after the task has finished:
@@ -370,29 +404,25 @@ module Async
370
404
 
371
405
  # Attempt to remove this node from the task tree.
372
406
  consume
373
-
374
- # If this task was being used as a future, signal completion here:
375
- if @finished
376
- @finished.signal(self)
377
- @finished = nil
378
- end
379
407
  end
380
408
 
381
409
  # State transition into the completed state.
382
410
  def completed!(result)
383
- @result = result
384
- @status = :completed
411
+ # Resolve the promise with the result:
412
+ @promise&.resolve(result)
385
413
  end
386
414
 
387
415
  # State transition into the failed state.
388
416
  def failed!(exception = false)
389
- @result = exception
390
- @status = :failed
417
+ # Reject the promise with the exception:
418
+ @promise&.reject(exception)
391
419
  end
392
420
 
393
421
  def stopped!
394
422
  # Console.info(self, status:) {"Task #{self} was stopped with #{@children&.size.inspect} children!"}
395
- @status = :stopped
423
+
424
+ # Cancel the promise:
425
+ @promise&.cancel
396
426
 
397
427
  stopped = false
398
428
 
@@ -437,7 +467,7 @@ module Async
437
467
 
438
468
  @fiber.async_task = self
439
469
 
440
- self.root.resume(@fiber)
470
+ (Fiber.scheduler || self.reactor).resume(@fiber)
441
471
  end
442
472
  end
443
473
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ module Async
7
+ # Represents a flexible timeout that can be rescheduled or extended.
8
+ # @public Since *Async v2.24*.
9
+ class Timeout
10
+ # Initialize a new timeout.
11
+ def initialize(timers, handle)
12
+ @timers = timers
13
+ @handle = handle
14
+ end
15
+
16
+ # @returns [Numeric] The time remaining until the timeout occurs, in seconds.
17
+ def duration
18
+ @handle.time - @timers.now
19
+ end
20
+
21
+ # Update the duration of the timeout.
22
+ #
23
+ # The duration is relative to the current time, e.g. setting the duration to 5 means the timeout will occur in 5 seconds from now.
24
+ #
25
+ # @parameter value [Numeric] The new duration to assign to the timeout, in seconds.
26
+ def duration=(value)
27
+ self.reschedule(@timers.now + value)
28
+ end
29
+
30
+ # Adjust the timeout by the specified duration.
31
+ #
32
+ # The duration is relative to the timeout time, e.g. adjusting the timeout by 5 increases the current duration by 5 seconds.
33
+ #
34
+ # @parameter duration [Numeric] The duration to adjust the timeout by, in seconds.
35
+ # @returns [Numeric] The new time at which the timeout will occur.
36
+ def adjust(duration)
37
+ self.reschedule(time + duration)
38
+ end
39
+
40
+ # @returns [Numeric] The time at which the timeout will occur, in seconds since {now}.
41
+ def time
42
+ @handle.time
43
+ end
44
+
45
+ # Assign a new time to the timeout, rescheduling it if necessary.
46
+ #
47
+ # @parameter value [Numeric] The new time to assign to the timeout.
48
+ # @returns [Numeric] The new time at which the timeout will occur.
49
+ def time=(value)
50
+ self.reschedule(value)
51
+ end
52
+
53
+ # @returns [Numeric] The current time in the scheduler, relative to the time of this timeout, in seconds.
54
+ def now
55
+ @timers.now
56
+ end
57
+
58
+ # Cancel the timeout, preventing it from executing.
59
+ def cancel!
60
+ @handle.cancel!
61
+ end
62
+
63
+ # @returns [Boolean] Whether the timeout has been cancelled.
64
+ def cancelled?
65
+ @handle.cancelled?
66
+ end
67
+
68
+ # Raised when attempting to reschedule a cancelled timeout.
69
+ class CancelledError < RuntimeError
70
+ end
71
+
72
+ # Reschedule the timeout to occur at the specified time.
73
+ #
74
+ # @parameter time [Numeric] The new time to schedule the timeout for.
75
+ # @returns [Numeric] The new time at which the timeout will occur.
76
+ private def reschedule(time)
77
+ if block = @handle&.block
78
+ @handle.cancel!
79
+
80
+ @handle = @timers.schedule(time, block)
81
+
82
+ return time
83
+ else
84
+ raise CancelledError, "Cannot reschedule a cancelled timeout!"
85
+ end
86
+ end
87
+ end
88
+ end
@@ -1,17 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2024, by Samuel Williams.
4
+ # Copyright, 2021-2025, by Samuel Williams.
5
+ # Copyright, 2025, by Shopify Inc.
5
6
 
6
- require_relative 'condition'
7
+ require_relative "condition"
7
8
 
8
9
  module Async
9
10
  # A synchronization primitive that allows one task to wait for another task to resolve a value.
11
+ #
12
+ # @deprecated Use {Async::Promise} instead.
10
13
  class Variable
11
14
  # Create a new variable.
12
15
  #
13
16
  # @parameter condition [Condition] The condition to use for synchronization.
14
17
  def initialize(condition = Condition.new)
18
+ warn("`Async::Variable` is deprecated, use `Async::Promise` instead.", category: :deprecated, uplevel: 1) if $VERBOSE
19
+
15
20
  @condition = condition
16
21
  @value = nil
17
22
  end
@@ -31,7 +36,10 @@ module Async
31
36
  condition.signal(value)
32
37
  end
33
38
 
34
- alias value= resolve
39
+ # Alias for {#resolve}.
40
+ def value=(value)
41
+ self.resolve(value)
42
+ end
35
43
 
36
44
  # Whether the value has been resolved.
37
45
  #
@@ -48,6 +56,9 @@ module Async
48
56
  return @value
49
57
  end
50
58
 
51
- alias value wait
59
+ # Alias for {#wait}.
60
+ def value
61
+ self.wait
62
+ end
52
63
  end
53
64
  end
data/lib/async/version.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2024, by Samuel Williams.
4
+ # Copyright, 2017-2025, by Samuel Williams.
5
5
 
6
6
  module Async
7
- VERSION = "2.17.0"
7
+ VERSION = "2.32.0"
8
8
  end
data/lib/async/waiter.rb CHANGED
@@ -1,17 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2025, by Samuel Williams.
5
5
  # Copyright, 2024, by Patrik Wenger.
6
+ # Copyright, 2025, by Shopify Inc.
6
7
 
7
8
  module Async
8
9
  # 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}.
10
+ #
11
+ # @deprecated `Async::Waiter` is deprecated, use `Async::Barrier` instead.
9
12
  class Waiter
10
13
  # Create a waiter instance.
11
14
  #
12
15
  # @parameter parent [Interface(:async) | Nil] The parent task to use for asynchronous operations.
13
16
  # @parameter finished [Async::Condition] The condition to signal when a task completes.
14
17
  def initialize(parent: nil, finished: Async::Condition.new)
18
+ warn("`Async::Waiter` is deprecated, use `Async::Barrier` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
19
+
15
20
  @finished = finished
16
21
  @done = []
17
22
 
data/lib/kernel/async.rb CHANGED
@@ -19,7 +19,7 @@ module Kernel
19
19
  # @yields {|task| ...} The block that will execute asynchronously.
20
20
  # @parameter task [Async::Task] The task that is executing the given block.
21
21
  #
22
- # @public Since `stable-v1`.
22
+ # @public Since *Async v1*.
23
23
  # @asynchronous May block until given block completes executing.
24
24
  def Async(...)
25
25
  if current = ::Async::Task.current?
data/lib/kernel/sync.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
  # Copyright, 2020, by Brian Morearty.
6
+ # Copyright, 2024, by Patrik Wenger.
7
+ # Copyright, 2025, by Shopify Inc.
6
8
 
7
9
  require_relative "../async/reactor"
8
10
 
@@ -13,11 +15,15 @@ module Kernel
13
15
  # @yields {|task| ...} The block that will execute asynchronously.
14
16
  # @parameter task [Async::Task] The task that is executing the given block.
15
17
  #
16
- # @public Since `stable-v1`.
18
+ # @public Since *Async v1*.
17
19
  # @asynchronous Will block until given block completes executing.
18
- def Sync(&block)
20
+ def Sync(annotation: nil, &block)
19
21
  if task = ::Async::Task.current?
20
- yield task
22
+ if annotation
23
+ task.annotate(annotation) {yield task}
24
+ else
25
+ yield task
26
+ end
21
27
  elsif scheduler = Fiber.scheduler
22
28
  ::Async::Task.run(scheduler, &block).wait
23
29
  else
@@ -25,7 +31,10 @@ module Kernel
25
31
  reactor = Async::Reactor.new
26
32
 
27
33
  begin
28
- return reactor.run(finished: ::Async::Condition.new, &block).wait
34
+ # Use finished: false to suppress warnings since we're handling exceptions explicitly
35
+ task = reactor.async(annotation: annotation, finished: false, &block)
36
+ reactor.run
37
+ return task.wait
29
38
  ensure
30
39
  Fiber.set_scheduler(nil)
31
40
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
+
6
+ require_relative "../../../async/task"
7
+ require "metrics/provider"
8
+
9
+ Metrics::Provider(Async::Task) do
10
+ ASYNC_TASK_SCHEDULED = Metrics.metric("async.task.scheduled", :counter, description: "The number of tasks scheduled.")
11
+ ASYNC_TASK_FINISHED = Metrics.metric("async.task.finished", :counter, description: "The number of tasks finished.")
12
+
13
+ def schedule(&block)
14
+ ASYNC_TASK_SCHEDULED.emit(1)
15
+
16
+ super(&block)
17
+ ensure
18
+ ASYNC_TASK_FINISHED.emit(1)
19
+ end
20
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative "async/task"
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
+
6
+ require_relative "../../../async/barrier"
7
+ require "traces/provider"
8
+
9
+ Traces::Provider(Async::Barrier) do
10
+ def wait
11
+ attributes = {
12
+ "size" => self.size
13
+ }
14
+
15
+ Traces.trace("async.barrier.wait", attributes: attributes) {super}
16
+ end
17
+ end