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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/scheduler.md +3 -3
- data/context/tasks.md +28 -28
- data/lib/async/barrier.rb +10 -4
- data/lib/async/cancel.rb +80 -0
- data/lib/async/node.rb +18 -6
- data/lib/async/promise.rb +64 -47
- data/lib/async/scheduler.rb +9 -4
- data/lib/async/stop.rb +3 -75
- data/lib/async/task.rb +97 -81
- data/lib/async/version.rb +2 -1
- data/lib/async.rb +1 -5
- data/readme.md +6 -4
- data/releases.md +6 -0
- data.tar.gz.sig +0 -0
- metadata +3 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 847417a49140235dad0d6e2b063dd6a3465247cb7b3000708d58bd01adbcffb1
|
|
4
|
+
data.tar.gz: f79a5b29cd8e10bdfe7f10ba24f0fb850bb0856e8612dffcecdd7882c133bc61
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
###
|
|
72
|
+
### Cancelling a Scheduler
|
|
73
73
|
|
|
74
|
-
{ruby Async::Scheduler#
|
|
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
|
|
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
|
|
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,
|
|
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 -->
|
|
43
|
+
running --> cancelled : cancel
|
|
44
44
|
|
|
45
|
-
initialized -->
|
|
45
|
+
initialized --> cancelled : cancel
|
|
46
46
|
|
|
47
47
|
failed --> [*]
|
|
48
48
|
complete --> [*]
|
|
49
|
-
|
|
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 `
|
|
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
|
|
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
|
|
179
|
-
barrier.
|
|
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
|
-
#
|
|
203
|
-
barrier.
|
|
202
|
+
# Cancel any remaining jobs:
|
|
203
|
+
barrier.cancel
|
|
204
204
|
end
|
|
205
205
|
~~~
|
|
206
206
|
|
|
207
|
-
##
|
|
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
|
|
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
|
|
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
|
-
#
|
|
225
|
-
tasks.each(&:
|
|
224
|
+
# Cancel all the above tasks:
|
|
225
|
+
tasks.each(&:cancel)
|
|
226
226
|
end
|
|
227
227
|
```
|
|
228
228
|
|
|
229
|
-
###
|
|
229
|
+
### Cancelling all Tasks held in a Barrier
|
|
230
230
|
|
|
231
|
-
To
|
|
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.
|
|
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#
|
|
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.
|
|
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::
|
|
276
|
+
socket = connect(remote_address) # May raise Async::Cancel
|
|
277
277
|
|
|
278
|
-
socket.write(...) # May raise Async::
|
|
279
|
-
socket.read(...) # May raise Async::
|
|
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
|
|
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 `
|
|
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::
|
|
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-
|
|
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
|
-
#
|
|
91
|
+
# Cancel all tasks held by the barrier.
|
|
92
92
|
# @asynchronous May wait for tasks to finish executing.
|
|
93
|
-
def
|
|
93
|
+
def cancel
|
|
94
94
|
@tasks.each do |waiting|
|
|
95
|
-
waiting.task.
|
|
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
|
data/lib/async/cancel.rb
ADDED
|
@@ -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
|
|
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
|
|
286
|
-
def
|
|
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.
|
|
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
|
|
305
|
-
def
|
|
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
|
-
# @
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/async/scheduler.rb
CHANGED
|
@@ -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 (#{
|
|
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
|
-
#
|
|
514
|
+
# Cancel all children, including transient children.
|
|
515
515
|
#
|
|
516
516
|
# @public Since *Async v1*.
|
|
517
|
-
def
|
|
517
|
+
def cancel
|
|
518
518
|
@children&.each do |child|
|
|
519
|
-
child.
|
|
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
|
-
|
|
7
|
-
require "console"
|
|
6
|
+
require_relative "cancel"
|
|
8
7
|
|
|
9
8
|
module Async
|
|
10
|
-
|
|
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`,
|
|
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 -->
|
|
38
|
-
#
|
|
39
|
-
# Completed -->
|
|
40
|
-
# Failed -->
|
|
41
|
-
# Initialized -->
|
|
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
|
-
@
|
|
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
|
|
187
|
-
def
|
|
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`, `:
|
|
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
|
-
:
|
|
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
|
|
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
|
|
274
|
-
|
|
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
|
|
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
|
-
#
|
|
322
|
+
# Cancel the task and all of its children.
|
|
327
323
|
#
|
|
328
|
-
# If `later` is false, it means that `
|
|
324
|
+
# If `later` is false, it means that `cancel` has been invoked directly. When `later` is true, it means that `cancel` is invoked by `stop_children` or some other indirect mechanism. In that case, if we encounter the "current" fiber, we can't cancel it right away, as it's currently performing `#cancel`. Cancelling it immediately would interrupt the current cancel traversal, so we need to schedule the cancel to occur later.
|
|
329
325
|
#
|
|
330
|
-
# @parameter later [Boolean] Whether to
|
|
331
|
-
# @parameter cause [Exception] The cause of the
|
|
332
|
-
def
|
|
326
|
+
# @parameter later [Boolean] Whether to cancel the task later, or immediately.
|
|
327
|
+
# @parameter cause [Exception] The cause of the cancel operation.
|
|
328
|
+
def cancel(later = false, cause: $!)
|
|
333
329
|
# If no cause is given, we generate one from the current call stack:
|
|
334
330
|
unless cause
|
|
335
|
-
cause =
|
|
331
|
+
cause = Cancel::Cause.for("Cancelling task!")
|
|
336
332
|
end
|
|
337
333
|
|
|
338
|
-
if self.
|
|
339
|
-
# If the task is already
|
|
340
|
-
return
|
|
334
|
+
if self.cancelled?
|
|
335
|
+
# If the task is already cancelled, a `cancel` state transition re-enters the same state which is a no-op. However, we will also attempt to cancel any running children too. This can happen if the children did not cancel correctly the first time around. Doing this should probably be considered a bug, but it's better to be safe than sorry.
|
|
336
|
+
return cancelled!
|
|
341
337
|
end
|
|
342
338
|
|
|
343
|
-
# If the fiber is alive, we need to
|
|
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
|
|
349
|
-
if @
|
|
350
|
-
# Don't
|
|
351
|
-
@
|
|
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
|
|
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
|
|
359
|
-
Fiber.scheduler.push(
|
|
354
|
+
# If the fiber is the current fiber and we want to cancel it later, schedule it:
|
|
355
|
+
Fiber.scheduler.push(Cancel::Later.new(self, cause))
|
|
360
356
|
else
|
|
361
357
|
# Otherwise, raise the exception directly:
|
|
362
|
-
raise
|
|
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
|
|
368
|
-
Fiber.scheduler.raise(@fiber,
|
|
363
|
+
# There is a chance that this will cancel the fiber that originally called cancel. If that happens, the exception handling in `#cancelled` will rescue the exception and re-raise it later.
|
|
364
|
+
Fiber.scheduler.raise(@fiber, Cancel, cause: cause)
|
|
369
365
|
rescue FiberError
|
|
370
|
-
# In some cases, this can cause a FiberError (it might be resumed already), so we schedule it to be
|
|
371
|
-
Fiber.scheduler.push(
|
|
366
|
+
# In some cases, this can cause a FiberError (it might be resumed already), so we schedule it to be cancelled later:
|
|
367
|
+
Fiber.scheduler.push(Cancel::Later.new(self, cause))
|
|
372
368
|
end
|
|
373
369
|
end
|
|
374
370
|
else
|
|
375
|
-
# We are not running, but children might be, so transition directly into
|
|
376
|
-
|
|
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
|
|
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
|
|
378
|
+
# You can nest calls to defer_cancel, but the cancel will only be deferred until the outermost block exits.
|
|
383
379
|
#
|
|
384
|
-
# If
|
|
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
|
|
389
|
-
# Tri-state variable for controlling
|
|
390
|
-
# - nil:
|
|
391
|
-
# - false:
|
|
392
|
-
# - true:
|
|
393
|
-
if @
|
|
384
|
+
def defer_cancel
|
|
385
|
+
# Tri-state variable for controlling cancel:
|
|
386
|
+
# - nil: defer_cancel has not been called.
|
|
387
|
+
# - false: defer_cancel has been called and we are not cancelling.
|
|
388
|
+
# - true: defer_cancel has been called and we will cancel when exiting the block.
|
|
389
|
+
if @defer_cancel.nil?
|
|
394
390
|
begin
|
|
395
|
-
# If we are not deferring
|
|
396
|
-
@
|
|
391
|
+
# If we are not deferring cancel already, we can defer it now:
|
|
392
|
+
@defer_cancel = false
|
|
397
393
|
|
|
398
394
|
yield
|
|
399
|
-
rescue
|
|
400
|
-
# If we are exiting due to a
|
|
401
|
-
@
|
|
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
|
-
|
|
400
|
+
defer_cancel = @defer_cancel
|
|
405
401
|
|
|
406
402
|
# We need to ensure the state is reset before we exit the block:
|
|
407
|
-
@
|
|
403
|
+
@defer_cancel = nil
|
|
408
404
|
|
|
409
|
-
# If we were asked to
|
|
410
|
-
if
|
|
411
|
-
raise
|
|
405
|
+
# If we were asked to cancel, we should do so now:
|
|
406
|
+
if defer_cancel
|
|
407
|
+
raise Cancel, "Cancelling current task (was deferred)!", cause: defer_cancel
|
|
412
408
|
end
|
|
413
409
|
end
|
|
414
410
|
else
|
|
415
|
-
# If we are deferring
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
472
|
-
# Console.info(self, status:) {"Task #{self} was
|
|
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
|
-
|
|
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
|
|
483
|
-
|
|
484
|
-
# If we are
|
|
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
|
|
489
|
-
raise
|
|
496
|
+
if cancelled
|
|
497
|
+
raise Cancel, "Cancelling current task!"
|
|
490
498
|
end
|
|
491
499
|
end
|
|
492
500
|
|
|
493
|
-
def
|
|
494
|
-
|
|
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
|
|
504
|
-
|
|
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
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-
|
|
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.
|
|
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.
|
|
211
|
+
version: '3.3'
|
|
211
212
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
212
213
|
requirements:
|
|
213
214
|
- - ">="
|
metadata.gz.sig
CHANGED
|
Binary file
|