async 2.24.0 → 2.27.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/agent.md +47 -0
- data/lib/async/barrier.md +0 -1
- data/lib/async/barrier.rb +30 -9
- data/lib/async/clock.rb +1 -1
- data/lib/async/condition.rb +3 -1
- data/lib/async/limited_queue.rb +7 -1
- data/lib/async/list.rb +16 -8
- data/lib/async/node.rb +2 -0
- data/lib/async/notification.rb +5 -3
- data/lib/async/queue.rb +52 -2
- data/lib/async/reactor.rb +3 -1
- data/lib/async/scheduler.rb +75 -7
- data/lib/async/stop.rb +82 -0
- data/lib/async/task.rb +21 -32
- data/lib/async/variable.rb +1 -1
- data/lib/async/version.rb +2 -2
- data/lib/async/waiter.rb +4 -1
- data/lib/metrics/provider/async/task.rb +1 -1
- data/lib/traces/provider/async/barrier.rb +1 -1
- data/lib/traces/provider/async/task.rb +1 -1
- data/license.md +6 -1
- data/readme.md +23 -5
- data/releases.md +116 -2
- data.tar.gz.sig +0 -0
- metadata +19 -14
- metadata.gz.sig +0 -0
- data/lib/async/waiter.md +0 -50
- data/lib/async/worker_pool.rb +0 -182
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4c9d36d758f8a197b7c00d3d2e4d83d280cfd8ddf4fb60bba1d7b9e95b64ec47
|
4
|
+
data.tar.gz: c0c19dcc509563b18a9385c1ff07a982054e6e984c9ed9c1fd715ee027c74f09
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b936e5c17d8e9e2eec7f3d9b3d2d4b00b5a16359cfb267a036f72fb254b53aeb234f8b9368ba1cd2ba41010438dd8da120621005bc1791fc554ee62647e46ed1
|
7
|
+
data.tar.gz: 808b0ce51e2bfa4a28d01126d59627409e6a25b37ee8e9e1f8146c138fcef995ab693e699f9c3ece326e234100a397c19e917a55c9d1e34dd797d26531e411f5
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/agent.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# Agent
|
2
|
+
|
3
|
+
## Context
|
4
|
+
|
5
|
+
This section provides links to documentation from installed packages. It is automatically generated and may be updated by running `bake agent:context:install`.
|
6
|
+
|
7
|
+
**Important:** Before performing any code, documentation, or analysis tasks, always read and apply the full content of any relevant documentation referenced in the following sections. These context files contain authoritative standards and best practices for documentation, code style, and project-specific workflows. **Do not proceed with any actions until you have read and incorporated the guidance from relevant context files.**
|
8
|
+
|
9
|
+
### agent-context
|
10
|
+
|
11
|
+
Install and manage context files from Ruby gems.
|
12
|
+
|
13
|
+
#### [Usage Guide](.context/agent-context/usage.md)
|
14
|
+
|
15
|
+
`agent-context` is a tool that helps you discover and install contextual information from Ruby gems for AI agents. Gems can provide additional documentation, examples, and guidance in a `context/` ...
|
16
|
+
|
17
|
+
### decode
|
18
|
+
|
19
|
+
Code analysis for documentation generation.
|
20
|
+
|
21
|
+
#### [Getting Started with Decode](.context/decode/getting-started.md)
|
22
|
+
|
23
|
+
The Decode gem provides programmatic access to Ruby code structure and metadata. It can parse Ruby files and extract definitions, comments, and documentation pragmas, enabling code analysis, docume...
|
24
|
+
|
25
|
+
#### [Documentation Coverage](.context/decode/coverage.md)
|
26
|
+
|
27
|
+
This guide explains how to test and monitor documentation coverage in your Ruby projects using the Decode gem's built-in bake tasks.
|
28
|
+
|
29
|
+
#### [Ruby Documentation](.context/decode/ruby-documentation.md)
|
30
|
+
|
31
|
+
This guide covers documentation practices and pragmas supported by the Decode gem for documenting Ruby code. These pragmas provide structured documentation that can be parsed and used to generate A...
|
32
|
+
|
33
|
+
### sus
|
34
|
+
|
35
|
+
A fast and scalable test runner.
|
36
|
+
|
37
|
+
#### [Using Sus Testing Framework](.context/sus/usage.md)
|
38
|
+
|
39
|
+
Sus is a modern Ruby testing framework that provides a clean, BDD-style syntax for writing tests. It's designed to be fast, simple, and expressive.
|
40
|
+
|
41
|
+
#### [Mocking](.context/sus/mocking.md)
|
42
|
+
|
43
|
+
There are two types of mocking in sus: `receive` and `mock`. The `receive` matcher is a subset of full mocking and is used to set expectations on method calls, while `mock` can be used to replace m...
|
44
|
+
|
45
|
+
#### [Shared Test Behaviors and Fixtures](.context/sus/shared.md)
|
46
|
+
|
47
|
+
Sus provides shared test contexts which can be used to define common behaviours or tests that can be reused across one or more test files.
|
data/lib/async/barrier.md
CHANGED
data/lib/async/barrier.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2019-
|
4
|
+
# Copyright, 2019-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "list"
|
7
7
|
require_relative "task"
|
8
|
+
require_relative "queue"
|
8
9
|
|
9
10
|
module Async
|
10
11
|
# A general purpose synchronisation primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore}.
|
@@ -16,6 +17,7 @@ module Async
|
|
16
17
|
# @public Since *Async v1*.
|
17
18
|
def initialize(parent: nil)
|
18
19
|
@tasks = List.new
|
20
|
+
@finished = Queue.new
|
19
21
|
|
20
22
|
@parent = parent
|
21
23
|
end
|
@@ -41,11 +43,15 @@ module Async
|
|
41
43
|
# Execute a child task and add it to the barrier.
|
42
44
|
# @asynchronous Executes the given block concurrently.
|
43
45
|
def async(*arguments, parent: (@parent or Task.current), **options, &block)
|
44
|
-
|
46
|
+
waiting = nil
|
45
47
|
|
46
|
-
|
47
|
-
|
48
|
-
|
48
|
+
parent.async(*arguments, **options) do |task, *arguments|
|
49
|
+
waiting = TaskNode.new(task)
|
50
|
+
@tasks.append(waiting)
|
51
|
+
block.call(task, *arguments)
|
52
|
+
ensure
|
53
|
+
@finished.signal(waiting)
|
54
|
+
end
|
49
55
|
end
|
50
56
|
|
51
57
|
# Whether there are any tasks being held by the barrier.
|
@@ -55,14 +61,27 @@ module Async
|
|
55
61
|
end
|
56
62
|
|
57
63
|
# Wait for all tasks to complete by invoking {Task#wait} on each waiting task, which may raise an error. As long as the task has completed, it will be removed from the barrier.
|
64
|
+
#
|
65
|
+
# @yields {|task| ...} If a block is given, the unwaited task is yielded. You must invoke {Task#wait} yourself. In addition, you may `break` if you have captured enough results.
|
66
|
+
#
|
58
67
|
# @asynchronous Will wait for tasks to finish executing.
|
59
68
|
def wait
|
60
|
-
|
69
|
+
while !@tasks.empty?
|
70
|
+
# Wait for a task to finish (we get the task node):
|
71
|
+
return unless waiting = @finished.wait
|
72
|
+
|
73
|
+
# Remove the task as it is now finishing:
|
74
|
+
@tasks.remove?(waiting)
|
75
|
+
|
76
|
+
# Get the task:
|
61
77
|
task = waiting.task
|
62
|
-
|
78
|
+
|
79
|
+
# If a block is given, the user can implement their own behaviour:
|
80
|
+
if block_given?
|
81
|
+
yield task
|
82
|
+
else
|
83
|
+
# Wait for it to either complete or raise an error:
|
63
84
|
task.wait
|
64
|
-
ensure
|
65
|
-
@tasks.remove?(waiting) unless task.alive?
|
66
85
|
end
|
67
86
|
end
|
68
87
|
end
|
@@ -73,6 +92,8 @@ module Async
|
|
73
92
|
@tasks.each do |waiting|
|
74
93
|
waiting.task.stop
|
75
94
|
end
|
95
|
+
|
96
|
+
@finished.close
|
76
97
|
end
|
77
98
|
end
|
78
99
|
end
|
data/lib/async/clock.rb
CHANGED
data/lib/async/condition.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-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2017, by Kent Gruber.
|
6
6
|
|
7
7
|
require "fiber"
|
@@ -42,6 +42,8 @@ module Async
|
|
42
42
|
|
43
43
|
# @deprecated Replaced by {#waiting?}
|
44
44
|
def empty?
|
45
|
+
warn("`Async::Condition#empty?` is deprecated, use `Async::Condition#waiting?` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
|
46
|
+
|
45
47
|
@waiting.empty?
|
46
48
|
end
|
47
49
|
|
data/lib/async/limited_queue.rb
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright,
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
5
5
|
|
6
6
|
# The implementation lives in `queue.rb` but later we may move it here for better autoload/inference.
|
7
7
|
require_relative "queue"
|
8
|
+
|
9
|
+
module Async
|
10
|
+
class LimitedQueue < Queue
|
11
|
+
singleton_class.remove_method(:new)
|
12
|
+
end
|
13
|
+
end
|
data/lib/async/list.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2022-
|
4
|
+
# Copyright, 2022-2025, by Samuel Williams.
|
5
|
+
# Copyright, 2025, by Shopify Inc.
|
5
6
|
|
6
7
|
module Async
|
7
8
|
# A general doublely linked list. This is used internally by {Async::Barrier} and {Async::Condition} to manage child tasks.
|
@@ -18,6 +19,7 @@ module Async
|
|
18
19
|
sprintf("#<%s:0x%x size=%d>", self.class.name, object_id, @size)
|
19
20
|
end
|
20
21
|
|
22
|
+
# @returns [String] A short summary of the list.
|
21
23
|
alias inspect to_s
|
22
24
|
|
23
25
|
# Fast, safe, unbounded accumulation of children.
|
@@ -134,7 +136,7 @@ module Async
|
|
134
136
|
return removed(node)
|
135
137
|
end
|
136
138
|
|
137
|
-
# @returns [Boolean]
|
139
|
+
# @returns [Boolean] True if the list is empty.
|
138
140
|
def empty?
|
139
141
|
@size == 0
|
140
142
|
end
|
@@ -143,26 +145,26 @@ module Async
|
|
143
145
|
# previous = self
|
144
146
|
# current = @tail
|
145
147
|
# found = node.equal?(self)
|
146
|
-
|
148
|
+
|
147
149
|
# while true
|
148
150
|
# break if current.equal?(self)
|
149
|
-
|
151
|
+
|
150
152
|
# if current.head != previous
|
151
153
|
# raise "Invalid previous linked list node!"
|
152
154
|
# end
|
153
|
-
|
155
|
+
|
154
156
|
# if current.is_a?(List) and !current.equal?(self)
|
155
157
|
# raise "Invalid list in list node!"
|
156
158
|
# end
|
157
|
-
|
159
|
+
|
158
160
|
# if node
|
159
161
|
# found ||= current.equal?(node)
|
160
162
|
# end
|
161
|
-
|
163
|
+
|
162
164
|
# previous = current
|
163
165
|
# current = current.tail
|
164
166
|
# end
|
165
|
-
|
167
|
+
|
166
168
|
# if node and !found
|
167
169
|
# raise "Node not found in list!"
|
168
170
|
# end
|
@@ -238,6 +240,12 @@ module Async
|
|
238
240
|
attr_accessor :head
|
239
241
|
attr_accessor :tail
|
240
242
|
|
243
|
+
# @returns [String] A string representation of the node.
|
244
|
+
def to_s
|
245
|
+
sprintf("#<%s:0x%x>", self.class.name, object_id)
|
246
|
+
end
|
247
|
+
|
248
|
+
# @returns [String] A string representation of the node.
|
241
249
|
alias inspect to_s
|
242
250
|
end
|
243
251
|
|
data/lib/async/node.rb
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
# Copyright, 2017-2024, by Samuel Williams.
|
5
5
|
# Copyright, 2017, by Kent Gruber.
|
6
6
|
# Copyright, 2022, by Shannon Skipper.
|
7
|
+
# Copyright, 2025, by Shopify Inc.
|
7
8
|
|
8
9
|
require "fiber/annotation"
|
9
10
|
|
@@ -180,6 +181,7 @@ module Async
|
|
180
181
|
"\#<#{self.description}>"
|
181
182
|
end
|
182
183
|
|
184
|
+
# @returns [String] A description of the node.
|
183
185
|
alias inspect to_s
|
184
186
|
|
185
187
|
# Change the parent of this node.
|
data/lib/async/notification.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "condition"
|
7
7
|
|
@@ -10,12 +10,14 @@ module Async
|
|
10
10
|
# @public Since *Async v1*.
|
11
11
|
class Notification < Condition
|
12
12
|
# Signal to a given task that it should resume operations.
|
13
|
+
#
|
14
|
+
# @returns [Boolean] if a task was signalled.
|
13
15
|
def signal(value = nil, task: Task.current)
|
14
|
-
return if @waiting.empty?
|
16
|
+
return false if @waiting.empty?
|
15
17
|
|
16
18
|
Fiber.scheduler.push Signal.new(self.exchange, value)
|
17
19
|
|
18
|
-
return
|
20
|
+
return true
|
19
21
|
end
|
20
22
|
|
21
23
|
Signal = Struct.new(:waiting, :value) do
|
data/lib/async/queue.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2019, by Ryan Musgrave.
|
6
6
|
# Copyright, 2020-2022, by Bruno Sutic.
|
7
|
+
# Copyright, 2025, by Jahfer Husain.
|
8
|
+
# Copyright, 2025, by Shopify Inc.
|
7
9
|
|
8
10
|
require_relative "notification"
|
9
11
|
|
@@ -14,16 +16,31 @@ module Async
|
|
14
16
|
#
|
15
17
|
# @public Since *Async v1*.
|
16
18
|
class Queue
|
19
|
+
# An error raised when trying to enqueue items to a closed queue.
|
20
|
+
# @public Since *Async v2.24*.
|
21
|
+
class ClosedError < RuntimeError
|
22
|
+
end
|
23
|
+
|
17
24
|
# Create a new queue.
|
18
25
|
#
|
19
26
|
# @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
|
20
27
|
# @parameter available [Notification] The notification to use for signaling when items are available.
|
21
28
|
def initialize(parent: nil, available: Notification.new)
|
22
29
|
@items = []
|
30
|
+
@closed = false
|
23
31
|
@parent = parent
|
24
32
|
@available = available
|
25
33
|
end
|
26
34
|
|
35
|
+
# Close the queue, causing all waiting tasks to return `nil`. Any subsequent calls to {enqueue} will raise an exception.
|
36
|
+
def close
|
37
|
+
@closed = true
|
38
|
+
|
39
|
+
while @available.waiting?
|
40
|
+
@available.signal(nil)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
27
44
|
# @attribute [Array] The items in the queue.
|
28
45
|
attr :items
|
29
46
|
|
@@ -39,6 +56,10 @@ module Async
|
|
39
56
|
|
40
57
|
# Add an item to the queue.
|
41
58
|
def push(item)
|
59
|
+
if @closed
|
60
|
+
raise ClosedError, "Cannot push items to a closed queue."
|
61
|
+
end
|
62
|
+
|
42
63
|
@items << item
|
43
64
|
|
44
65
|
@available.signal unless self.empty?
|
@@ -51,6 +72,10 @@ module Async
|
|
51
72
|
|
52
73
|
# Add multiple items to the queue.
|
53
74
|
def enqueue(*items)
|
75
|
+
if @closed
|
76
|
+
raise ClosedError, "Cannot enqueue items to a closed queue."
|
77
|
+
end
|
78
|
+
|
54
79
|
@items.concat(items)
|
55
80
|
|
56
81
|
@available.signal unless self.empty?
|
@@ -59,6 +84,10 @@ module Async
|
|
59
84
|
# Remove and return the next item from the queue.
|
60
85
|
def dequeue
|
61
86
|
while @items.empty?
|
87
|
+
if @closed
|
88
|
+
return nil
|
89
|
+
end
|
90
|
+
|
62
91
|
@available.wait
|
63
92
|
end
|
64
93
|
|
@@ -105,6 +134,13 @@ module Async
|
|
105
134
|
# A queue which limits the number of items that can be enqueued.
|
106
135
|
# @public Since *Async v1*.
|
107
136
|
class LimitedQueue < Queue
|
137
|
+
# @private This exists purely for emitting a warning.
|
138
|
+
def self.new(...)
|
139
|
+
warn("`require 'async/limited_queue'` to use `Async::LimitedQueue`.", uplevel: 1, category: :deprecated) if $VERBOSE
|
140
|
+
|
141
|
+
super
|
142
|
+
end
|
143
|
+
|
108
144
|
# Create a new limited queue.
|
109
145
|
#
|
110
146
|
# @parameter limit [Integer] The maximum number of items that can be enqueued.
|
@@ -119,9 +155,19 @@ module Async
|
|
119
155
|
# @attribute [Integer] The maximum number of items that can be enqueued.
|
120
156
|
attr :limit
|
121
157
|
|
158
|
+
# Close the queue, causing all waiting tasks to return `nil`. Any subsequent calls to {enqueue} will raise an exception.
|
159
|
+
# Also signals all tasks waiting for the queue to be full.
|
160
|
+
def close
|
161
|
+
super
|
162
|
+
|
163
|
+
while @full.waiting?
|
164
|
+
@full.signal(nil)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
122
168
|
# @returns [Boolean] Whether trying to enqueue an item would block.
|
123
169
|
def limited?
|
124
|
-
@items.size >= @limit
|
170
|
+
!@closed && @items.size >= @limit
|
125
171
|
end
|
126
172
|
|
127
173
|
# Add an item to the queue.
|
@@ -148,6 +194,10 @@ module Async
|
|
148
194
|
@full.wait
|
149
195
|
end
|
150
196
|
|
197
|
+
if @closed
|
198
|
+
raise ClosedError, "Cannot enqueue items to a closed queue."
|
199
|
+
end
|
200
|
+
|
151
201
|
available = @limit - @items.size
|
152
202
|
@items.concat(items.shift(available))
|
153
203
|
|
data/lib/async/reactor.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-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2017, by Kent Gruber.
|
6
6
|
# Copyright, 2018, by Sokolov Yura.
|
7
7
|
|
@@ -12,6 +12,8 @@ module Async
|
|
12
12
|
class Reactor < Scheduler
|
13
13
|
# @deprecated Replaced by {Kernel::Async}.
|
14
14
|
def self.run(...)
|
15
|
+
warn("`Async::Reactor.run{}` is deprecated, use `Async{}` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
|
16
|
+
|
15
17
|
Async(...)
|
16
18
|
end
|
17
19
|
|
data/lib/async/scheduler.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2020-
|
4
|
+
# Copyright, 2020-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2020, by Jun Jiang.
|
6
6
|
# Copyright, 2021, by Julien Portalier.
|
7
|
+
# Copyright, 2025, by Shopify Inc.
|
7
8
|
|
8
9
|
require_relative "clock"
|
9
10
|
require_relative "task"
|
10
11
|
require_relative "timeout"
|
11
|
-
require_relative "worker_pool"
|
12
12
|
|
13
13
|
require "io/event"
|
14
14
|
|
@@ -46,6 +46,28 @@ module Async
|
|
46
46
|
true
|
47
47
|
end
|
48
48
|
|
49
|
+
# Used to augment the scheduler to add support for blocking operations.
|
50
|
+
module BlockingOperationWait
|
51
|
+
# Wait for the given work to be executed.
|
52
|
+
#
|
53
|
+
# @public Since *Async v2.21* and *Ruby v3.4*.
|
54
|
+
# @asynchronous May be non-blocking.
|
55
|
+
#
|
56
|
+
# @parameter work [Proc] The work to execute on a background thread.
|
57
|
+
# @returns [Object] The result of the work.
|
58
|
+
def blocking_operation_wait(work)
|
59
|
+
@worker_pool.call(work)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private_constant :BlockingOperationWait
|
64
|
+
|
65
|
+
if ::IO::Event.const_defined?(:WorkerPool)
|
66
|
+
WorkerPool = ::IO::Event::WorkerPool
|
67
|
+
else
|
68
|
+
WorkerPool = nil
|
69
|
+
end
|
70
|
+
|
49
71
|
# Create a new scheduler.
|
50
72
|
#
|
51
73
|
# @public Since *Async v1*.
|
@@ -65,14 +87,15 @@ module Async
|
|
65
87
|
@idle_time = 0.0
|
66
88
|
|
67
89
|
@timers = ::IO::Event::Timers.new
|
90
|
+
|
68
91
|
if worker_pool == true
|
69
|
-
@worker_pool = WorkerPool
|
92
|
+
@worker_pool = WorkerPool&.new
|
70
93
|
else
|
71
94
|
@worker_pool = worker_pool
|
72
95
|
end
|
73
96
|
|
74
97
|
if @worker_pool
|
75
|
-
self.singleton_class.prepend(
|
98
|
+
self.singleton_class.prepend(BlockingOperationWait)
|
76
99
|
end
|
77
100
|
end
|
78
101
|
|
@@ -97,7 +120,7 @@ module Async
|
|
97
120
|
return @busy_time / total_time
|
98
121
|
end
|
99
122
|
end
|
100
|
-
|
123
|
+
|
101
124
|
# Invoked when the fiber scheduler is being closed.
|
102
125
|
#
|
103
126
|
# Executes the run loop until all tasks are finished, then closes the scheduler.
|
@@ -234,7 +257,7 @@ module Async
|
|
234
257
|
# @parameter blocker [Object] The object that was blocking the fiber.
|
235
258
|
# @parameter fiber [Fiber] The fiber to unblock.
|
236
259
|
def unblock(blocker, fiber)
|
237
|
-
# $stderr.puts "unblock(#{blocker}, #{fiber})"
|
260
|
+
# Fiber.blocking{$stderr.puts "unblock(#{blocker}, #{fiber})"}
|
238
261
|
|
239
262
|
# This operation is protected by the GVL:
|
240
263
|
if selector = @selector
|
@@ -250,6 +273,8 @@ module Async
|
|
250
273
|
#
|
251
274
|
# @parameter duration [Numeric | Nil] The time in seconds to sleep, or if nil, indefinitely.
|
252
275
|
def kernel_sleep(duration = nil)
|
276
|
+
# Fiber.blocking{$stderr.puts "kernel_sleep(#{duration}, #{Fiber.current})"}
|
277
|
+
|
253
278
|
if duration
|
254
279
|
self.block(nil, duration)
|
255
280
|
else
|
@@ -348,6 +373,34 @@ module Async
|
|
348
373
|
end
|
349
374
|
end
|
350
375
|
|
376
|
+
# Used to defer stopping the current task until later.
|
377
|
+
class FiberInterrupt
|
378
|
+
# Create a new stop later operation.
|
379
|
+
#
|
380
|
+
# @parameter task [Task] The task to stop later.
|
381
|
+
def initialize(fiber, exception)
|
382
|
+
@fiber = fiber
|
383
|
+
@exception = exception
|
384
|
+
end
|
385
|
+
|
386
|
+
# @returns [Boolean] Whether the task is alive.
|
387
|
+
def alive?
|
388
|
+
@fiber.alive?
|
389
|
+
end
|
390
|
+
|
391
|
+
# Transfer control to the operation - this will stop the task.
|
392
|
+
def transfer
|
393
|
+
# Fiber.blocking{$stderr.puts "FiberInterrupt#transfer(#{@fiber}, #{@exception})"}
|
394
|
+
@fiber.raise(@exception)
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# Raise an exception on the specified fiber, waking up the event loop if necessary.
|
399
|
+
def fiber_interrupt(fiber, exception)
|
400
|
+
# Fiber.blocking{$stderr.puts "fiber_interrupt(#{fiber}, #{exception})"}
|
401
|
+
unblock(nil, FiberInterrupt.new(fiber, exception))
|
402
|
+
end
|
403
|
+
|
351
404
|
# Wait for the specified process ID to exit.
|
352
405
|
#
|
353
406
|
# @public Since *Async v2*.
|
@@ -361,6 +414,19 @@ module Async
|
|
361
414
|
return @selector.process_wait(Fiber.current, pid, flags)
|
362
415
|
end
|
363
416
|
|
417
|
+
# Wait for the specified IOs to become ready for the specified events.
|
418
|
+
#
|
419
|
+
# @public Since *Async v2.25*.
|
420
|
+
# @asynchronous May be non-blocking.
|
421
|
+
def io_select(...)
|
422
|
+
Thread.new do
|
423
|
+
# Don't make unnecessary output, since we will propagate the exception:
|
424
|
+
Thread.current.report_on_exception = false
|
425
|
+
|
426
|
+
::IO.select(...)
|
427
|
+
end.value
|
428
|
+
end
|
429
|
+
|
364
430
|
# Run one iteration of the event loop.
|
365
431
|
#
|
366
432
|
# When terminating the event loop, we already know we are finished. So we don't need to check the task tree. This is a logical requirement because `run_once` ignores transient tasks. For example, a single top level transient task is not enough to keep the reactor running, but during termination we must still process it in order to terminate child tasks.
|
@@ -517,7 +583,7 @@ module Async
|
|
517
583
|
# @yields {|task| ...} Executed within the task.
|
518
584
|
# @returns [Task] The task that was scheduled into the reactor.
|
519
585
|
def async(*arguments, **options, &block)
|
520
|
-
|
586
|
+
warn("Async::Scheduler#async is deprecated. Use `run` or `Task#async` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
|
521
587
|
|
522
588
|
Kernel.raise ClosedError if @selector.nil?
|
523
589
|
|
@@ -528,6 +594,8 @@ module Async
|
|
528
594
|
return task
|
529
595
|
end
|
530
596
|
|
597
|
+
# Create a new fiber and return it without starting execution.
|
598
|
+
# @returns [Fiber] The fiber that was created.
|
531
599
|
def fiber(...)
|
532
600
|
return async(...).fiber
|
533
601
|
end
|
data/lib/async/stop.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
5
|
+
|
6
|
+
require "fiber"
|
7
|
+
require "console"
|
8
|
+
|
9
|
+
module Async
|
10
|
+
# Raised when a task is explicitly stopped.
|
11
|
+
class Stop < Exception
|
12
|
+
# Represents the source of the stop operation.
|
13
|
+
class Cause < Exception
|
14
|
+
if RUBY_VERSION >= "3.4"
|
15
|
+
# @returns [Array(Thread::Backtrace::Location)] The backtrace of the caller.
|
16
|
+
def self.backtrace
|
17
|
+
caller_locations(2..-1)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
# @returns [Array(String)] The backtrace of the caller.
|
21
|
+
def self.backtrace
|
22
|
+
caller(2..-1)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Create a new cause of the stop operation, with the given message.
|
27
|
+
#
|
28
|
+
# @parameter message [String] The error message.
|
29
|
+
# @returns [Cause] The cause of the stop operation.
|
30
|
+
def self.for(message = "Task was stopped")
|
31
|
+
instance = self.new(message)
|
32
|
+
instance.set_backtrace(self.backtrace)
|
33
|
+
return instance
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
if RUBY_VERSION < "3.5"
|
38
|
+
# Create a new stop operation.
|
39
|
+
#
|
40
|
+
# This is a compatibility method for Ruby versions before 3.5 where cause is not propagated correctly when using {Fiber#raise}
|
41
|
+
#
|
42
|
+
# @parameter message [String | Hash] The error message or a hash containing the cause.
|
43
|
+
def initialize(message = "Task was stopped")
|
44
|
+
if message.is_a?(Hash)
|
45
|
+
@cause = message[:cause]
|
46
|
+
message = "Task was stopped"
|
47
|
+
end
|
48
|
+
|
49
|
+
super(message)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @returns [Exception] The cause of the stop operation.
|
53
|
+
#
|
54
|
+
# This is a compatibility method for Ruby versions before 3.5 where cause is not propagated correctly when using {Fiber#raise}, we explicitly capture the cause here.
|
55
|
+
def cause
|
56
|
+
super || @cause
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Used to defer stopping the current task until later.
|
61
|
+
class Later
|
62
|
+
# Create a new stop later operation.
|
63
|
+
#
|
64
|
+
# @parameter task [Task] The task to stop later.
|
65
|
+
# @parameter cause [Exception] The cause of the stop operation.
|
66
|
+
def initialize(task, cause = nil)
|
67
|
+
@task = task
|
68
|
+
@cause = cause
|
69
|
+
end
|
70
|
+
|
71
|
+
# @returns [Boolean] Whether the task is alive.
|
72
|
+
def alive?
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
# Transfer control to the operation - this will stop the task.
|
77
|
+
def transfer
|
78
|
+
@task.stop(false, cause: @cause)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|