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.
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-2025, by Samuel Williams.
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
- # Raised if a timeout occurs on a specific Fiber. Handled gracefully by `Task`.
24
- # @public Since *Async v1*.
25
- class TimeoutError < StandardError
26
- # Create a new timeout error.
27
- #
28
- # @parameter message [String] The error message.
29
- def initialize(message = "execution expired")
30
- super
31
- end
32
- end
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
- @defer_stop = nil
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 stopped.
170
- def stopped?
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`, `:stopped` or `:failed`.
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
- :stopped
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 stopped, this will return `nil`.
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 - 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
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
- # For backward compatibility, return nil for stopped tasks
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
- # Stop the task and all of its children.
322
+ # Cancel the task and all of its children.
277
323
  #
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.
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 stop the task later, or immediately.
281
- # @parameter cause [Exception] The cause of the stop operation.
282
- def stop(later = false, cause: $!)
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 = Stop::Cause.for("Stopping task!")
331
+ cause = Cancel::Cause.for("Cancelling task!")
286
332
  end
287
333
 
288
- if self.stopped?
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.
290
- return stopped!
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 stop it:
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 stop...
299
- if @defer_stop == false
300
- # Don't stop now... but update the state so we know we need to stop later.
301
- @defer_stop = cause
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 stopped later, as it's currently invoking `stop`:
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 stop it later, schedule it:
309
- Fiber.scheduler.push(Stop::Later.new(self, cause))
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 Stop, "Stopping current task!", cause: cause
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 stop the fiber that originally called stop. If that happens, the exception handling in `#stopped` will rescue the exception and re-raise it later.
318
- Fiber.scheduler.raise(@fiber, Stop, cause: cause)
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 stopped later:
321
- Fiber.scheduler.push(Stop::Later.new(self, cause))
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 stopped state:
326
- stop!
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 stop. During the execution of the given block, if a stop 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_stop block to ensure that the task is stopped when the response is complete but not before.
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 defer_stop, but the stop will only be deferred until the outermost block exits.
378
+ # You can nest calls to defer_cancel, but the cancel will only be deferred until the outermost block exits.
333
379
  #
334
- # If stop is invoked a second time, it will be immediately executed.
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 defer_stop
339
- # Tri-state variable for controlling stop:
340
- # - nil: defer_stop has not been called.
341
- # - false: defer_stop has been called and we are not stopping.
342
- # - true: defer_stop has been called and we will stop when exiting the block.
343
- if @defer_stop.nil?
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 stop already, we can defer it now:
346
- @defer_stop = false
391
+ # If we are not deferring cancel already, we can defer it now:
392
+ @defer_cancel = false
347
393
 
348
394
  yield
349
- rescue Stop
350
- # If we are exiting due to a stop, we shouldn't try to invoke stop again:
351
- @defer_stop = nil
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
- defer_stop = @defer_stop
400
+ defer_cancel = @defer_cancel
355
401
 
356
402
  # We need to ensure the state is reset before we exit the block:
357
- @defer_stop = nil
403
+ @defer_cancel = nil
358
404
 
359
- # If we were asked to stop, we should do so now:
360
- if defer_stop
361
- raise Stop, "Stopping current task (was deferred)!", cause: defer_stop
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 stop already, entering it again is a no-op.
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
- # @returns [Boolean] Whether stop has been deferred.
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
- !!@defer_stop
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&.resolve(result)
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&.reject(exception)
479
+ @promise.reject(exception)
419
480
  end
420
481
 
421
- def stopped!
422
- # Console.info(self, status:) {"Task #{self} was stopped with #{@children&.size.inspect} children!"}
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&.cancel
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
- stopped = false
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 Stop
433
- stopped = true
434
- # If we are stopping children, and one of them tries to stop the current task, we should ignore it. We will be stopped later.
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 stopped
439
- raise Stop, "Stopping current task!"
499
+ if cancelled
500
+ raise Cancel, "Cancelling current task!"
440
501
  end
441
502
  end
442
503
 
443
- def stop!
444
- stopped!
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 Stop
454
- stopped!
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
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2025, by Samuel Williams.
4
+ # Copyright, 2017-2026, by Samuel Williams.
5
5
 
6
+ # @namespace
6
7
  module Async
7
- VERSION = "2.32.0"
8
+ VERSION = "2.39.0"
8
9
  end
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-2024, by Samuel Williams.
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
@@ -20,7 +20,7 @@ module Kernel
20
20
  def Sync(annotation: nil, &block)
21
21
  if task = ::Async::Task.current?
22
22
  if annotation
23
- task.annotate(annotation) {yield task}
23
+ task.annotate(annotation){yield task}
24
24
  else
25
25
  yield task
26
26
  end
@@ -12,6 +12,6 @@ Traces::Provider(Async::Barrier) do
12
12
  "size" => self.size
13
13
  }
14
14
 
15
- Traces.trace("async.barrier.wait", attributes: attributes) {super}
15
+ Traces.trace("async.barrier.wait", attributes: attributes){super}
16
16
  end
17
17
  end
data/license.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2017-2025, by Samuel Williams.
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.32.0
38
+ ### v2.39.0
39
39
 
40
- - Introduce `Queue#waiting_count` and `PriorityQueue#waiting_count`. Generally for statistics/testing purposes only.
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.31.0
42
+ ### v2.38.1
43
43
 
44
- - Introduce `Async::Deadline` for precise timeout management in compound operations.
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.30.0
46
+ ### v2.38.0
47
47
 
48
- - Add timeout support to `Async::Queue#dequeue` and `Async::Queue#pop` methods.
49
- - Add timeout support to `Async::PriorityQueue#dequeue` and `Async::PriorityQueue#pop` methods.
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.29.0
51
+ ### v2.37.0
54
52
 
55
- This release introduces thread-safety as a core concept of Async. Many core classes now have thread-safe guarantees, allowing them to be used safely across multiple threads.
53
+ - Introduce `Async::Loop` for robust, time-aligned loops.
54
+ - Add support for `Async::Promise#wait(timeout: N)`.
56
55
 
57
- - Thread-safe `Async::Condition` and `Async::Notification`, implemented using `Thread::Queue`.
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
- ### v2.28.1
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
- - Fix race condition between `Async::Barrier#stop` and finish signalling.
61
+ ### v2.35.3
66
62
 
67
- ### v2.28.0
63
+ - `Async::Clock` now implements `#as_json` and `#to_json` for nicer log formatting.
68
64
 
69
- - Use `Traces.current_context` and `Traces.with_context` for better integration with OpenTelemetry.
65
+ ### v2.35.2
70
66
 
71
- ### v2.27.4
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
- - Suppress excessive warning in `Async::Scheduler#async`.
70
+ ### v2.35.1
74
71
 
75
- ### v2.27.3
72
+ - Fix incorrect handling of spurious wakeups in `Async::Promise#wait`, which could lead to premature (incorrect) resolution of the promise.
76
73
 
77
- - Ensure trace attributes are strings, fixes integration with OpenTelemetry.
74
+ ### v2.35.0
78
75
 
79
- ### v2.27.2
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
- - Fix `context/index.yaml` schema.
78
+ ### v2.34.0
82
79
 
83
- ### v2.27.1
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
- - [falcon](https://github.com/socketry/falcon) — A rack compatible server built on top of `async-http`.
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