async 2.37.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: 59107a94e01d137ccdba59b8204cfbb0a223418e3ce5374386fe9c12bf7f455d
4
- data.tar.gz: 7a2dfe8b7b15bbe059e898faa3fd59b1b024e4156744897246368763ed35a106
3
+ metadata.gz: 847417a49140235dad0d6e2b063dd6a3465247cb7b3000708d58bd01adbcffb1
4
+ data.tar.gz: f79a5b29cd8e10bdfe7f10ba24f0fb850bb0856e8612dffcecdd7882c133bc61
5
5
  SHA512:
6
- metadata.gz: 03c75631a638f69c5e3bbd720c75c9edc1bb7f9a71a296fdc5f089532d0eb86d91babc5760cd97afdbe60ae1aabd226de2ae325b5d8f458538376faa18f3d820
7
- data.tar.gz: c328e8223cd12b1e44329737156163fc152f981caedd7af7bb5b1f3920ed96cf6a93f18564a49d9d2e52f03bdd2e61aec39b519e5c13c9830553e2c709c0d486
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/node.rb CHANGED
@@ -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
@@ -6,6 +6,7 @@
6
6
 
7
7
  require_relative "error"
8
8
  require_relative "deadline"
9
+ require_relative "cancel"
9
10
 
10
11
  module Async
11
12
  # A promise represents a value that will be available in the future.
@@ -77,42 +78,6 @@ module Async
77
78
  @mutex.synchronize{@resolved ? @value : nil}
78
79
  end
79
80
 
80
- # Wait for the promise to be resolved and return the value.
81
- # If already resolved, returns immediately. If rejected, raises the stored exception.
82
- #
83
- # @parameter timeout [Numeric | Nil] Maximum time to wait. If nil, waits indefinitely. If 0, raises immediately if not resolved.
84
- # @returns [Object] The resolved value.
85
- # @raises [Exception] The rejected or cancelled exception.
86
- # @raises [Async::TimeoutError] If timeout expires before the promise is resolved.
87
- def wait(timeout: nil)
88
- @mutex.synchronize do
89
- # Increment waiting count:
90
- @waiting += 1
91
-
92
- begin
93
- # Wait for resolution if not already resolved:
94
- unless @resolved
95
- if timeout.nil?
96
- wait_indefinitely
97
- else
98
- wait_with_timeout(timeout)
99
- end
100
- end
101
-
102
- # Return value or raise exception based on resolution type:
103
- if @resolved == :completed
104
- return @value
105
- else
106
- # Both :failed and :cancelled store exceptions in @value
107
- raise @value
108
- end
109
- ensure
110
- # Decrement waiting count when done:
111
- @waiting -= 1
112
- end
113
- end
114
- end
115
-
116
81
  # Wait indefinitely for the promise to be resolved.
117
82
  private def wait_indefinitely
118
83
  until @resolved
@@ -122,14 +87,14 @@ module Async
122
87
 
123
88
  # Wait for the promise to be resolved, respecting the deadline timeout.
124
89
  # @parameter timeout [Numeric] The timeout duration.
125
- # @raises [Async::TimeoutError] If the timeout expires before resolution.
90
+ # @returns [Boolean] True if resolved, false if timeout expires.
126
91
  private def wait_with_timeout(timeout)
127
92
  # Create deadline for timeout tracking:
128
93
  deadline = Deadline.start(timeout)
129
94
 
130
95
  # Handle immediate timeout (non-blocking):
131
96
  if deadline == Deadline::Zero && !@resolved
132
- raise Async::TimeoutError, "Promise wait not resolved!"
97
+ return false
133
98
  end
134
99
 
135
100
  # Wait with deadline tracking:
@@ -139,11 +104,67 @@ module Async
139
104
 
140
105
  # Check if deadline has expired before waiting:
141
106
  if remaining <= 0
142
- raise Async::TimeoutError, "Promise wait timed out!"
107
+ return false
143
108
  end
144
109
 
145
110
  @condition.wait(@mutex, remaining)
146
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
+
150
+ # Wait for the promise to be resolved and return the value.
151
+ #
152
+ # If already resolved, returns immediately. If rejected, raises the stored exception.
153
+ #
154
+ # @returns [Object] The resolved value.
155
+ # @raises [Exception] The rejected or cancelled exception.
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
167
+ end
147
168
  end
148
169
 
149
170
  # Resolve the promise with a value.
@@ -155,8 +176,8 @@ module Async
155
176
  @mutex.synchronize do
156
177
  return if @resolved
157
178
 
158
- @value = value
159
179
  @resolved = :completed
180
+ @value = value
160
181
 
161
182
  # Wake up all waiting fibers:
162
183
  @condition.broadcast
@@ -174,8 +195,8 @@ module Async
174
195
  @mutex.synchronize do
175
196
  return if @resolved
176
197
 
177
- @value = exception
178
198
  @resolved = :failed
199
+ @value = exception
179
200
 
180
201
  # Wake up all waiting fibers:
181
202
  @condition.broadcast
@@ -184,20 +205,16 @@ module Async
184
205
  return nil
185
206
  end
186
207
 
187
- # Exception used to indicate cancellation.
188
- class Cancel < Exception
189
- end
190
-
191
208
  # Cancel the promise, indicating cancellation.
192
209
  # All current and future waiters will receive nil.
193
210
  # Can only be called on pending promises - no-op if already resolved.
194
- def cancel(exception = Cancel.new("Promise was cancelled!"))
211
+ def cancel(exception = Cancel.new("Promise cancelled!"))
195
212
  @mutex.synchronize do
196
213
  # No-op if already in any final state
197
214
  return if @resolved
198
215
 
199
- @value = exception
200
216
  @resolved = :cancelled
217
+ @value = exception
201
218
 
202
219
  # Wake up all waiting fibers:
203
220
  @condition.broadcast
@@ -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
@@ -21,7 +21,7 @@ require_relative "stop"
21
21
  Fiber.attr_accessor :async_task
22
22
 
23
23
  module Async
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`, `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`.
25
25
  #
26
26
  # ```mermaid
27
27
  # stateDiagram-v2
@@ -34,11 +34,11 @@ module Async
34
34
  # Completed --> [*]
35
35
  # Failed --> [*]
36
36
  #
37
- # Running --> Stopped : Stop
38
- # Stopped --> [*]
39
- # Completed --> Stopped : Stop
40
- # Failed --> Stopped : Stop
41
- # Initialized --> Stopped : Stop
37
+ # Running --> Cancelled : Cancel
38
+ # Cancelled --> [*]
39
+ # Completed --> Cancelled : Cancel
40
+ # Failed --> Cancelled : Cancel
41
+ # Initialized --> Cancelled : Cancel
42
42
  # ```
43
43
  #
44
44
  # @example Creating a task that sleeps for 1 second.
@@ -98,7 +98,7 @@ module Async
98
98
  warn("finished: argument with non-false value is deprecated and will be removed.", uplevel: 1, category: :deprecated) if $VERBOSE
99
99
  end
100
100
 
101
- @defer_stop = nil
101
+ @defer_cancel = nil
102
102
 
103
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
104
  super(parent, **options)
@@ -183,8 +183,8 @@ module Async
183
183
  @promise.failed?
184
184
  end
185
185
 
186
- # @returns [Boolean] Whether the task has been stopped.
187
- def stopped?
186
+ # @returns [Boolean] Whether the task has been cancelled.
187
+ def cancelled?
188
188
  @promise.cancelled?
189
189
  end
190
190
 
@@ -198,11 +198,11 @@ module Async
198
198
  self.completed?
199
199
  end
200
200
 
201
- # @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`.
202
202
  def status
203
203
  case @promise.resolved
204
204
  when :cancelled
205
- :stopped
205
+ :cancelled
206
206
  when :failed
207
207
  :failed
208
208
  when :completed
@@ -260,23 +260,19 @@ module Async
260
260
  return task
261
261
  end
262
262
 
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 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`.
264
264
  #
265
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).
266
266
  #
267
+ # @parameter timeout [Numeric] The maximum number of seconds to wait for the result before raising a `TimeoutError`, if specified.
267
268
  # @raises [RuntimeError] If the task's fiber is the current fiber.
268
269
  # @returns [Object] The final expression/result of the task's block.
269
270
  # @asynchronous This method is thread-safe.
270
- def wait
271
+ def wait(...)
271
272
  raise "Cannot wait on own fiber!" if Fiber.current.equal?(@fiber)
272
273
 
273
- # Wait for the task to complete - Promise handles all the complexity:
274
- begin
275
- @promise.wait
276
- rescue Promise::Cancel
277
- # For backward compatibility, stopped tasks return nil:
278
- return nil
279
- end
274
+ # Wait for the task to complete:
275
+ @promise.wait(...)
280
276
  end
281
277
 
282
278
  # For compatibility with `Thread#join` and similar interfaces.
@@ -315,7 +311,7 @@ module Async
315
311
  def result
316
312
  value = @promise.value
317
313
 
318
- # For backward compatibility, return nil for stopped tasks:
314
+ # For backward compatibility, return nil for cancelled tasks:
319
315
  if @promise.cancelled?
320
316
  nil
321
317
  else
@@ -323,103 +319,115 @@ module Async
323
319
  end
324
320
  end
325
321
 
326
- # Stop the task and all of its children.
322
+ # Cancel the task and all of its children.
327
323
  #
328
- # 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.
329
325
  #
330
- # @parameter later [Boolean] Whether to stop the task later, or immediately.
331
- # @parameter cause [Exception] The cause of the stop operation.
332
- 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: $!)
333
329
  # If no cause is given, we generate one from the current call stack:
334
330
  unless cause
335
- cause = Stop::Cause.for("Stopping task!")
331
+ cause = Cancel::Cause.for("Cancelling task!")
336
332
  end
337
333
 
338
- if self.stopped?
339
- # 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.
340
- 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!
341
337
  end
342
338
 
343
- # If the fiber is alive, we need to stop it:
339
+ # If the fiber is alive, we need to cancel it:
344
340
  if @fiber&.alive?
345
341
  # As the task is now exiting, we want to ensure the event loop continues to execute until the task finishes.
346
342
  self.transient = false
347
343
 
348
- # If we are deferring stop...
349
- if @defer_stop == false
350
- # Don't stop now... but update the state so we know we need to stop later.
351
- @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
352
348
  return false
353
349
  end
354
350
 
355
351
  if self.current?
356
- # 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`:
357
353
  if later
358
- # If the fiber is the current fiber and we want to stop it later, schedule it:
359
- 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))
360
356
  else
361
357
  # Otherwise, raise the exception directly:
362
- raise Stop, "Stopping current task!", cause: cause
358
+ raise Cancel, "Cancelling current task!", cause: cause
363
359
  end
364
360
  else
365
361
  # If the fiber is not curent, we can raise the exception directly:
366
362
  begin
367
- # 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.
368
- 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)
369
365
  rescue FiberError
370
- # In some cases, this can cause a FiberError (it might be resumed already), so we schedule it to be stopped later:
371
- 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))
372
368
  end
373
369
  end
374
370
  else
375
- # We are not running, but children might be, so transition directly into stopped state:
376
- stop!
371
+ # We are not running, but children might be, so transition directly into cancelled state:
372
+ cancel!
377
373
  end
378
374
  end
379
375
 
380
- # 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.
381
377
  #
382
- # 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.
383
379
  #
384
- # 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.
385
381
  #
386
382
  # @yields {} The block of code to execute.
387
383
  # @public Since *Async v1*.
388
- def defer_stop
389
- # Tri-state variable for controlling stop:
390
- # - nil: defer_stop has not been called.
391
- # - false: defer_stop has been called and we are not stopping.
392
- # - true: defer_stop has been called and we will stop when exiting the block.
393
- 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?
394
390
  begin
395
- # If we are not deferring stop already, we can defer it now:
396
- @defer_stop = false
391
+ # If we are not deferring cancel already, we can defer it now:
392
+ @defer_cancel = false
397
393
 
398
394
  yield
399
- rescue Stop
400
- # If we are exiting due to a stop, we shouldn't try to invoke stop again:
401
- @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
402
398
  raise
403
399
  ensure
404
- defer_stop = @defer_stop
400
+ defer_cancel = @defer_cancel
405
401
 
406
402
  # We need to ensure the state is reset before we exit the block:
407
- @defer_stop = nil
403
+ @defer_cancel = nil
408
404
 
409
- # If we were asked to stop, we should do so now:
410
- if defer_stop
411
- 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
412
408
  end
413
409
  end
414
410
  else
415
- # 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.
416
412
  yield
417
413
  end
418
414
  end
419
415
 
420
- # @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.
421
429
  def stop_deferred?
422
- !!@defer_stop
430
+ cancel_deferred?
423
431
  end
424
432
 
425
433
  # Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
@@ -468,40 +476,48 @@ module Async
468
476
  @promise.reject(exception)
469
477
  end
470
478
 
471
- def stopped!
472
- # 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!"}
473
481
 
474
- # Cancel the promise:
475
- @promise.cancel
482
+ # Cancel the promise, specify nil here so that no exception is raised when waiting on the promise:
483
+ @promise.cancel(nil)
476
484
 
477
- stopped = false
485
+ cancelled = false
478
486
 
479
487
  begin
480
488
  # We are not running, but children might be so we should stop them:
481
489
  stop_children(true)
482
- rescue Stop
483
- stopped = true
484
- # 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.
485
493
  retry
486
494
  end
487
495
 
488
- if stopped
489
- raise Stop, "Stopping current task!"
496
+ if cancelled
497
+ raise Cancel, "Cancelling current task!"
490
498
  end
491
499
  end
492
500
 
493
- def stop!
494
- stopped!
501
+ def stopped!
502
+ cancelled!
503
+ end
504
+
505
+ def cancel!
506
+ cancelled!
495
507
 
496
508
  finish!
497
509
  end
498
510
 
511
+ def stop!
512
+ cancel!
513
+ end
514
+
499
515
  def schedule(&block)
500
516
  @fiber = Fiber.new(annotation: self.annotation) do
501
517
  begin
502
518
  completed!(yield)
503
- rescue Stop
504
- stopped!
519
+ rescue Cancel
520
+ cancelled!
505
521
  rescue StandardError => error
506
522
  failed!(error)
507
523
  rescue Exception => exception
data/lib/async/version.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2017-2026, by Samuel Williams.
5
5
 
6
+ # @namespace
6
7
  module Async
7
- VERSION = "2.37.0"
8
+ VERSION = "2.38.0"
8
9
  end
data/lib/async.rb CHANGED
@@ -1,7 +1,7 @@
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"
@@ -11,7 +11,3 @@ require_relative "async/loop"
11
11
  require_relative "kernel/async"
12
12
  require_relative "kernel/sync"
13
13
  require_relative "kernel/barrier"
14
-
15
- # Asynchronous programming framework.
16
- module Async
17
- end
data/readme.md CHANGED
@@ -35,9 +35,15 @@ 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
+
38
43
  ### v2.37.0
39
44
 
40
45
  - Introduce `Async::Loop` for robust, time-aligned loops.
46
+ - Add support for `Async::Promise#wait(timeout: N)`.
41
47
 
42
48
  ### v2.36.0
43
49
 
@@ -73,10 +79,6 @@ Please see the [project releases](https://socketry.github.io/async/releases/inde
73
79
 
74
80
  - Fix typo in documentation.
75
81
 
76
- ### v2.32.0
77
-
78
- - Introduce `Queue#waiting_count` and `PriorityQueue#waiting_count`. Generally for statistics/testing purposes only.
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,8 +1,14 @@
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
+
3
8
  ## v2.37.0
4
9
 
5
10
  - Introduce `Async::Loop` for robust, time-aligned loops.
11
+ - Add support for `Async::Promise#wait(timeout: N)`.
6
12
 
7
13
  ## v2.36.0
8
14
 
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.37.0
4
+ version: 2.38.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -156,6 +156,7 @@ 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
@@ -207,7 +208,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
207
208
  requirements:
208
209
  - - ">="
209
210
  - !ruby/object:Gem::Version
210
- version: '3.2'
211
+ version: '3.3'
211
212
  required_rubygems_version: !ruby/object:Gem::Requirement
212
213
  requirements:
213
214
  - - ">="
metadata.gz.sig CHANGED
Binary file