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 +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/clock.rb +2 -1
- data/lib/async/error.rb +17 -0
- data/lib/async/fork_handler.rb +2 -2
- data/lib/async/loop.rb +84 -0
- data/lib/async/node.rb +20 -8
- data/lib/async/promise.rb +89 -30
- data/lib/async/scheduler.rb +11 -6
- data/lib/async/stop.rb +3 -75
- data/lib/async/task.rb +100 -94
- data/lib/async/version.rb +3 -2
- data/lib/async.rb +2 -5
- data/license.md +1 -1
- data/readme.md +10 -8
- data/releases.md +10 -0
- data.tar.gz.sig +0 -0
- metadata +5 -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/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-
|
|
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.
|
data/lib/async/error.rb
ADDED
|
@@ -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
|
data/lib/async/fork_handler.rb
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
@@ -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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
data/lib/async/scheduler.rb
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2020-
|
|
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 (#{
|
|
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
|
@@ -1,37 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2017-
|
|
4
|
+
# Copyright, 2017-2026, by Samuel Williams.
|
|
5
5
|
# Copyright, 2017, by Kent Gruber.
|
|
6
6
|
# Copyright, 2017, by Devin Christensen.
|
|
7
7
|
# Copyright, 2020, by Patrik Wenger.
|
|
8
8
|
# Copyright, 2023, by Math Ieu.
|
|
9
9
|
# Copyright, 2025, by Shigeru Nakajima.
|
|
10
|
-
# Copyright, 2025, by Shopify Inc.
|
|
10
|
+
# Copyright, 2025-2026, by Shopify Inc.
|
|
11
11
|
|
|
12
12
|
require "fiber"
|
|
13
13
|
require "console"
|
|
14
14
|
|
|
15
15
|
require_relative "node"
|
|
16
16
|
require_relative "condition"
|
|
17
|
+
require_relative "error"
|
|
17
18
|
require_relative "promise"
|
|
18
19
|
require_relative "stop"
|
|
19
20
|
|
|
20
21
|
Fiber.attr_accessor :async_task
|
|
21
22
|
|
|
22
23
|
module Async
|
|
23
|
-
#
|
|
24
|
-
# @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 -->
|
|
48
|
-
#
|
|
49
|
-
# Completed -->
|
|
50
|
-
# Failed -->
|
|
51
|
-
# Initialized -->
|
|
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
|
-
@
|
|
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
|
|
197
|
-
def
|
|
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`, `:
|
|
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
|
-
:
|
|
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
|
|
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
|
|
284
|
-
|
|
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
|
|
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
|
-
#
|
|
322
|
+
# Cancel the task and all of its children.
|
|
337
323
|
#
|
|
338
|
-
# 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.
|
|
339
325
|
#
|
|
340
|
-
# @parameter later [Boolean] Whether to
|
|
341
|
-
# @parameter cause [Exception] The cause of the
|
|
342
|
-
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: $!)
|
|
343
329
|
# If no cause is given, we generate one from the current call stack:
|
|
344
330
|
unless cause
|
|
345
|
-
cause =
|
|
331
|
+
cause = Cancel::Cause.for("Cancelling task!")
|
|
346
332
|
end
|
|
347
333
|
|
|
348
|
-
if self.
|
|
349
|
-
# If the task is already
|
|
350
|
-
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!
|
|
351
337
|
end
|
|
352
338
|
|
|
353
|
-
# If the fiber is alive, we need to
|
|
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
|
|
359
|
-
if @
|
|
360
|
-
# Don't
|
|
361
|
-
@
|
|
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
|
|
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
|
|
369
|
-
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))
|
|
370
356
|
else
|
|
371
357
|
# Otherwise, raise the exception directly:
|
|
372
|
-
raise
|
|
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
|
|
378
|
-
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)
|
|
379
365
|
rescue FiberError
|
|
380
|
-
# In some cases, this can cause a FiberError (it might be resumed already), so we schedule it to be
|
|
381
|
-
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))
|
|
382
368
|
end
|
|
383
369
|
end
|
|
384
370
|
else
|
|
385
|
-
# We are not running, but children might be, so transition directly into
|
|
386
|
-
|
|
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
|
|
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
|
|
378
|
+
# You can nest calls to defer_cancel, but the cancel will only be deferred until the outermost block exits.
|
|
393
379
|
#
|
|
394
|
-
# If
|
|
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
|
|
399
|
-
# Tri-state variable for controlling
|
|
400
|
-
# - nil:
|
|
401
|
-
# - false:
|
|
402
|
-
# - true:
|
|
403
|
-
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?
|
|
404
390
|
begin
|
|
405
|
-
# If we are not deferring
|
|
406
|
-
@
|
|
391
|
+
# If we are not deferring cancel already, we can defer it now:
|
|
392
|
+
@defer_cancel = false
|
|
407
393
|
|
|
408
394
|
yield
|
|
409
|
-
rescue
|
|
410
|
-
# If we are exiting due to a
|
|
411
|
-
@
|
|
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
|
-
|
|
400
|
+
defer_cancel = @defer_cancel
|
|
415
401
|
|
|
416
402
|
# We need to ensure the state is reset before we exit the block:
|
|
417
|
-
@
|
|
403
|
+
@defer_cancel = nil
|
|
418
404
|
|
|
419
|
-
# If we were asked to
|
|
420
|
-
if
|
|
421
|
-
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
|
|
422
408
|
end
|
|
423
409
|
end
|
|
424
410
|
else
|
|
425
|
-
# If we are deferring
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
482
|
-
# Console.info(self, status:) {"Task #{self} was
|
|
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
|
-
|
|
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
|
|
493
|
-
|
|
494
|
-
# 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.
|
|
495
493
|
retry
|
|
496
494
|
end
|
|
497
495
|
|
|
498
|
-
if
|
|
499
|
-
raise
|
|
496
|
+
if cancelled
|
|
497
|
+
raise Cancel, "Cancelling current task!"
|
|
500
498
|
end
|
|
501
499
|
end
|
|
502
500
|
|
|
503
|
-
def
|
|
504
|
-
|
|
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
|
|
514
|
-
|
|
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
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-
|
|
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.
|
|
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.
|
|
211
|
+
version: '3.3'
|
|
209
212
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
210
213
|
requirements:
|
|
211
214
|
- - ">="
|
metadata.gz.sig
CHANGED
|
Binary file
|