async 2.36.0 → 2.38.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b514097c721290749f38fed73721c5cb6d17af2a8af189f5a8e537f7b30c14d3
4
- data.tar.gz: 3eba30b0a03bdb9f9da1d1e4eeb6b11ec3224c0f3d02f27cc07ac1e5e7d7a41e
3
+ metadata.gz: 847417a49140235dad0d6e2b063dd6a3465247cb7b3000708d58bd01adbcffb1
4
+ data.tar.gz: f79a5b29cd8e10bdfe7f10ba24f0fb850bb0856e8612dffcecdd7882c133bc61
5
5
  SHA512:
6
- metadata.gz: 9ea8d371279ec9bf8feeaa85895f25733247c8ecb059d599f34e8ecc85b4b124c05dee5421414d28c24316d7c4e69bdff622c31b182e9f883456836a6aab17ed
7
- data.tar.gz: 55ddefa4f011c2dccf235043cad5ad334418c394ced59a960b993d18ead0850f7d60875a3edbdc55418d3a9d3045cef1d607170fea0ae484f4dc62bc13f15ca2
6
+ metadata.gz: c0b69a0ff96474b52956268018bf8a08aa147aff8c0c80c60185cd070f7d3d36360f3b9f2f8ae1d082a0ee8d4fc172621323bd1e99de5a8ea946f7263761e120
7
+ data.tar.gz: 80c5f6e6e946b3675ad55cc40ae30ff860aeb08dd732a5d87e2350a4c6ac48d6619c434900e39f9012a76d398930debb56c8d183ef9e139a62138b060a0b24bc
checksums.yaml.gz.sig CHANGED
Binary file
data/context/scheduler.md CHANGED
@@ -38,7 +38,7 @@ Async do |task|
38
38
  task.print_hierarchy($stderr)
39
39
 
40
40
  # Kill the subtask
41
- subtask.stop
41
+ subtask.cancel
42
42
  end
43
43
  ~~~
44
44
 
@@ -69,9 +69,9 @@ end
69
69
 
70
70
  You can use this approach to embed the reactor in another event loop. For some integrations, you may want to specify the maximum time to wait to {ruby Async::Scheduler#run_once}.
71
71
 
72
- ### Stopping a Scheduler
72
+ ### Cancelling a Scheduler
73
73
 
74
- {ruby Async::Scheduler#stop} will stop the current scheduler and all children tasks.
74
+ {ruby Async::Scheduler#cancel} will cancel the current scheduler and all children tasks.
75
75
 
76
76
  ### Fiber Scheduler Integration
77
77
 
data/context/tasks.md CHANGED
@@ -4,7 +4,7 @@ This guide explains how asynchronous tasks work and how to use them.
4
4
 
5
5
  ## Overview
6
6
 
7
- Tasks are the smallest unit of sequential code execution in {ruby Async}. Tasks can create other tasks, and Async tracks the parent-child relationship between tasks. When a parent task is stopped, it will also stop all its children tasks. The reactor always starts with one root task.
7
+ Tasks are the smallest unit of sequential code execution in {ruby Async}. Tasks can create other tasks, and Async tracks the parent-child relationship between tasks. When a parent task is cancelled, it will also cancel all its children tasks. The reactor always starts with one root task.
8
8
 
9
9
  ```mermaid
10
10
  graph LR
@@ -23,11 +23,11 @@ graph LR
23
23
 
24
24
  A fiber is a lightweight unit of execution that can be suspended and resumed at specific points. After a fiber is suspended, it can be resumed later at the same point with the same execution state. Because only one fiber can execute at a time, they are often referred to as a mechanism for cooperative concurrency.
25
25
 
26
- A task provides extra functionality on top of fibers. A task behaves like a promise: it either succeeds with a value or fails with an exception. Tasks keep track of their parent-child relationships, and when a parent task is stopped, it will also stop all its children tasks. This makes it easier to create complex programs with many concurrent tasks.
26
+ A task provides extra functionality on top of fibers. A task behaves like a promise: it either succeeds with a value or fails with an exception. Tasks keep track of their parent-child relationships, and when a parent task is cancelled, it will also cancel all its children tasks. This makes it easier to create complex programs with many concurrent tasks.
27
27
 
28
28
  ### Why does Async manipulate tasks and not fibers?
29
29
 
30
- The {ruby Async::Scheduler} actually works directly with fibers for most operations and isn't aware of tasks. However, the reactor does maintain a tree of tasks for the purpose of managing task and reactor life-cycle. For example, stopping a parent task will stop all its children tasks, and the reactor will exit when all tasks are finished.
30
+ The {ruby Async::Scheduler} actually works directly with fibers for most operations and isn't aware of tasks. However, the reactor does maintain a tree of tasks for the purpose of managing task and reactor life-cycle. For example, cancelling a parent task will cancel all its children tasks, and the reactor will exit when all tasks are finished.
31
31
 
32
32
  ## Task Lifecycle
33
33
 
@@ -40,20 +40,20 @@ stateDiagram-v2
40
40
 
41
41
  running --> failed : unhandled StandardError-derived exception
42
42
  running --> complete : user code finished
43
- running --> stopped : stop
43
+ running --> cancelled : cancel
44
44
 
45
- initialized --> stopped : stop
45
+ initialized --> cancelled : cancel
46
46
 
47
47
  failed --> [*]
48
48
  complete --> [*]
49
- stopped --> [*]
49
+ cancelled --> [*]
50
50
  ```
51
51
 
52
- Tasks are created in the `initialized` state, and are run by the reactor. During the execution, a task can either `complete` successfully, become `failed` with an unhandled `StandardError`-derived exception, or be explicitly `stopped`. In all of these cases, you can wait for a task to complete by using {ruby Async::Task#wait}.
52
+ Tasks are created in the `initialized` state, and are run by the reactor. During the execution, a task can either `complete` successfully, become `failed` with an unhandled `StandardError`-derived exception, or be explicitly `cancelled`. In all of these cases, you can wait for a task to complete by using {ruby Async::Task#wait}.
53
53
 
54
54
  1. In the case the task successfully completed, the result will be whatever value was generated by the last expression in the task.
55
55
  2. In the case the task failed with an unhandled `StandardError`-derived exception, waiting on the task will re-raise the exception.
56
- 3. In the case the task was stopped, the result will be `nil`.
56
+ 3. In the case the task was cancelled, the result will be `nil`.
57
57
 
58
58
  ## Starting A Task
59
59
 
@@ -175,8 +175,8 @@ Async do
175
175
  break if done.size >= 2
176
176
  end
177
177
  ensure
178
- # The remainder of the tasks will be stopped:
179
- barrier.stop
178
+ # The remainder of the tasks will be cancelled:
179
+ barrier.cancel
180
180
  end
181
181
  end
182
182
  ```
@@ -199,18 +199,18 @@ begin
199
199
  # Wait until all jobs are done:
200
200
  barrier.wait
201
201
  ensure
202
- # Stop any remaining jobs:
203
- barrier.stop
202
+ # Cancel any remaining jobs:
203
+ barrier.cancel
204
204
  end
205
205
  ~~~
206
206
 
207
- ## Stopping a Task
207
+ ## Cancelling a Task
208
208
 
209
209
  When a task completes execution, it will enter the `complete` state (or the `failed` state if it raises an unhandled exception).
210
210
 
211
- There are various situations where you may want to stop a task ({ruby Async::Task#stop}) before it completes. The most common case is shutting down a server. A more complex example is this: you may fan out multiple (10s, 100s) of requests, wait for a subset to complete (e.g. the first 5 or all those that complete within a given deadline), and then stop (terminate/cancel) the remaining operations.
211
+ There are various situations where you may want to cancel a task ({ruby Async::Task#cancel}) before it completes. The most common case is shutting down a server. A more complex example is this: you may fan out multiple (10s, 100s) of requests, wait for a subset to complete (e.g. the first 5 or all those that complete within a given deadline), and then cancel the remaining operations.
212
212
 
213
- Using the above program as an example, let's stop all the tasks just after the first one completes.
213
+ Using the above program as an example, let's cancel all the tasks just after the first one completes.
214
214
 
215
215
  ```ruby
216
216
  Async do
@@ -221,14 +221,14 @@ Async do
221
221
  end
222
222
  end
223
223
 
224
- # Stop all the above tasks:
225
- tasks.each(&:stop)
224
+ # Cancel all the above tasks:
225
+ tasks.each(&:cancel)
226
226
  end
227
227
  ```
228
228
 
229
- ### Stopping all Tasks held in a Barrier
229
+ ### Cancelling all Tasks held in a Barrier
230
230
 
231
- To stop (terminate/cancel) all the tasks held in a barrier:
231
+ To cancel all the tasks held in a barrier:
232
232
 
233
233
  ```ruby
234
234
  barrier = Async::Barrier.new
@@ -241,11 +241,11 @@ Async do
241
241
  end
242
242
  end
243
243
 
244
- barrier.stop
244
+ barrier.cancel
245
245
  end
246
246
  ```
247
247
 
248
- Unless your tasks all rescue and suppresses `StandardError`-derived exceptions, be sure to call ({ruby Async::Barrier#stop}) to stop the remaining tasks:
248
+ Unless your tasks all rescue and suppresses `StandardError`-derived exceptions, be sure to call ({ruby Async::Barrier#cancel}) to cancel the remaining tasks:
249
249
 
250
250
  ```ruby
251
251
  barrier = Async::Barrier.new
@@ -261,7 +261,7 @@ Async do
261
261
  begin
262
262
  barrier.wait
263
263
  ensure
264
- barrier.stop
264
+ barrier.cancel
265
265
  end
266
266
  end
267
267
  ```
@@ -273,10 +273,10 @@ In order to ensure your resources are cleaned up correctly, make sure you wrap r
273
273
  ~~~ ruby
274
274
  Async do
275
275
  begin
276
- socket = connect(remote_address) # May raise Async::Stop
276
+ socket = connect(remote_address) # May raise Async::Cancel
277
277
 
278
- socket.write(...) # May raise Async::Stop
279
- socket.read(...) # May raise Async::Stop
278
+ socket.write(...) # May raise Async::Cancel
279
+ socket.read(...) # May raise Async::Cancel
280
280
  ensure
281
281
  socket.close if socket
282
282
  end
@@ -398,9 +398,9 @@ end
398
398
 
399
399
  Transient tasks are similar to normal tasks, except for the following differences:
400
400
 
401
- 1. They are not considered by {ruby Async::Task#finished?}, so they will not keep the reactor alive. Instead, they are stopped (with a {ruby Async::Stop} exception) when all other (non-transient) tasks are finished.
401
+ 1. They are not considered by {ruby Async::Task#finished?}, so they will not keep the reactor alive. Instead, they are cancelled (with a {ruby Async::Cancel} exception) when all other (non-transient) tasks are finished.
402
402
  2. As soon as a parent task is finished, any transient child tasks will be moved up to be children of the parent's parent. This ensures that they never keep a sub-tree alive.
403
- 3. Similarly, if you `stop` a task, any transient child tasks will be moved up the tree as above rather than being stopped.
403
+ 3. Similarly, if you `cancel` a task, any transient child tasks will be moved up the tree as above rather than being cancelled.
404
404
 
405
405
  The purpose of transient tasks is when a task is an implementation detail of an object or instance, rather than a concurrency process. Some examples of transient tasks:
406
406
 
@@ -439,7 +439,7 @@ class TimeStringCache
439
439
  sleep(1)
440
440
  end
441
441
  ensure
442
- # When the reactor terminates all tasks, `Async::Stop` will be raised from `sleep` and this code will be invoked. By clearing `@refresh`, we ensure that the task will be recreated if needed again:
442
+ # When the reactor terminates all tasks, `Async::Cancel` will be raised from `sleep` and this code will be invoked. By clearing `@refresh`, we ensure that the task will be recreated if needed again:
443
443
  @refresh = nil
444
444
  end
445
445
  end
data/lib/async/barrier.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2025, by Samuel Williams.
4
+ # Copyright, 2019-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "list"
7
7
  require_relative "task"
@@ -88,14 +88,20 @@ module Async
88
88
  end
89
89
  end
90
90
 
91
- # Stop all tasks held by the barrier.
91
+ # Cancel all tasks held by the barrier.
92
92
  # @asynchronous May wait for tasks to finish executing.
93
- def stop
93
+ def cancel
94
94
  @tasks.each do |waiting|
95
- waiting.task.stop
95
+ waiting.task.cancel
96
96
  end
97
97
 
98
98
  @finished.close
99
99
  end
100
+
101
+ # Backward compatibility alias for {#cancel}.
102
+ # @deprecated Use {#cancel} instead.
103
+ def stop
104
+ cancel
105
+ end
100
106
  end
101
107
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ module Async
7
+ # Raised when a task is explicitly cancelled.
8
+ class Cancel < Exception
9
+ # Represents the source of the cancel operation.
10
+ class Cause < Exception
11
+ if RUBY_VERSION >= "3.4"
12
+ # @returns [Array(Thread::Backtrace::Location)] The backtrace of the caller.
13
+ def self.backtrace
14
+ caller_locations(2..-1)
15
+ end
16
+ else
17
+ # @returns [Array(String)] The backtrace of the caller.
18
+ def self.backtrace
19
+ caller(2..-1)
20
+ end
21
+ end
22
+
23
+ # Create a new cause of the cancel operation, with the given message.
24
+ #
25
+ # @parameter message [String] The error message.
26
+ # @returns [Cause] The cause of the cancel operation.
27
+ def self.for(message = "Task was cancelled!")
28
+ instance = self.new(message)
29
+ instance.set_backtrace(self.backtrace)
30
+ return instance
31
+ end
32
+ end
33
+
34
+ if RUBY_VERSION < "3.5"
35
+ # Create a new cancel operation.
36
+ #
37
+ # This is a compatibility method for Ruby versions before 3.5 where cause is not propagated correctly when using {Fiber#raise}
38
+ #
39
+ # @parameter message [String | Hash] The error message or a hash containing the cause.
40
+ def initialize(message = "Task was cancelled")
41
+
42
+ if message.is_a?(Hash)
43
+ @cause = message[:cause]
44
+ message = "Task was cancelled"
45
+ end
46
+
47
+ super(message)
48
+ end
49
+
50
+ # @returns [Exception] The cause of the cancel operation.
51
+ #
52
+ # This is a compatibility method for Ruby versions before 3.5 where cause is not propagated correctly when using {Fiber#raise}, we explicitly capture the cause here.
53
+ def cause
54
+ super || @cause
55
+ end
56
+ end
57
+
58
+ # Used to defer cancelling the current task until later.
59
+ class Later
60
+ # Create a new cancel later operation.
61
+ #
62
+ # @parameter task [Task] The task to cancel later.
63
+ # @parameter cause [Exception] The cause of the cancel operation.
64
+ def initialize(task, cause = nil)
65
+ @task = task
66
+ @cause = cause
67
+ end
68
+
69
+ # @returns [Boolean] Whether the task is alive.
70
+ def alive?
71
+ true
72
+ end
73
+
74
+ # Transfer control to the operation - this will cancel the task.
75
+ def transfer
76
+ @task.cancel(false, cause: @cause)
77
+ end
78
+ end
79
+ end
80
+ end
data/lib/async/clock.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2025, by Samuel Williams.
4
+ # Copyright, 2018-2026, by Samuel Williams.
5
+ # Copyright, 2026, by Shopify Inc.
5
6
 
6
7
  module Async
7
8
  # A convenient wrapper around the internal monotonic clock.
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ module Async
7
+ # Raised if a timeout occurs on a specific Fiber. Handled gracefully by `Task`.
8
+ # @public Since *Async v1*.
9
+ class TimeoutError < StandardError
10
+ # Create a new timeout error.
11
+ #
12
+ # @parameter message [String] The error message.
13
+ def initialize(message = "execution expired")
14
+ super
15
+ end
16
+ end
17
+ end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2025, by Shopify Inc.
5
- # Copyright, 2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Shopify Inc.
5
+ # Copyright, 2025-2026, by Samuel Williams.
6
6
 
7
7
  module Async
8
8
  # Private module that hooks into Process._fork to handle fork events.
data/lib/async/loop.rb ADDED
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "console"
7
+
8
+ module Async
9
+ # @namespace
10
+ module Loop
11
+ # Execute a block repeatedly at quantized (time-aligned) intervals.
12
+ #
13
+ # The alignment is computed modulo the current clock time in seconds. For example, with
14
+ # `interval: 60`, executions will occur at 00:00, 01:00, 02:00, etc., regardless of when
15
+ # the loop is started. With `interval: 300` (5 minutes), executions align to 00:00, 00:05,
16
+ # 00:10, etc.
17
+ #
18
+ # This is particularly useful for tasks that should run at predictable wall-clock times,
19
+ # such as metrics collection, periodic cleanup, or scheduled jobs that need to align
20
+ # across multiple processes.
21
+ #
22
+ # If an error occurs during block execution, it is logged and the loop continues.
23
+ #
24
+ # @example Run every minute at :00 seconds:
25
+ # Async::Loop.quantized(interval: 60) do
26
+ # puts "Current time: #{Time.now}"
27
+ # end
28
+ #
29
+ # @example Run every 5 minutes aligned to the hour:
30
+ # Async::Loop.quantized(interval: 300) do
31
+ # collect_metrics
32
+ # end
33
+ #
34
+ # @parameter interval [Numeric] The interval in seconds. Executions will align to multiples of this interval based on the current time.
35
+ # @yields The block to execute at each interval.
36
+ #
37
+ # @public Since *Async v2.37*.
38
+ def self.quantized(interval: 60, &block)
39
+ while true
40
+ # Compute the wait time to the next interval:
41
+ wait = interval - (Time.now.to_f % interval)
42
+ if wait.positive?
43
+ # Sleep until the next interval boundary:
44
+ sleep(wait)
45
+ end
46
+
47
+ begin
48
+ yield
49
+ rescue => error
50
+ Console.error(self, "Loop error:", error)
51
+ end
52
+ end
53
+ end
54
+
55
+ # Execute a block repeatedly with a fixed delay between executions.
56
+ #
57
+ # Unlike {quantized}, this method waits for the specified interval *after* each execution
58
+ # completes. This means the actual time between the start of successive executions will be
59
+ # `interval + execution_time`.
60
+ #
61
+ # If an error occurs during block execution, it is logged and the loop continues.
62
+ #
63
+ # @example Run every 5 seconds (plus execution time):
64
+ # Async::Loop.periodic(interval: 5) do
65
+ # process_queue
66
+ # end
67
+ #
68
+ # @parameter interval [Numeric] The delay in seconds between executions.
69
+ # @yields The block to execute periodically.
70
+ #
71
+ # @public Since *Async v2.37*.
72
+ def self.periodic(interval: 60, &block)
73
+ while true
74
+ begin
75
+ yield
76
+ rescue => error
77
+ Console.error(self, "Loop error:", error)
78
+ end
79
+
80
+ sleep(interval)
81
+ end
82
+ end
83
+ end
84
+ end
data/lib/async/node.rb CHANGED
@@ -1,10 +1,10 @@
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, 2022, by Shannon Skipper.
7
- # Copyright, 2025, by Shopify Inc.
7
+ # Copyright, 2025-2026, by Shopify Inc.
8
8
 
9
9
  require "fiber/annotation"
10
10
 
@@ -280,18 +280,24 @@ module Async
280
280
  return @children.nil?
281
281
  end
282
282
 
283
- # Attempt to stop the current node immediately, including all non-transient children. Invokes {#stop_children} to stop all children.
283
+ # Attempt to cancel the current node immediately, including all non-transient children. Invokes {#stop_children} to cancel all children.
284
284
  #
285
- # @parameter later [Boolean] Whether to defer stopping until some point in the future.
286
- def stop(later = false)
285
+ # @parameter later [Boolean] Whether to defer cancelling until some point in the future.
286
+ def cancel(later = false)
287
287
  # The implementation of this method may defer calling `stop_children`.
288
288
  stop_children(later)
289
289
  end
290
290
 
291
+ # Backward compatibility alias for {#cancel}.
292
+ # @deprecated Use {#cancel} instead.
293
+ def stop(...)
294
+ cancel(...)
295
+ end
296
+
291
297
  # Attempt to stop all non-transient children.
292
298
  private def stop_children(later = false)
293
299
  @children&.each do |child|
294
- child.stop(later) unless child.transient?
300
+ child.cancel(later) unless child.transient?
295
301
  end
296
302
  end
297
303
 
@@ -301,11 +307,17 @@ module Async
301
307
  nil
302
308
  end
303
309
 
304
- # Whether the node has been stopped.
305
- def stopped?
310
+ # Whether the node has been cancelled.
311
+ def cancelled?
306
312
  @children.nil?
307
313
  end
308
314
 
315
+ # Backward compatibility alias for {#cancelled?}.
316
+ # @deprecated Use {#cancelled?} instead.
317
+ def stopped?
318
+ cancelled?
319
+ end
320
+
309
321
  # Print the hierarchy of the task tree from the given node.
310
322
  #
311
323
  # @parameter out [IO] The output stream to write to.
data/lib/async/promise.rb CHANGED
@@ -4,6 +4,10 @@
4
4
  # Copyright, 2025, by Shopify Inc.
5
5
  # Copyright, 2025-2026, by Samuel Williams.
6
6
 
7
+ require_relative "error"
8
+ require_relative "deadline"
9
+ require_relative "cancel"
10
+
7
11
  module Async
8
12
  # A promise represents a value that will be available in the future.
9
13
  # Unlike Condition, once resolved (or rejected), all future waits return immediately
@@ -74,33 +78,92 @@ module Async
74
78
  @mutex.synchronize{@resolved ? @value : nil}
75
79
  end
76
80
 
81
+ # Wait indefinitely for the promise to be resolved.
82
+ private def wait_indefinitely
83
+ until @resolved
84
+ @condition.wait(@mutex)
85
+ end
86
+ end
87
+
88
+ # Wait for the promise to be resolved, respecting the deadline timeout.
89
+ # @parameter timeout [Numeric] The timeout duration.
90
+ # @returns [Boolean] True if resolved, false if timeout expires.
91
+ private def wait_with_timeout(timeout)
92
+ # Create deadline for timeout tracking:
93
+ deadline = Deadline.start(timeout)
94
+
95
+ # Handle immediate timeout (non-blocking):
96
+ if deadline == Deadline::Zero && !@resolved
97
+ return false
98
+ end
99
+
100
+ # Wait with deadline tracking:
101
+ until @resolved
102
+ # Get remaining time for this wait iteration:
103
+ remaining = deadline.remaining
104
+
105
+ # Check if deadline has expired before waiting:
106
+ if remaining <= 0
107
+ return false
108
+ end
109
+
110
+ @condition.wait(@mutex, remaining)
111
+ end
112
+
113
+ return true
114
+ end
115
+
116
+ # Wait for the promise to be resolved (without raising exceptions).
117
+ #
118
+ # If already resolved, returns immediately. Otherwise, waits until resolution or timeout.
119
+ #
120
+ # @parameter timeout [Numeric | Nil] Maximum time to wait. If nil, waits indefinitely. If 0, returns immediately if not resolved.
121
+ # @returns [Boolean] True if the promise is resolved, false if timeout expires
122
+ def wait?(timeout: nil)
123
+ unless @resolved
124
+ @mutex.synchronize do
125
+ # Increment waiting count:
126
+ @waiting += 1
127
+
128
+ begin
129
+ # Wait for resolution if not already resolved:
130
+ unless @resolved
131
+ if timeout.nil?
132
+ wait_indefinitely
133
+ else
134
+ unless wait_with_timeout(timeout)
135
+ # We don't want to race on @resolved after exiting the mutex:
136
+ return nil
137
+ end
138
+ end
139
+ end
140
+ ensure
141
+ # Decrement waiting count when done:
142
+ @waiting -= 1
143
+ end
144
+ end
145
+ end
146
+
147
+ return @resolved
148
+ end
149
+
77
150
  # Wait for the promise to be resolved and return the value.
151
+ #
78
152
  # If already resolved, returns immediately. If rejected, raises the stored exception.
79
153
  #
80
154
  # @returns [Object] The resolved value.
81
155
  # @raises [Exception] The rejected or cancelled exception.
82
- def wait
83
- @mutex.synchronize do
84
- # Increment waiting count:
85
- @waiting += 1
86
-
87
- begin
88
- # Wait for resolution if not already resolved:
89
- until @resolved
90
- @condition.wait(@mutex)
91
- end
92
-
93
- # Return value or raise exception based on resolution type:
94
- if @resolved == :completed
95
- return @value
96
- else
97
- # Both :failed and :cancelled store exceptions in @value
98
- raise @value
99
- end
100
- ensure
101
- # Decrement waiting count when done:
102
- @waiting -= 1
103
- end
156
+ # @raises [Async::TimeoutError] If timeout expires before the promise is resolved.
157
+ def wait(...)
158
+ resolved = wait?(...)
159
+
160
+ if resolved.nil?
161
+ raise TimeoutError, "Timeout while waiting for promise!"
162
+ elsif resolved == :completed
163
+ return @value
164
+ elsif @value
165
+ # If we aren't completed, we should have an exception or cancel reason stored:
166
+ raise @value
104
167
  end
105
168
  end
106
169
 
@@ -113,8 +176,8 @@ module Async
113
176
  @mutex.synchronize do
114
177
  return if @resolved
115
178
 
116
- @value = value
117
179
  @resolved = :completed
180
+ @value = value
118
181
 
119
182
  # Wake up all waiting fibers:
120
183
  @condition.broadcast
@@ -132,8 +195,8 @@ module Async
132
195
  @mutex.synchronize do
133
196
  return if @resolved
134
197
 
135
- @value = exception
136
198
  @resolved = :failed
199
+ @value = exception
137
200
 
138
201
  # Wake up all waiting fibers:
139
202
  @condition.broadcast
@@ -142,20 +205,16 @@ module Async
142
205
  return nil
143
206
  end
144
207
 
145
- # Exception used to indicate cancellation.
146
- class Cancel < Exception
147
- end
148
-
149
208
  # Cancel the promise, indicating cancellation.
150
209
  # All current and future waiters will receive nil.
151
210
  # Can only be called on pending promises - no-op if already resolved.
152
- def cancel(exception = Cancel.new("Promise was cancelled!"))
211
+ def cancel(exception = Cancel.new("Promise cancelled!"))
153
212
  @mutex.synchronize do
154
213
  # No-op if already in any final state
155
214
  return if @resolved
156
215
 
157
- @value = exception
158
216
  @resolved = :cancelled
217
+ @value = exception
159
218
 
160
219
  # Wake up all waiting fibers:
161
220
  @condition.broadcast
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2025, by Samuel Williams.
4
+ # Copyright, 2020-2026, by Samuel Williams.
5
5
  # Copyright, 2020, by Jun Jiang.
6
6
  # Copyright, 2021, by Julien Portalier.
7
- # Copyright, 2025, by Shopify Inc.
7
+ # Copyright, 2025-2026, by Shopify Inc.
8
8
 
9
9
  require_relative "clock"
10
10
  require_relative "task"
@@ -179,7 +179,7 @@ module Async
179
179
 
180
180
  # @returns [String] A description of the scheduler.
181
181
  def to_s
182
- "\#<#{self.description} #{@children&.size || 0} children (#{stopped? ? 'stopped' : 'running'})>"
182
+ "\#<#{self.description} #{@children&.size || 0} children (#{cancelled? ? 'cancelled' : 'running'})>"
183
183
  end
184
184
 
185
185
  # Interrupt the event loop and cause it to exit.
@@ -511,15 +511,20 @@ module Async
511
511
  return false
512
512
  end
513
513
 
514
- # Stop all children, including transient children.
514
+ # Cancel all children, including transient children.
515
515
  #
516
516
  # @public Since *Async v1*.
517
- def stop
517
+ def cancel
518
518
  @children&.each do |child|
519
- child.stop
519
+ child.cancel
520
520
  end
521
521
  end
522
522
 
523
+ # Backward compatibility alias for cancel.
524
+ def stop
525
+ cancel
526
+ end
527
+
523
528
  private def run_loop(&block)
524
529
  interrupt = nil
525
530
 
data/lib/async/stop.rb CHANGED
@@ -1,82 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Samuel Williams.
5
5
 
6
- require "fiber"
7
- require "console"
6
+ require_relative "cancel"
8
7
 
9
8
  module Async
10
- # Raised when a task is explicitly stopped.
11
- class Stop < Exception
12
- # Represents the source of the stop operation.
13
- class Cause < Exception
14
- if RUBY_VERSION >= "3.4"
15
- # @returns [Array(Thread::Backtrace::Location)] The backtrace of the caller.
16
- def self.backtrace
17
- caller_locations(2..-1)
18
- end
19
- else
20
- # @returns [Array(String)] The backtrace of the caller.
21
- def self.backtrace
22
- caller(2..-1)
23
- end
24
- end
25
-
26
- # Create a new cause of the stop operation, with the given message.
27
- #
28
- # @parameter message [String] The error message.
29
- # @returns [Cause] The cause of the stop operation.
30
- def self.for(message = "Task was stopped")
31
- instance = self.new(message)
32
- instance.set_backtrace(self.backtrace)
33
- return instance
34
- end
35
- end
36
-
37
- if RUBY_VERSION < "3.5"
38
- # Create a new stop operation.
39
- #
40
- # This is a compatibility method for Ruby versions before 3.5 where cause is not propagated correctly when using {Fiber#raise}
41
- #
42
- # @parameter message [String | Hash] The error message or a hash containing the cause.
43
- def initialize(message = "Task was stopped")
44
- if message.is_a?(Hash)
45
- @cause = message[:cause]
46
- message = "Task was stopped"
47
- end
48
-
49
- super(message)
50
- end
51
-
52
- # @returns [Exception] The cause of the stop operation.
53
- #
54
- # This is a compatibility method for Ruby versions before 3.5 where cause is not propagated correctly when using {Fiber#raise}, we explicitly capture the cause here.
55
- def cause
56
- super || @cause
57
- end
58
- end
59
-
60
- # Used to defer stopping the current task until later.
61
- class Later
62
- # Create a new stop later operation.
63
- #
64
- # @parameter task [Task] The task to stop later.
65
- # @parameter cause [Exception] The cause of the stop operation.
66
- def initialize(task, cause = nil)
67
- @task = task
68
- @cause = cause
69
- end
70
-
71
- # @returns [Boolean] Whether the task is alive.
72
- def alive?
73
- true
74
- end
75
-
76
- # Transfer control to the operation - this will stop the task.
77
- def transfer
78
- @task.stop(false, cause: @cause)
79
- end
80
- end
81
- end
9
+ Stop = Cancel
82
10
  end
data/lib/async/task.rb CHANGED
@@ -1,37 +1,27 @@
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
-
34
- # 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`, `cancelled` or `stopped`.
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`.
35
25
  #
36
26
  # ```mermaid
37
27
  # stateDiagram-v2
@@ -44,11 +34,11 @@ module Async
44
34
  # Completed --> [*]
45
35
  # Failed --> [*]
46
36
  #
47
- # Running --> Stopped : Stop
48
- # Stopped --> [*]
49
- # Completed --> Stopped : Stop
50
- # Failed --> Stopped : Stop
51
- # Initialized --> Stopped : Stop
37
+ # Running --> Cancelled : Cancel
38
+ # Cancelled --> [*]
39
+ # Completed --> Cancelled : Cancel
40
+ # Failed --> Cancelled : Cancel
41
+ # Initialized --> Cancelled : Cancel
52
42
  # ```
53
43
  #
54
44
  # @example Creating a task that sleeps for 1 second.
@@ -108,7 +98,7 @@ module Async
108
98
  warn("finished: argument with non-false value is deprecated and will be removed.", uplevel: 1, category: :deprecated) if $VERBOSE
109
99
  end
110
100
 
111
- @defer_stop = nil
101
+ @defer_cancel = nil
112
102
 
113
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.
114
104
  super(parent, **options)
@@ -193,8 +183,8 @@ module Async
193
183
  @promise.failed?
194
184
  end
195
185
 
196
- # @returns [Boolean] Whether the task has been stopped.
197
- def stopped?
186
+ # @returns [Boolean] Whether the task has been cancelled.
187
+ def cancelled?
198
188
  @promise.cancelled?
199
189
  end
200
190
 
@@ -208,11 +198,11 @@ module Async
208
198
  self.completed?
209
199
  end
210
200
 
211
- # @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`.
212
202
  def status
213
203
  case @promise.resolved
214
204
  when :cancelled
215
- :stopped
205
+ :cancelled
216
206
  when :failed
217
207
  :failed
218
208
  when :completed
@@ -270,23 +260,19 @@ module Async
270
260
  return task
271
261
  end
272
262
 
273
- # 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`.
274
264
  #
275
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).
276
266
  #
267
+ # @parameter timeout [Numeric] The maximum number of seconds to wait for the result before raising a `TimeoutError`, if specified.
277
268
  # @raises [RuntimeError] If the task's fiber is the current fiber.
278
269
  # @returns [Object] The final expression/result of the task's block.
279
270
  # @asynchronous This method is thread-safe.
280
- def wait
271
+ def wait(...)
281
272
  raise "Cannot wait on own fiber!" if Fiber.current.equal?(@fiber)
282
273
 
283
- # Wait for the task to complete - Promise handles all the complexity:
284
- begin
285
- @promise.wait
286
- rescue Promise::Cancel
287
- # For backward compatibility, stopped tasks return nil:
288
- return nil
289
- end
274
+ # Wait for the task to complete:
275
+ @promise.wait(...)
290
276
  end
291
277
 
292
278
  # For compatibility with `Thread#join` and similar interfaces.
@@ -325,7 +311,7 @@ module Async
325
311
  def result
326
312
  value = @promise.value
327
313
 
328
- # For backward compatibility, return nil for stopped tasks:
314
+ # For backward compatibility, return nil for cancelled tasks:
329
315
  if @promise.cancelled?
330
316
  nil
331
317
  else
@@ -333,103 +319,115 @@ module Async
333
319
  end
334
320
  end
335
321
 
336
- # Stop the task and all of its children.
322
+ # Cancel the task and all of its children.
337
323
  #
338
- # 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.
339
325
  #
340
- # @parameter later [Boolean] Whether to stop the task later, or immediately.
341
- # @parameter cause [Exception] The cause of the stop operation.
342
- 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: $!)
343
329
  # If no cause is given, we generate one from the current call stack:
344
330
  unless cause
345
- cause = Stop::Cause.for("Stopping task!")
331
+ cause = Cancel::Cause.for("Cancelling task!")
346
332
  end
347
333
 
348
- if self.stopped?
349
- # 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.
350
- 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!
351
337
  end
352
338
 
353
- # If the fiber is alive, we need to stop it:
339
+ # If the fiber is alive, we need to cancel it:
354
340
  if @fiber&.alive?
355
341
  # As the task is now exiting, we want to ensure the event loop continues to execute until the task finishes.
356
342
  self.transient = false
357
343
 
358
- # If we are deferring stop...
359
- if @defer_stop == false
360
- # Don't stop now... but update the state so we know we need to stop later.
361
- @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
362
348
  return false
363
349
  end
364
350
 
365
351
  if self.current?
366
- # 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`:
367
353
  if later
368
- # If the fiber is the current fiber and we want to stop it later, schedule it:
369
- 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))
370
356
  else
371
357
  # Otherwise, raise the exception directly:
372
- raise Stop, "Stopping current task!", cause: cause
358
+ raise Cancel, "Cancelling current task!", cause: cause
373
359
  end
374
360
  else
375
361
  # If the fiber is not curent, we can raise the exception directly:
376
362
  begin
377
- # 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.
378
- 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)
379
365
  rescue FiberError
380
- # In some cases, this can cause a FiberError (it might be resumed already), so we schedule it to be stopped later:
381
- 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))
382
368
  end
383
369
  end
384
370
  else
385
- # We are not running, but children might be, so transition directly into stopped state:
386
- stop!
371
+ # We are not running, but children might be, so transition directly into cancelled state:
372
+ cancel!
387
373
  end
388
374
  end
389
375
 
390
- # 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.
391
377
  #
392
- # 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.
393
379
  #
394
- # 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.
395
381
  #
396
382
  # @yields {} The block of code to execute.
397
383
  # @public Since *Async v1*.
398
- def defer_stop
399
- # Tri-state variable for controlling stop:
400
- # - nil: defer_stop has not been called.
401
- # - false: defer_stop has been called and we are not stopping.
402
- # - true: defer_stop has been called and we will stop when exiting the block.
403
- 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?
404
390
  begin
405
- # If we are not deferring stop already, we can defer it now:
406
- @defer_stop = false
391
+ # If we are not deferring cancel already, we can defer it now:
392
+ @defer_cancel = false
407
393
 
408
394
  yield
409
- rescue Stop
410
- # If we are exiting due to a stop, we shouldn't try to invoke stop again:
411
- @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
412
398
  raise
413
399
  ensure
414
- defer_stop = @defer_stop
400
+ defer_cancel = @defer_cancel
415
401
 
416
402
  # We need to ensure the state is reset before we exit the block:
417
- @defer_stop = nil
403
+ @defer_cancel = nil
418
404
 
419
- # If we were asked to stop, we should do so now:
420
- if defer_stop
421
- 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
422
408
  end
423
409
  end
424
410
  else
425
- # 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.
426
412
  yield
427
413
  end
428
414
  end
429
415
 
430
- # @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.
431
429
  def stop_deferred?
432
- !!@defer_stop
430
+ cancel_deferred?
433
431
  end
434
432
 
435
433
  # Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
@@ -478,40 +476,48 @@ module Async
478
476
  @promise.reject(exception)
479
477
  end
480
478
 
481
- def stopped!
482
- # Console.info(self, status:) {"Task #{self} was stopped with #{@children&.size.inspect} children!"}
479
+ def cancelled!
480
+ # Console.info(self, status:) {"Task #{self} was cancelled with #{@children&.size.inspect} children!"}
483
481
 
484
- # Cancel the promise:
485
- @promise.cancel
482
+ # Cancel the promise, specify nil here so that no exception is raised when waiting on the promise:
483
+ @promise.cancel(nil)
486
484
 
487
- stopped = false
485
+ cancelled = false
488
486
 
489
487
  begin
490
488
  # We are not running, but children might be so we should stop them:
491
489
  stop_children(true)
492
- rescue Stop
493
- stopped = true
494
- # If we are stopping children, and one of them tries to stop the current task, we should ignore it. We will be stopped later.
490
+ rescue Cancel
491
+ cancelled = true
492
+ # If we are cancelling children, and one of them tries to cancel the current task, we should ignore it. We will be cancelled later.
495
493
  retry
496
494
  end
497
495
 
498
- if stopped
499
- raise Stop, "Stopping current task!"
496
+ if cancelled
497
+ raise Cancel, "Cancelling current task!"
500
498
  end
501
499
  end
502
500
 
503
- def stop!
504
- stopped!
501
+ def stopped!
502
+ cancelled!
503
+ end
504
+
505
+ def cancel!
506
+ cancelled!
505
507
 
506
508
  finish!
507
509
  end
508
510
 
511
+ def stop!
512
+ cancel!
513
+ end
514
+
509
515
  def schedule(&block)
510
516
  @fiber = Fiber.new(annotation: self.annotation) do
511
517
  begin
512
518
  completed!(yield)
513
- rescue Stop
514
- stopped!
519
+ rescue Cancel
520
+ cancelled!
515
521
  rescue StandardError => error
516
522
  failed!(error)
517
523
  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.36.0"
8
+ VERSION = "2.38.0"
8
9
  end
data/lib/async.rb CHANGED
@@ -1,16 +1,13 @@
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, 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
  require_relative "kernel/barrier"
13
-
14
- # Asynchronous programming framework.
15
- module Async
16
- end
data/license.md CHANGED
@@ -31,7 +31,7 @@ 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
37
  Copyright, 2025, by Yuhi Sato.
data/readme.md CHANGED
@@ -35,6 +35,16 @@ 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.0
39
+
40
+ - 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.
41
+ - Forward arguments from `Task#wait` -\> `Promise#wait`, so `task.wait(timeout: N)` is supported.
42
+
43
+ ### v2.37.0
44
+
45
+ - Introduce `Async::Loop` for robust, time-aligned loops.
46
+ - Add support for `Async::Promise#wait(timeout: N)`.
47
+
38
48
  ### v2.36.0
39
49
 
40
50
  - Introduce `Task#wait_all` which recursively waits for all children and self, excepting the current task.
@@ -69,14 +79,6 @@ Please see the [project releases](https://socketry.github.io/async/releases/inde
69
79
 
70
80
  - Fix typo in documentation.
71
81
 
72
- ### v2.32.0
73
-
74
- - Introduce `Queue#waiting_count` and `PriorityQueue#waiting_count`. Generally for statistics/testing purposes only.
75
-
76
- ### v2.31.0
77
-
78
- - Introduce `Async::Deadline` for precise timeout management in compound operations.
79
-
80
82
  ## See Also
81
83
 
82
84
  - [async-http](https://github.com/socketry/async-http) — Asynchronous HTTP client/server.
data/releases.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Releases
2
2
 
3
+ ## v2.38.0
4
+
5
+ - 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.
6
+ - Forward arguments from `Task#wait` -\> `Promise#wait`, so `task.wait(timeout: N)` is supported.
7
+
8
+ ## v2.37.0
9
+
10
+ - Introduce `Async::Loop` for robust, time-aligned loops.
11
+ - Add support for `Async::Promise#wait(timeout: N)`.
12
+
3
13
  ## v2.36.0
4
14
 
5
15
  - Introduce `Task#wait_all` which recursively waits for all children and self, excepting the current task.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.36.0
4
+ version: 2.38.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -156,15 +156,18 @@ files:
156
156
  - lib/async.rb
157
157
  - lib/async/barrier.md
158
158
  - lib/async/barrier.rb
159
+ - lib/async/cancel.rb
159
160
  - lib/async/clock.rb
160
161
  - lib/async/condition.md
161
162
  - lib/async/condition.rb
162
163
  - lib/async/console.rb
163
164
  - lib/async/deadline.rb
165
+ - lib/async/error.rb
164
166
  - lib/async/fork_handler.rb
165
167
  - lib/async/idler.rb
166
168
  - lib/async/limited_queue.rb
167
169
  - lib/async/list.rb
170
+ - lib/async/loop.rb
168
171
  - lib/async/node.rb
169
172
  - lib/async/notification.rb
170
173
  - lib/async/priority_queue.rb
@@ -205,7 +208,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
205
208
  requirements:
206
209
  - - ">="
207
210
  - !ruby/object:Gem::Version
208
- version: '3.2'
211
+ version: '3.3'
209
212
  required_rubygems_version: !ruby/object:Gem::Requirement
210
213
  requirements:
211
214
  - - ">="
metadata.gz.sig CHANGED
Binary file