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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/best-practices.md +188 -0
- data/context/debugging.md +63 -0
- data/context/getting-started.md +177 -0
- data/context/index.yaml +29 -0
- data/context/scheduler.md +109 -0
- data/context/tasks.md +448 -0
- data/context/thread-safety.md +651 -0
- data/lib/async/barrier.md +1 -2
- data/lib/async/barrier.rb +35 -12
- data/lib/async/clock.rb +11 -2
- data/lib/async/condition.md +1 -1
- data/lib/async/condition.rb +18 -34
- data/lib/async/console.rb +42 -0
- data/lib/async/deadline.rb +70 -0
- data/lib/async/idler.rb +2 -1
- data/lib/async/limited_queue.rb +13 -0
- data/lib/async/list.rb +16 -8
- data/lib/async/node.rb +5 -3
- data/lib/async/notification.rb +13 -9
- data/lib/async/priority_queue.rb +253 -0
- data/lib/async/promise.rb +188 -0
- data/lib/async/queue.rb +70 -82
- data/lib/async/reactor.rb +4 -2
- data/lib/async/scheduler.rb +233 -54
- data/lib/async/semaphore.rb +3 -3
- data/lib/async/stop.rb +82 -0
- data/lib/async/task.rb +111 -81
- data/lib/async/timeout.rb +88 -0
- data/lib/async/variable.rb +15 -4
- data/lib/async/version.rb +2 -2
- data/lib/async/waiter.rb +6 -1
- data/lib/kernel/async.rb +1 -1
- data/lib/kernel/sync.rb +14 -5
- data/lib/metrics/provider/async/task.rb +20 -0
- data/lib/metrics/provider/async.rb +6 -0
- data/lib/traces/provider/async/barrier.rb +17 -0
- data/lib/traces/provider/async/task.rb +40 -0
- data/lib/traces/provider/async.rb +7 -0
- data/license.md +8 -1
- data/readme.md +50 -7
- data/releases.md +357 -0
- data.tar.gz.sig +0 -0
- metadata +61 -20
- metadata.gz.sig +0 -0
- data/lib/async/waiter.md +0 -50
- 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-
|
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
|
11
|
-
require
|
12
|
+
require "fiber"
|
13
|
+
require "console"
|
12
14
|
|
13
|
-
require_relative
|
14
|
-
require_relative
|
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
|
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
|
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
|
-
@
|
91
|
-
|
92
|
-
|
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} (#{
|
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
|
-
|
161
|
+
self.alive?
|
167
162
|
end
|
168
163
|
|
169
164
|
# @returns [Boolean] Whether the task failed with an exception.
|
170
165
|
def failed?
|
171
|
-
@
|
166
|
+
@promise.failed?
|
172
167
|
end
|
173
168
|
|
174
169
|
# @returns [Boolean] Whether the task has been stopped.
|
175
170
|
def stopped?
|
176
|
-
@
|
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
|
-
@
|
176
|
+
@promise.completed?
|
182
177
|
end
|
183
178
|
|
184
|
-
|
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
|
-
|
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
|
-
|
194
|
-
|
202
|
+
# Move from initialized to running by clearing @block
|
203
|
+
if block = @block
|
204
|
+
@block = nil
|
195
205
|
|
196
206
|
schedule do
|
197
|
-
|
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
|
-
|
201
|
-
|
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
|
-
#
|
237
|
-
|
238
|
-
@
|
239
|
-
|
240
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
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
|
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
|
-
|
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
|
-
|
384
|
-
@
|
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
|
-
|
390
|
-
@
|
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
|
-
|
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.
|
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
|
data/lib/async/variable.rb
CHANGED
@@ -1,17 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2021-
|
4
|
+
# Copyright, 2021-2025, by Samuel Williams.
|
5
|
+
# Copyright, 2025, by Shopify Inc.
|
5
6
|
|
6
|
-
require_relative
|
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
|
-
|
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
|
-
|
59
|
+
# Alias for {#wait}.
|
60
|
+
def value
|
61
|
+
self.wait
|
62
|
+
end
|
52
63
|
end
|
53
64
|
end
|
data/lib/async/version.rb
CHANGED
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-
|
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
|
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-
|
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
|
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
|
-
|
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
|
-
|
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,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
|