async 2.32.0 → 2.39.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 +53 -32
- data/context/debugging.md +3 -3
- data/context/getting-started.md +6 -6
- data/context/scheduler.md +4 -4
- data/context/tasks.md +41 -36
- data/context/thread-safety.md +267 -224
- data/lib/async/barrier.rb +41 -10
- data/lib/async/cancel.rb +80 -0
- data/lib/async/clock.rb +22 -1
- data/lib/async/deadline.rb +1 -0
- data/lib/async/error.rb +17 -0
- data/lib/async/fork_handler.rb +32 -0
- data/lib/async/idler.rb +27 -15
- data/lib/async/loop.rb +84 -0
- data/lib/async/node.rb +28 -9
- data/lib/async/promise.rb +112 -37
- data/lib/async/queue.rb +1 -1
- data/lib/async/scheduler.rb +40 -17
- data/lib/async/stop.rb +3 -75
- data/lib/async/task.rb +160 -91
- data/lib/async/version.rb +3 -2
- data/lib/async.rb +3 -5
- data/lib/kernel/barrier.rb +31 -0
- data/lib/kernel/sync.rb +1 -1
- data/lib/traces/provider/async/barrier.rb +1 -1
- data/license.md +4 -2
- data/readme.md +26 -32
- data/releases.md +104 -33
- data.tar.gz.sig +0 -0
- metadata +9 -3
- metadata.gz.sig +0 -0
- data/lib/async/task.md +0 -30
data/lib/async/task.rb
CHANGED
|
@@ -1,36 +1,52 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2017-
|
|
4
|
+
# Copyright, 2017-2026, 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
9
|
# Copyright, 2025, by Shigeru Nakajima.
|
|
10
|
-
# Copyright, 2025, by Shopify Inc.
|
|
10
|
+
# Copyright, 2025-2026, by Shopify Inc.
|
|
11
11
|
|
|
12
12
|
require "fiber"
|
|
13
13
|
require "console"
|
|
14
14
|
|
|
15
15
|
require_relative "node"
|
|
16
16
|
require_relative "condition"
|
|
17
|
+
require_relative "error"
|
|
17
18
|
require_relative "promise"
|
|
18
19
|
require_relative "stop"
|
|
19
20
|
|
|
20
21
|
Fiber.attr_accessor :async_task
|
|
21
22
|
|
|
22
23
|
module Async
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
# Represents a sequential unit of work, defined by a block, which is executed concurrently with other tasks. A task can be in one of the following states: `initialized`, `running`, `completed`, `failed`, or `cancelled`.
|
|
25
|
+
#
|
|
26
|
+
# ```mermaid
|
|
27
|
+
# stateDiagram-v2
|
|
28
|
+
# [*] --> Initialized
|
|
29
|
+
# Initialized --> Running : Run
|
|
30
|
+
#
|
|
31
|
+
# Running --> Completed : Return Value
|
|
32
|
+
# Running --> Failed : Exception
|
|
33
|
+
#
|
|
34
|
+
# Completed --> [*]
|
|
35
|
+
# Failed --> [*]
|
|
36
|
+
#
|
|
37
|
+
# Running --> Cancelled : Cancel
|
|
38
|
+
# Cancelled --> [*]
|
|
39
|
+
# Completed --> Cancelled : Cancel
|
|
40
|
+
# Failed --> Cancelled : Cancel
|
|
41
|
+
# Initialized --> Cancelled : Cancel
|
|
42
|
+
# ```
|
|
43
|
+
#
|
|
44
|
+
# @example Creating a task that sleeps for 1 second.
|
|
45
|
+
# require "async"
|
|
46
|
+
# Async do |task|
|
|
47
|
+
# sleep(1)
|
|
48
|
+
# end
|
|
49
|
+
#
|
|
34
50
|
# @public Since *Async v1*.
|
|
35
51
|
class Task < Node
|
|
36
52
|
# Raised when a child task is created within a task that has finished execution.
|
|
@@ -61,8 +77,6 @@ module Async
|
|
|
61
77
|
# @parameter reactor [Reactor] the reactor this task will run within.
|
|
62
78
|
# @parameter parent [Task] the parent task.
|
|
63
79
|
def initialize(parent = Task.current?, finished: nil, **options, &block)
|
|
64
|
-
super(parent, **options)
|
|
65
|
-
|
|
66
80
|
# These instance variables are critical to the state of the task.
|
|
67
81
|
# In the initialized state, the @block should be set, but the @fiber should be nil.
|
|
68
82
|
# In the running state, the @fiber should be set, and @block should be nil.
|
|
@@ -84,7 +98,10 @@ module Async
|
|
|
84
98
|
warn("finished: argument with non-false value is deprecated and will be removed.", uplevel: 1, category: :deprecated) if $VERBOSE
|
|
85
99
|
end
|
|
86
100
|
|
|
87
|
-
@
|
|
101
|
+
@defer_cancel = nil
|
|
102
|
+
|
|
103
|
+
# Call this after all state is initialized, as it may call `add_child` which will set the parent and make it visible to the scheduler.
|
|
104
|
+
super(parent, **options)
|
|
88
105
|
end
|
|
89
106
|
|
|
90
107
|
# @returns [Scheduler] The scheduler for this task.
|
|
@@ -166,8 +183,8 @@ module Async
|
|
|
166
183
|
@promise.failed?
|
|
167
184
|
end
|
|
168
185
|
|
|
169
|
-
# @returns [Boolean] Whether the task has been
|
|
170
|
-
def
|
|
186
|
+
# @returns [Boolean] Whether the task has been cancelled.
|
|
187
|
+
def cancelled?
|
|
171
188
|
@promise.cancelled?
|
|
172
189
|
end
|
|
173
190
|
|
|
@@ -181,11 +198,11 @@ module Async
|
|
|
181
198
|
self.completed?
|
|
182
199
|
end
|
|
183
200
|
|
|
184
|
-
# @attribute [Symbol] The status of the execution of the task, one of `:initialized`, `:running`, `:complete`, `:
|
|
201
|
+
# @attribute [Symbol] The status of the execution of the task, one of `:initialized`, `:running`, `:complete`, `:cancelled` or `:failed`.
|
|
185
202
|
def status
|
|
186
203
|
case @promise.resolved
|
|
187
204
|
when :cancelled
|
|
188
|
-
:
|
|
205
|
+
:cancelled
|
|
189
206
|
when :failed
|
|
190
207
|
:failed
|
|
191
208
|
when :completed
|
|
@@ -243,29 +260,58 @@ module Async
|
|
|
243
260
|
return task
|
|
244
261
|
end
|
|
245
262
|
|
|
246
|
-
# Retrieve the current result of the task. Will cause the caller to wait until result is available. If the task resulted in an unhandled error (derived from `StandardError`), this will be raised. If the task was
|
|
263
|
+
# Retrieve the current result of the task. Will cause the caller to wait until result is available. If the task resulted in an unhandled error (derived from `StandardError`), this will be raised. If the task was cancelled, this will return `nil`.
|
|
247
264
|
#
|
|
248
265
|
# Conceptually speaking, waiting on a task should return a result, and if it throws an exception, this is certainly an exceptional case that should represent a failure in your program, not an expected outcome. In other words, you should not design your programs to expect exceptions from `#wait` as a normal flow control, and prefer to catch known exceptions within the task itself and return a result that captures the intention of the failure, e.g. a `TimeoutError` might simply return `nil` or `false` to indicate that the operation did not generate a valid result (as a timeout was an expected outcome of the internal operation in this case).
|
|
249
266
|
#
|
|
267
|
+
# @parameter timeout [Numeric] The maximum number of seconds to wait for the result before raising a `TimeoutError`, if specified.
|
|
250
268
|
# @raises [RuntimeError] If the task's fiber is the current fiber.
|
|
251
269
|
# @returns [Object] The final expression/result of the task's block.
|
|
252
270
|
# @asynchronous This method is thread-safe.
|
|
253
|
-
def wait
|
|
271
|
+
def wait(...)
|
|
254
272
|
raise "Cannot wait on own fiber!" if Fiber.current.equal?(@fiber)
|
|
255
273
|
|
|
256
|
-
# Wait for the task to complete
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
274
|
+
# Wait for the task to complete:
|
|
275
|
+
@promise.wait(...)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# For compatibility with `Thread#join` and similar interfaces.
|
|
279
|
+
alias join wait
|
|
280
|
+
|
|
281
|
+
# Wait on all non-transient children to complete, recursively, then wait on the task itself, if it is not the current task.
|
|
282
|
+
#
|
|
283
|
+
# If any child task fails with an exception, that exception will be raised immediately, and remaining children may not be waited on.
|
|
284
|
+
#
|
|
285
|
+
# @example Waiting on all children.
|
|
286
|
+
# Async do |task|
|
|
287
|
+
# child = task.async do
|
|
288
|
+
# sleep(0.01)
|
|
289
|
+
# end
|
|
290
|
+
# task.wait_all # Will wait on the child task.
|
|
291
|
+
# end
|
|
292
|
+
#
|
|
293
|
+
# @raises [StandardError] If any child task failed with an exception, that exception will be raised.
|
|
294
|
+
# @returns [Object | Nil] The final expression/result of the task's block, or nil if called from within the task.
|
|
295
|
+
# @asynchronous This method is thread-safe.
|
|
296
|
+
def wait_all
|
|
297
|
+
@children&.each do |child|
|
|
298
|
+
# Skip transient tasks
|
|
299
|
+
next if child.transient?
|
|
300
|
+
|
|
301
|
+
child.wait_all
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Only wait on the task if we're not waiting on ourselves:
|
|
305
|
+
unless self.current?
|
|
306
|
+
return self.wait
|
|
262
307
|
end
|
|
263
308
|
end
|
|
264
309
|
|
|
265
310
|
# Access the result of the task without waiting. May be nil if the task is not completed. Does not raise exceptions.
|
|
266
311
|
def result
|
|
267
312
|
value = @promise.value
|
|
268
|
-
|
|
313
|
+
|
|
314
|
+
# For backward compatibility, return nil for cancelled tasks:
|
|
269
315
|
if @promise.cancelled?
|
|
270
316
|
nil
|
|
271
317
|
else
|
|
@@ -273,103 +319,115 @@ module Async
|
|
|
273
319
|
end
|
|
274
320
|
end
|
|
275
321
|
|
|
276
|
-
#
|
|
322
|
+
# Cancel the task and all of its children.
|
|
277
323
|
#
|
|
278
|
-
# If `later` is false, it means that `
|
|
324
|
+
# If `later` is false, it means that `cancel` has been invoked directly. When `later` is true, it means that `cancel` is invoked by `stop_children` or some other indirect mechanism. In that case, if we encounter the "current" fiber, we can't cancel it right away, as it's currently performing `#cancel`. Cancelling it immediately would interrupt the current cancel traversal, so we need to schedule the cancel to occur later.
|
|
279
325
|
#
|
|
280
|
-
# @parameter later [Boolean] Whether to
|
|
281
|
-
# @parameter cause [Exception] The cause of the
|
|
282
|
-
def
|
|
326
|
+
# @parameter later [Boolean] Whether to cancel the task later, or immediately.
|
|
327
|
+
# @parameter cause [Exception] The cause of the cancel operation.
|
|
328
|
+
def cancel(later = false, cause: $!)
|
|
283
329
|
# If no cause is given, we generate one from the current call stack:
|
|
284
330
|
unless cause
|
|
285
|
-
cause =
|
|
331
|
+
cause = Cancel::Cause.for("Cancelling task!")
|
|
286
332
|
end
|
|
287
333
|
|
|
288
|
-
if self.
|
|
289
|
-
# If the task is already
|
|
290
|
-
return
|
|
334
|
+
if self.cancelled?
|
|
335
|
+
# If the task is already cancelled, a `cancel` state transition re-enters the same state which is a no-op. However, we will also attempt to cancel any running children too. This can happen if the children did not cancel correctly the first time around. Doing this should probably be considered a bug, but it's better to be safe than sorry.
|
|
336
|
+
return cancelled!
|
|
291
337
|
end
|
|
292
338
|
|
|
293
|
-
# If the fiber is alive, we need to
|
|
339
|
+
# If the fiber is alive, we need to cancel it:
|
|
294
340
|
if @fiber&.alive?
|
|
295
341
|
# As the task is now exiting, we want to ensure the event loop continues to execute until the task finishes.
|
|
296
342
|
self.transient = false
|
|
297
343
|
|
|
298
|
-
# If we are deferring
|
|
299
|
-
if @
|
|
300
|
-
# Don't
|
|
301
|
-
@
|
|
344
|
+
# If we are deferring cancel...
|
|
345
|
+
if @defer_cancel == false
|
|
346
|
+
# Don't cancel now... but update the state so we know we need to cancel later.
|
|
347
|
+
@defer_cancel = cause
|
|
302
348
|
return false
|
|
303
349
|
end
|
|
304
350
|
|
|
305
351
|
if self.current?
|
|
306
|
-
# If the fiber is current, and later is `true`, we need to schedule the fiber to be
|
|
352
|
+
# If the fiber is current, and later is `true`, we need to schedule the fiber to be cancelled later, as it's currently invoking `cancel`:
|
|
307
353
|
if later
|
|
308
|
-
# If the fiber is the current fiber and we want to
|
|
309
|
-
Fiber.scheduler.push(
|
|
354
|
+
# If the fiber is the current fiber and we want to cancel it later, schedule it:
|
|
355
|
+
Fiber.scheduler.push(Cancel::Later.new(self, cause))
|
|
310
356
|
else
|
|
311
357
|
# Otherwise, raise the exception directly:
|
|
312
|
-
raise
|
|
358
|
+
raise Cancel, "Cancelling current task!", cause: cause
|
|
313
359
|
end
|
|
314
360
|
else
|
|
315
361
|
# If the fiber is not curent, we can raise the exception directly:
|
|
316
362
|
begin
|
|
317
|
-
# There is a chance that this will
|
|
318
|
-
Fiber.scheduler.raise(@fiber,
|
|
363
|
+
# There is a chance that this will cancel the fiber that originally called cancel. If that happens, the exception handling in `#cancelled` will rescue the exception and re-raise it later.
|
|
364
|
+
Fiber.scheduler.raise(@fiber, Cancel, cause: cause)
|
|
319
365
|
rescue FiberError
|
|
320
|
-
# In some cases, this can cause a FiberError (it might be resumed already), so we schedule it to be
|
|
321
|
-
Fiber.scheduler.push(
|
|
366
|
+
# In some cases, this can cause a FiberError (it might be resumed already), so we schedule it to be cancelled later:
|
|
367
|
+
Fiber.scheduler.push(Cancel::Later.new(self, cause))
|
|
322
368
|
end
|
|
323
369
|
end
|
|
324
370
|
else
|
|
325
|
-
# We are not running, but children might be, so transition directly into
|
|
326
|
-
|
|
371
|
+
# We are not running, but children might be, so transition directly into cancelled state:
|
|
372
|
+
cancel!
|
|
327
373
|
end
|
|
328
374
|
end
|
|
329
375
|
|
|
330
|
-
# Defer the handling of
|
|
376
|
+
# Defer the handling of cancel. During the execution of the given block, if a cancel is requested, it will be deferred until the block exits. This is useful for ensuring graceful shutdown of servers and other long-running tasks. You should wrap the response handling code in a defer_cancel block to ensure that the task is cancelled when the response is complete but not before.
|
|
331
377
|
#
|
|
332
|
-
# You can nest calls to
|
|
378
|
+
# You can nest calls to defer_cancel, but the cancel will only be deferred until the outermost block exits.
|
|
333
379
|
#
|
|
334
|
-
# If
|
|
380
|
+
# If cancel is invoked a second time, it will be immediately executed.
|
|
335
381
|
#
|
|
336
382
|
# @yields {} The block of code to execute.
|
|
337
383
|
# @public Since *Async v1*.
|
|
338
|
-
def
|
|
339
|
-
# Tri-state variable for controlling
|
|
340
|
-
# - nil:
|
|
341
|
-
# - false:
|
|
342
|
-
# - true:
|
|
343
|
-
if @
|
|
384
|
+
def defer_cancel
|
|
385
|
+
# Tri-state variable for controlling cancel:
|
|
386
|
+
# - nil: defer_cancel has not been called.
|
|
387
|
+
# - false: defer_cancel has been called and we are not cancelling.
|
|
388
|
+
# - true: defer_cancel has been called and we will cancel when exiting the block.
|
|
389
|
+
if @defer_cancel.nil?
|
|
344
390
|
begin
|
|
345
|
-
# If we are not deferring
|
|
346
|
-
@
|
|
391
|
+
# If we are not deferring cancel already, we can defer it now:
|
|
392
|
+
@defer_cancel = false
|
|
347
393
|
|
|
348
394
|
yield
|
|
349
|
-
rescue
|
|
350
|
-
# If we are exiting due to a
|
|
351
|
-
@
|
|
395
|
+
rescue Cancel
|
|
396
|
+
# If we are exiting due to a cancel, we shouldn't try to invoke cancel again:
|
|
397
|
+
@defer_cancel = nil
|
|
352
398
|
raise
|
|
353
399
|
ensure
|
|
354
|
-
|
|
400
|
+
defer_cancel = @defer_cancel
|
|
355
401
|
|
|
356
402
|
# We need to ensure the state is reset before we exit the block:
|
|
357
|
-
@
|
|
403
|
+
@defer_cancel = nil
|
|
358
404
|
|
|
359
|
-
# If we were asked to
|
|
360
|
-
if
|
|
361
|
-
raise
|
|
405
|
+
# If we were asked to cancel, we should do so now:
|
|
406
|
+
if defer_cancel
|
|
407
|
+
raise Cancel, "Cancelling current task (was deferred)!", cause: defer_cancel
|
|
362
408
|
end
|
|
363
409
|
end
|
|
364
410
|
else
|
|
365
|
-
# If we are deferring
|
|
411
|
+
# If we are deferring cancel already, entering it again is a no-op.
|
|
366
412
|
yield
|
|
367
413
|
end
|
|
368
414
|
end
|
|
369
415
|
|
|
370
|
-
#
|
|
416
|
+
# Backward compatibility alias for {#defer_cancel}.
|
|
417
|
+
# @deprecated Use {#defer_cancel} instead.
|
|
418
|
+
def defer_stop(&block)
|
|
419
|
+
defer_cancel(&block)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# @returns [Boolean] Whether cancel has been deferred.
|
|
423
|
+
def cancel_deferred?
|
|
424
|
+
!!@defer_cancel
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Backward compatibility alias for {#cancel_deferred?}.
|
|
428
|
+
# @deprecated Use {#cancel_deferred?} instead.
|
|
371
429
|
def stop_deferred?
|
|
372
|
-
|
|
430
|
+
cancel_deferred?
|
|
373
431
|
end
|
|
374
432
|
|
|
375
433
|
# Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
|
|
@@ -398,6 +456,9 @@ module Async
|
|
|
398
456
|
|
|
399
457
|
# Finish the current task, moving any children to the parent.
|
|
400
458
|
def finish!
|
|
459
|
+
# Break the cycle:
|
|
460
|
+
@fiber&.async_task = nil
|
|
461
|
+
|
|
401
462
|
# Don't hold references to the fiber or block after the task has finished:
|
|
402
463
|
@fiber = nil
|
|
403
464
|
@block = nil # If some how we went directly from initialized to finished.
|
|
@@ -409,49 +470,57 @@ module Async
|
|
|
409
470
|
# State transition into the completed state.
|
|
410
471
|
def completed!(result)
|
|
411
472
|
# Resolve the promise with the result:
|
|
412
|
-
@promise
|
|
473
|
+
@promise.resolve(result)
|
|
413
474
|
end
|
|
414
475
|
|
|
415
476
|
# State transition into the failed state.
|
|
416
477
|
def failed!(exception = false)
|
|
417
478
|
# Reject the promise with the exception:
|
|
418
|
-
@promise
|
|
479
|
+
@promise.reject(exception)
|
|
419
480
|
end
|
|
420
481
|
|
|
421
|
-
def
|
|
422
|
-
# Console.info(self, status:) {"Task #{self} was
|
|
482
|
+
def cancelled!
|
|
483
|
+
# Console.info(self, status:) {"Task #{self} was cancelled with #{@children&.size.inspect} children!"}
|
|
423
484
|
|
|
424
|
-
# Cancel the promise:
|
|
425
|
-
@promise
|
|
485
|
+
# Cancel the promise, specify nil here so that no exception is raised when waiting on the promise:
|
|
486
|
+
@promise.cancel(nil)
|
|
426
487
|
|
|
427
|
-
|
|
488
|
+
cancelled = false
|
|
428
489
|
|
|
429
490
|
begin
|
|
430
491
|
# We are not running, but children might be so we should stop them:
|
|
431
492
|
stop_children(true)
|
|
432
|
-
rescue
|
|
433
|
-
|
|
434
|
-
# If we are
|
|
493
|
+
rescue Cancel
|
|
494
|
+
cancelled = true
|
|
495
|
+
# If we are cancelling children, and one of them tries to cancel the current task, we should ignore it. We will be cancelled later.
|
|
435
496
|
retry
|
|
436
497
|
end
|
|
437
498
|
|
|
438
|
-
if
|
|
439
|
-
raise
|
|
499
|
+
if cancelled
|
|
500
|
+
raise Cancel, "Cancelling current task!"
|
|
440
501
|
end
|
|
441
502
|
end
|
|
442
503
|
|
|
443
|
-
def
|
|
444
|
-
|
|
504
|
+
def stopped!
|
|
505
|
+
cancelled!
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def cancel!
|
|
509
|
+
cancelled!
|
|
445
510
|
|
|
446
511
|
finish!
|
|
447
512
|
end
|
|
448
513
|
|
|
514
|
+
def stop!
|
|
515
|
+
cancel!
|
|
516
|
+
end
|
|
517
|
+
|
|
449
518
|
def schedule(&block)
|
|
450
519
|
@fiber = Fiber.new(annotation: self.annotation) do
|
|
451
520
|
begin
|
|
452
521
|
completed!(yield)
|
|
453
|
-
rescue
|
|
454
|
-
|
|
522
|
+
rescue Cancel
|
|
523
|
+
cancelled!
|
|
455
524
|
rescue StandardError => error
|
|
456
525
|
failed!(error)
|
|
457
526
|
rescue Exception => exception
|
data/lib/async/version.rb
CHANGED
data/lib/async.rb
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2017-
|
|
4
|
+
# Copyright, 2017-2026, by Samuel Williams.
|
|
5
5
|
# Copyright, 2020, by Salim Semaoune.
|
|
6
6
|
|
|
7
7
|
require_relative "async/version"
|
|
8
8
|
require_relative "async/reactor"
|
|
9
|
+
require_relative "async/loop"
|
|
9
10
|
|
|
10
11
|
require_relative "kernel/async"
|
|
11
12
|
require_relative "kernel/sync"
|
|
12
|
-
|
|
13
|
-
# Asynchronous programming framework.
|
|
14
|
-
module Async
|
|
15
|
-
end
|
|
13
|
+
require_relative "kernel/barrier"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "sync"
|
|
7
|
+
require_relative "../async/barrier"
|
|
8
|
+
require_relative "../async/idler"
|
|
9
|
+
|
|
10
|
+
module Kernel
|
|
11
|
+
# Create a barrier, yield it to the block, and then wait for all tasks to complete.
|
|
12
|
+
#
|
|
13
|
+
# If no scheduler is running, one will be created automatically for the duration of the block.
|
|
14
|
+
#
|
|
15
|
+
# By default, the barrier uses an `Async::Idler` to manage load, but this can be overridden by providing a different parent or `nil` to disable load management.
|
|
16
|
+
#
|
|
17
|
+
# @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks.
|
|
18
|
+
# @parameter **options [Hash] Additional options passed to {Kernel::Sync}.
|
|
19
|
+
# @public Since *Async v2.34*.
|
|
20
|
+
def Barrier(parent: Async::Idler.new, **options)
|
|
21
|
+
Sync(**options) do |task|
|
|
22
|
+
barrier = ::Async::Barrier.new(parent: parent)
|
|
23
|
+
|
|
24
|
+
yield barrier
|
|
25
|
+
|
|
26
|
+
barrier.wait
|
|
27
|
+
ensure
|
|
28
|
+
barrier&.stop
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/kernel/sync.rb
CHANGED
data/license.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MIT License
|
|
2
2
|
|
|
3
|
-
Copyright, 2017-
|
|
3
|
+
Copyright, 2017-2026, by Samuel Williams.
|
|
4
4
|
Copyright, 2017, by Kent Gruber.
|
|
5
5
|
Copyright, 2017, by Devin Christensen.
|
|
6
6
|
Copyright, 2018, by Sokolov Yura.
|
|
@@ -31,9 +31,11 @@ Copyright, 2025, by Jahfer Husain.
|
|
|
31
31
|
Copyright, 2025, by Mark Montroy.
|
|
32
32
|
Copyright, 2025, by Shigeru Nakajima.
|
|
33
33
|
Copyright, 2025, by Alan Wu.
|
|
34
|
-
Copyright, 2025, by Shopify Inc.
|
|
34
|
+
Copyright, 2025-2026, by Shopify Inc.
|
|
35
35
|
Copyright, 2025, by Josh Teeter.
|
|
36
36
|
Copyright, 2025, by Jatin Goyal.
|
|
37
|
+
Copyright, 2025, by Yuhi Sato.
|
|
38
|
+
Copyright, 2026, by Tavian Barnes.
|
|
37
39
|
|
|
38
40
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
39
41
|
of this software and associated documentation files (the "Software"), to deal
|
data/readme.md
CHANGED
|
@@ -35,63 +35,57 @@ Please see the [project documentation](https://socketry.github.io/async/) for mo
|
|
|
35
35
|
|
|
36
36
|
Please see the [project releases](https://socketry.github.io/async/releases/index) for all releases.
|
|
37
37
|
|
|
38
|
-
### v2.
|
|
38
|
+
### v2.39.0
|
|
39
39
|
|
|
40
|
-
-
|
|
40
|
+
- `Async::Barrier#wait` now returns the number of tasks that were waited for, or `nil` if there were no tasks to wait for. This provides better feedback about the operation, and allows you to know how many tasks were involved in the wait.
|
|
41
41
|
|
|
42
|
-
### v2.
|
|
42
|
+
### v2.38.1
|
|
43
43
|
|
|
44
|
-
-
|
|
44
|
+
- Fix `Barrier#async` when `parent.async` yields before the child block executes. Previously, `Barrier#wait` could return early and miss tracking the task entirely, because the task had not yet appended itself to the barrier's task list.
|
|
45
45
|
|
|
46
|
-
### v2.
|
|
46
|
+
### v2.38.0
|
|
47
47
|
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
- Add `closed?` method to `Async::PriorityQueue` for full queue interface compatibility.
|
|
51
|
-
- Support non-blocking operations using `timeout: 0` parameter.
|
|
48
|
+
- Rename `Task#stop` to `Task#cancel` for better clarity and consistency with common concurrency terminology. The old `stop` method is still available as an alias for backward compatibility, but it is recommended to use `cancel` going forward.
|
|
49
|
+
- Forward arguments from `Task#wait` -\> `Promise#wait`, so `task.wait(timeout: N)` is supported.
|
|
52
50
|
|
|
53
|
-
### v2.
|
|
51
|
+
### v2.37.0
|
|
54
52
|
|
|
55
|
-
|
|
53
|
+
- Introduce `Async::Loop` for robust, time-aligned loops.
|
|
54
|
+
- Add support for `Async::Promise#wait(timeout: N)`.
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
- Thread-safe `Async::Queue` and `Async::LimitedQueue`, implemented using `Thread::Queue` and `Thread::LimitedQueue` respectively.
|
|
59
|
-
- `Async::Variable` is deprecated in favor of `Async::Promise`.
|
|
60
|
-
- [Introduce `Async::Promise`](https://socketry.github.io/async/releases/index#introduce-async::promise)
|
|
61
|
-
- [Introduce `Async::PriorityQueue`](https://socketry.github.io/async/releases/index#introduce-async::priorityqueue)
|
|
56
|
+
### v2.36.0
|
|
62
57
|
|
|
63
|
-
|
|
58
|
+
- Introduce `Task#wait_all` which recursively waits for all children and self, excepting the current task.
|
|
59
|
+
- Introduce `Task#join` as an alias for `Task#wait` for compatibility with `Thread#join` and similar interfaces.
|
|
64
60
|
|
|
65
|
-
|
|
61
|
+
### v2.35.3
|
|
66
62
|
|
|
67
|
-
|
|
63
|
+
- `Async::Clock` now implements `#as_json` and `#to_json` for nicer log formatting.
|
|
68
64
|
|
|
69
|
-
|
|
65
|
+
### v2.35.2
|
|
70
66
|
|
|
71
|
-
|
|
67
|
+
- Improved handling of `Process.fork` on Ruby 4+.
|
|
68
|
+
- Improve `@promise` state handling in `Task#initialize`, preventing incomplete instances being visible to the scheduler.
|
|
72
69
|
|
|
73
|
-
|
|
70
|
+
### v2.35.1
|
|
74
71
|
|
|
75
|
-
|
|
72
|
+
- Fix incorrect handling of spurious wakeups in `Async::Promise#wait`, which could lead to premature (incorrect) resolution of the promise.
|
|
76
73
|
|
|
77
|
-
|
|
74
|
+
### v2.35.0
|
|
78
75
|
|
|
79
|
-
|
|
76
|
+
- `Process.fork` is now properly handled by the Async fiber scheduler, ensuring that the scheduler state is correctly reset in the child process after a fork. This prevents issues where the child process inherits the scheduler state from the parent, which could lead to unexpected behavior.
|
|
80
77
|
|
|
81
|
-
|
|
78
|
+
### v2.34.0
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
- Updated documentation and agent context.
|
|
80
|
+
- [`Kernel::Barrier` Convenience Interface](https://socketry.github.io/async/releases/index#kernel::barrier-convenience-interface)
|
|
86
81
|
|
|
87
82
|
## See Also
|
|
88
83
|
|
|
89
84
|
- [async-http](https://github.com/socketry/async-http) — Asynchronous HTTP client/server.
|
|
85
|
+
- [falcon](https://github.com/socketry/falcon) — A rack compatible server built on top of `async-http`.
|
|
90
86
|
- [async-websocket](https://github.com/socketry/async-websocket) — Asynchronous client and server websockets.
|
|
91
87
|
- [async-dns](https://github.com/socketry/async-dns) — Asynchronous DNS resolver and server.
|
|
92
|
-
- [
|
|
93
|
-
- [rubydns](https://github.com/ioquatix/rubydns) — An easy to use Ruby DNS server.
|
|
94
|
-
- [slack-ruby-bot](https://github.com/slack-ruby/slack-ruby-bot) — A client for making slack bots.
|
|
88
|
+
- [toolbox](https://github.com/socketry/toolbox) — GDB & LLDB extensions for debugging Ruby applications with Fibers.
|
|
95
89
|
|
|
96
90
|
## Contributing
|
|
97
91
|
|