async 2.25.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/condition.rb +3 -1
- data/lib/async/limited_queue.rb +6 -0
- 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 +50 -1
- data/lib/async/reactor.rb +3 -1
- data/lib/async/scheduler.rb +11 -6
- data/lib/async/stop.rb +82 -0
- data/lib/async/task.rb +19 -31
- data/lib/async/version.rb +1 -1
- data/lib/async/waiter.rb +4 -1
- data/lib/traces/provider/async/barrier.rb +1 -1
- data/readme.md +17 -5
- data/releases.md +89 -2
- data.tar.gz.sig +0 -0
- metadata +4 -3
- metadata.gz.sig +0 -0
- data/lib/async/waiter.md +0 -50
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/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
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
@@ -5,6 +5,7 @@
|
|
5
5
|
# Copyright, 2019, by Ryan Musgrave.
|
6
6
|
# Copyright, 2020-2022, by Bruno Sutic.
|
7
7
|
# Copyright, 2025, by Jahfer Husain.
|
8
|
+
# Copyright, 2025, by Shopify Inc.
|
8
9
|
|
9
10
|
require_relative "notification"
|
10
11
|
|
@@ -15,16 +16,31 @@ module Async
|
|
15
16
|
#
|
16
17
|
# @public Since *Async v1*.
|
17
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
|
+
|
18
24
|
# Create a new queue.
|
19
25
|
#
|
20
26
|
# @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
|
21
27
|
# @parameter available [Notification] The notification to use for signaling when items are available.
|
22
28
|
def initialize(parent: nil, available: Notification.new)
|
23
29
|
@items = []
|
30
|
+
@closed = false
|
24
31
|
@parent = parent
|
25
32
|
@available = available
|
26
33
|
end
|
27
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
|
+
|
28
44
|
# @attribute [Array] The items in the queue.
|
29
45
|
attr :items
|
30
46
|
|
@@ -40,6 +56,10 @@ module Async
|
|
40
56
|
|
41
57
|
# Add an item to the queue.
|
42
58
|
def push(item)
|
59
|
+
if @closed
|
60
|
+
raise ClosedError, "Cannot push items to a closed queue."
|
61
|
+
end
|
62
|
+
|
43
63
|
@items << item
|
44
64
|
|
45
65
|
@available.signal unless self.empty?
|
@@ -52,6 +72,10 @@ module Async
|
|
52
72
|
|
53
73
|
# Add multiple items to the queue.
|
54
74
|
def enqueue(*items)
|
75
|
+
if @closed
|
76
|
+
raise ClosedError, "Cannot enqueue items to a closed queue."
|
77
|
+
end
|
78
|
+
|
55
79
|
@items.concat(items)
|
56
80
|
|
57
81
|
@available.signal unless self.empty?
|
@@ -60,6 +84,10 @@ module Async
|
|
60
84
|
# Remove and return the next item from the queue.
|
61
85
|
def dequeue
|
62
86
|
while @items.empty?
|
87
|
+
if @closed
|
88
|
+
return nil
|
89
|
+
end
|
90
|
+
|
63
91
|
@available.wait
|
64
92
|
end
|
65
93
|
|
@@ -106,6 +134,13 @@ module Async
|
|
106
134
|
# A queue which limits the number of items that can be enqueued.
|
107
135
|
# @public Since *Async v1*.
|
108
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
|
+
|
109
144
|
# Create a new limited queue.
|
110
145
|
#
|
111
146
|
# @parameter limit [Integer] The maximum number of items that can be enqueued.
|
@@ -120,9 +155,19 @@ module Async
|
|
120
155
|
# @attribute [Integer] The maximum number of items that can be enqueued.
|
121
156
|
attr :limit
|
122
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
|
+
|
123
168
|
# @returns [Boolean] Whether trying to enqueue an item would block.
|
124
169
|
def limited?
|
125
|
-
@items.size >= @limit
|
170
|
+
!@closed && @items.size >= @limit
|
126
171
|
end
|
127
172
|
|
128
173
|
# Add an item to the queue.
|
@@ -149,6 +194,10 @@ module Async
|
|
149
194
|
@full.wait
|
150
195
|
end
|
151
196
|
|
197
|
+
if @closed
|
198
|
+
raise ClosedError, "Cannot enqueue items to a closed queue."
|
199
|
+
end
|
200
|
+
|
152
201
|
available = @limit - @items.size
|
153
202
|
@items.concat(items.shift(available))
|
154
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
@@ -45,7 +45,7 @@ module Async
|
|
45
45
|
def self.supported?
|
46
46
|
true
|
47
47
|
end
|
48
|
-
|
48
|
+
|
49
49
|
# Used to augment the scheduler to add support for blocking operations.
|
50
50
|
module BlockingOperationWait
|
51
51
|
# Wait for the given work to be executed.
|
@@ -59,7 +59,7 @@ module Async
|
|
59
59
|
@worker_pool.call(work)
|
60
60
|
end
|
61
61
|
end
|
62
|
-
|
62
|
+
|
63
63
|
private_constant :BlockingOperationWait
|
64
64
|
|
65
65
|
if ::IO::Event.const_defined?(:WorkerPool)
|
@@ -67,7 +67,7 @@ module Async
|
|
67
67
|
else
|
68
68
|
WorkerPool = nil
|
69
69
|
end
|
70
|
-
|
70
|
+
|
71
71
|
# Create a new scheduler.
|
72
72
|
#
|
73
73
|
# @public Since *Async v1*.
|
@@ -93,7 +93,7 @@ module Async
|
|
93
93
|
else
|
94
94
|
@worker_pool = worker_pool
|
95
95
|
end
|
96
|
-
|
96
|
+
|
97
97
|
if @worker_pool
|
98
98
|
self.singleton_class.prepend(BlockingOperationWait)
|
99
99
|
end
|
@@ -120,7 +120,7 @@ module Async
|
|
120
120
|
return @busy_time / total_time
|
121
121
|
end
|
122
122
|
end
|
123
|
-
|
123
|
+
|
124
124
|
# Invoked when the fiber scheduler is being closed.
|
125
125
|
#
|
126
126
|
# Executes the run loop until all tasks are finished, then closes the scheduler.
|
@@ -420,6 +420,9 @@ module Async
|
|
420
420
|
# @asynchronous May be non-blocking.
|
421
421
|
def io_select(...)
|
422
422
|
Thread.new do
|
423
|
+
# Don't make unnecessary output, since we will propagate the exception:
|
424
|
+
Thread.current.report_on_exception = false
|
425
|
+
|
423
426
|
::IO.select(...)
|
424
427
|
end.value
|
425
428
|
end
|
@@ -580,7 +583,7 @@ module Async
|
|
580
583
|
# @yields {|task| ...} Executed within the task.
|
581
584
|
# @returns [Task] The task that was scheduled into the reactor.
|
582
585
|
def async(*arguments, **options, &block)
|
583
|
-
|
586
|
+
warn("Async::Scheduler#async is deprecated. Use `run` or `Task#async` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
|
584
587
|
|
585
588
|
Kernel.raise ClosedError if @selector.nil?
|
586
589
|
|
@@ -591,6 +594,8 @@ module Async
|
|
591
594
|
return task
|
592
595
|
end
|
593
596
|
|
597
|
+
# Create a new fiber and return it without starting execution.
|
598
|
+
# @returns [Fiber] The fiber that was created.
|
594
599
|
def fiber(...)
|
595
600
|
return async(...).fiber
|
596
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
|
data/lib/async/task.rb
CHANGED
@@ -13,33 +13,11 @@ require "console"
|
|
13
13
|
|
14
14
|
require_relative "node"
|
15
15
|
require_relative "condition"
|
16
|
+
require_relative "stop"
|
16
17
|
|
17
18
|
Fiber.attr_accessor :async_task
|
18
19
|
|
19
20
|
module Async
|
20
|
-
# Raised when a task is explicitly stopped.
|
21
|
-
class Stop < Exception
|
22
|
-
# Used to defer stopping the current task until later.
|
23
|
-
class Later
|
24
|
-
# Create a new stop later operation.
|
25
|
-
#
|
26
|
-
# @parameter task [Task] The task to stop later.
|
27
|
-
def initialize(task)
|
28
|
-
@task = task
|
29
|
-
end
|
30
|
-
|
31
|
-
# @returns [Boolean] Whether the task is alive.
|
32
|
-
def alive?
|
33
|
-
true
|
34
|
-
end
|
35
|
-
|
36
|
-
# Transfer control to the operation - this will stop the task.
|
37
|
-
def transfer
|
38
|
-
@task.stop
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
21
|
# Raised if a timeout occurs on a specific Fiber. Handled gracefully by `Task`.
|
44
22
|
# @public Since *Async v1*.
|
45
23
|
class TimeoutError < StandardError
|
@@ -65,6 +43,8 @@ module Async
|
|
65
43
|
|
66
44
|
# @deprecated With no replacement.
|
67
45
|
def self.yield
|
46
|
+
warn("`Async::Task.yield` is deprecated with no replacement.", uplevel: 1, category: :deprecated) if $VERBOSE
|
47
|
+
|
68
48
|
Fiber.scheduler.transfer
|
69
49
|
end
|
70
50
|
|
@@ -134,6 +114,8 @@ module Async
|
|
134
114
|
|
135
115
|
# @deprecated Prefer {Kernel#sleep} except when compatibility with `stable-v1` is required.
|
136
116
|
def sleep(duration = nil)
|
117
|
+
Kernel.warn("`Async::Task#sleep` is deprecated, use `Kernel#sleep` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
|
118
|
+
|
137
119
|
super
|
138
120
|
end
|
139
121
|
|
@@ -267,7 +249,13 @@ module Async
|
|
267
249
|
# If `later` is false, it means that `stop` has been invoked directly. When `later` is true, it means that `stop` is invoked by `stop_children` or some other indirect mechanism. In that case, if we encounter the "current" fiber, we can't stop it right away, as it's currently performing `#stop`. Stopping it immediately would interrupt the current stop traversal, so we need to schedule the stop to occur later.
|
268
250
|
#
|
269
251
|
# @parameter later [Boolean] Whether to stop the task later, or immediately.
|
270
|
-
|
252
|
+
# @parameter cause [Exception] The cause of the stop operation.
|
253
|
+
def stop(later = false, cause: $!)
|
254
|
+
# If no cause is given, we generate one from the current call stack:
|
255
|
+
unless cause
|
256
|
+
cause = Stop::Cause.for("Stopping task!")
|
257
|
+
end
|
258
|
+
|
271
259
|
if self.stopped?
|
272
260
|
# If the task is already stopped, a `stop` state transition re-enters the same state which is a no-op. However, we will also attempt to stop any running children too. This can happen if the children did not stop correctly the first time around. Doing this should probably be considered a bug, but it's better to be safe than sorry.
|
273
261
|
return stopped!
|
@@ -281,7 +269,7 @@ module Async
|
|
281
269
|
# If we are deferring stop...
|
282
270
|
if @defer_stop == false
|
283
271
|
# Don't stop now... but update the state so we know we need to stop later.
|
284
|
-
@defer_stop =
|
272
|
+
@defer_stop = cause
|
285
273
|
return false
|
286
274
|
end
|
287
275
|
|
@@ -289,19 +277,19 @@ module Async
|
|
289
277
|
# If the fiber is current, and later is `true`, we need to schedule the fiber to be stopped later, as it's currently invoking `stop`:
|
290
278
|
if later
|
291
279
|
# If the fiber is the current fiber and we want to stop it later, schedule it:
|
292
|
-
Fiber.scheduler.push(Stop::Later.new(self))
|
280
|
+
Fiber.scheduler.push(Stop::Later.new(self, cause))
|
293
281
|
else
|
294
282
|
# Otherwise, raise the exception directly:
|
295
|
-
raise Stop, "Stopping current task!"
|
283
|
+
raise Stop, "Stopping current task!", cause: cause
|
296
284
|
end
|
297
285
|
else
|
298
286
|
# If the fiber is not curent, we can raise the exception directly:
|
299
287
|
begin
|
300
288
|
# There is a chance that this will stop the fiber that originally called stop. If that happens, the exception handling in `#stopped` will rescue the exception and re-raise it later.
|
301
|
-
Fiber.scheduler.raise(@fiber, Stop)
|
289
|
+
Fiber.scheduler.raise(@fiber, Stop, cause: cause)
|
302
290
|
rescue FiberError
|
303
291
|
# In some cases, this can cause a FiberError (it might be resumed already), so we schedule it to be stopped later:
|
304
|
-
Fiber.scheduler.push(Stop::Later.new(self))
|
292
|
+
Fiber.scheduler.push(Stop::Later.new(self, cause))
|
305
293
|
end
|
306
294
|
end
|
307
295
|
else
|
@@ -341,7 +329,7 @@ module Async
|
|
341
329
|
|
342
330
|
# If we were asked to stop, we should do so now:
|
343
331
|
if defer_stop
|
344
|
-
raise Stop, "Stopping current task (was deferred)!"
|
332
|
+
raise Stop, "Stopping current task (was deferred)!", cause: defer_stop
|
345
333
|
end
|
346
334
|
end
|
347
335
|
else
|
@@ -352,7 +340,7 @@ module Async
|
|
352
340
|
|
353
341
|
# @returns [Boolean] Whether stop has been deferred.
|
354
342
|
def stop_deferred?
|
355
|
-
|
343
|
+
!!@defer_stop
|
356
344
|
end
|
357
345
|
|
358
346
|
# Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
|
data/lib/async/version.rb
CHANGED
data/lib/async/waiter.rb
CHANGED
@@ -1,17 +1,20 @@
|
|
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
5
|
# Copyright, 2024, by Patrik Wenger.
|
6
6
|
|
7
7
|
module Async
|
8
8
|
# A composable synchronization primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore} and/or {Barrier}.
|
9
|
+
# @deprecated `Async::Waiter` is deprecated, use `Async::Barrier` instead.
|
9
10
|
class Waiter
|
10
11
|
# Create a waiter instance.
|
11
12
|
#
|
12
13
|
# @parameter parent [Interface(:async) | Nil] The parent task to use for asynchronous operations.
|
13
14
|
# @parameter finished [Async::Condition] The condition to signal when a task completes.
|
14
15
|
def initialize(parent: nil, finished: Async::Condition.new)
|
16
|
+
warn("`Async::Waiter` is deprecated, use `Async::Barrier` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
|
17
|
+
|
15
18
|
@finished = finished
|
16
19
|
@done = []
|
17
20
|
|
data/readme.md
CHANGED
@@ -35,6 +35,22 @@ 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.27.0
|
39
|
+
|
40
|
+
- `Async::Task#stop` supports an optional `cause:` argument (that defaults to `$!`), which allows you to specify the cause (exception) for stopping the task.
|
41
|
+
- Add thread-safety agent context.
|
42
|
+
|
43
|
+
### v2.26.0
|
44
|
+
|
45
|
+
- `Async::Notification#signal` now returns `true` if a task was signaled, `false` otherwise, providing better feedback for notification operations.
|
46
|
+
- `require "async/limited_queue"` is required to use `Async::LimitedQueue` without a deprecation warning. `Async::LimitedQueue` is not deprecated, but it's usage via `async/queue` is deprecated.
|
47
|
+
- `Async::Task#sleep` is deprecated with no replacement.
|
48
|
+
- `Async::Task.yield` is deprecated with no replacement.
|
49
|
+
- `Async::Scheduler#async` is deprecated, use `Async{}`, `Sync{}` or `Async::Task#async` instead.
|
50
|
+
- Agent context is now available, via the [`agent-context` gem](https://github.com/ioquatix/agent-context).
|
51
|
+
- [`Async::Barrier` Improvements](https://socketry.github.io/async/releases/index#async::barrier-improvements)
|
52
|
+
- [Introduce `Async::Queue#close`](https://socketry.github.io/async/releases/index#introduce-async::queue#close)
|
53
|
+
|
38
54
|
### v2.25.0
|
39
55
|
|
40
56
|
- Added support for `io_select` hook in the fiber scheduler, allowing non-blocking `IO.select` operations. This enables better integration with code that uses `IO.select` for multiplexing IO operations.
|
@@ -62,7 +78,7 @@ Please see the [project releases](https://socketry.github.io/async/releases/inde
|
|
62
78
|
|
63
79
|
### v2.19.0
|
64
80
|
|
65
|
-
- [Async::Scheduler Debugging](https://socketry.github.io/async/releases/index#async::scheduler-debugging)
|
81
|
+
- [`Async::Scheduler` Debugging](https://socketry.github.io/async/releases/index#async::scheduler-debugging)
|
66
82
|
- [Console Shims](https://socketry.github.io/async/releases/index#console-shims)
|
67
83
|
|
68
84
|
### v2.18.0
|
@@ -73,10 +89,6 @@ Please see the [project releases](https://socketry.github.io/async/releases/inde
|
|
73
89
|
|
74
90
|
- Introduce `Async::Queue#push` and `Async::Queue#pop` for compatibility with `::Queue`.
|
75
91
|
|
76
|
-
### v2.16.0
|
77
|
-
|
78
|
-
- [Better Handling of Async and Sync in Nested Fibers](https://socketry.github.io/async/releases/index#better-handling-of-async-and-sync-in-nested-fibers)
|
79
|
-
|
80
92
|
## See Also
|
81
93
|
|
82
94
|
- [async-http](https://github.com/socketry/async-http) — Asynchronous HTTP client/server.
|
data/releases.md
CHANGED
@@ -1,5 +1,92 @@
|
|
1
1
|
# Releases
|
2
2
|
|
3
|
+
## v2.27.0
|
4
|
+
|
5
|
+
- `Async::Task#stop` supports an optional `cause:` argument (that defaults to `$!`), which allows you to specify the cause (exception) for stopping the task.
|
6
|
+
- Add thread-safety agent context.
|
7
|
+
|
8
|
+
## v2.26.0
|
9
|
+
|
10
|
+
- `Async::Notification#signal` now returns `true` if a task was signaled, `false` otherwise, providing better feedback for notification operations.
|
11
|
+
- `require "async/limited_queue"` is required to use `Async::LimitedQueue` without a deprecation warning. `Async::LimitedQueue` is not deprecated, but it's usage via `async/queue` is deprecated.
|
12
|
+
- `Async::Task#sleep` is deprecated with no replacement.
|
13
|
+
- `Async::Task.yield` is deprecated with no replacement.
|
14
|
+
- `Async::Scheduler#async` is deprecated, use `Async{}`, `Sync{}` or `Async::Task#async` instead.
|
15
|
+
- Agent context is now available, via the [`agent-context` gem](https://github.com/ioquatix/agent-context).
|
16
|
+
|
17
|
+
### `Async::Barrier` Improvements
|
18
|
+
|
19
|
+
`Async::Barrier` now provides more flexible and predictable behavior for waiting on task completion:
|
20
|
+
|
21
|
+
- **Completion-order waiting**: `barrier.wait` now processes tasks in the order they complete rather than the order they were created. This provides more predictable behavior when tasks have different execution times.
|
22
|
+
- **Block-based waiting**: `barrier.wait` now accepts an optional block that yields each task as it completes, allowing for custom handling of individual tasks:
|
23
|
+
|
24
|
+
<!-- end list -->
|
25
|
+
|
26
|
+
``` ruby
|
27
|
+
barrier = Async::Barrier.new
|
28
|
+
|
29
|
+
# Start several tasks
|
30
|
+
3.times do |i|
|
31
|
+
barrier.async do |task|
|
32
|
+
sleep(rand * 0.1) # Random completion time
|
33
|
+
"result_#{i}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Wait for all tasks, processing them as they complete
|
38
|
+
barrier.wait do |task|
|
39
|
+
result = task.wait
|
40
|
+
puts "Task completed with: #{result}"
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
- **Partial completion support**: The new block-based interface allows you to wait for only the first N tasks to complete:
|
45
|
+
|
46
|
+
<!-- end list -->
|
47
|
+
|
48
|
+
``` ruby
|
49
|
+
# Wait for only the first 3 tasks to complete
|
50
|
+
count = 0
|
51
|
+
barrier.wait do |task|
|
52
|
+
task.wait
|
53
|
+
count += 1
|
54
|
+
break if count >= 3
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
This makes `Async::Barrier` a superset of `Async::Waiter` functionality, providing more flexible task coordination patterns, and therrefore, `Async::Waiter` is now deprecated.
|
59
|
+
|
60
|
+
### Introduce `Async::Queue#close`
|
61
|
+
|
62
|
+
`Async::Queue` and `Async::LimitedQueue` can now be closed, which provides better resource management and error handling:
|
63
|
+
|
64
|
+
- **New `close` method**: Both queue types now have a `close` method that prevents further items from being added and signals any waiting tasks.
|
65
|
+
- **Consistent error handling**: All queue modification methods (`push`, `enqueue`, `<<`) now raise `Async::Queue::ClosedError` when called on a closed queue.
|
66
|
+
- **Waiting task signaling**: When a queue is closed, any tasks waiting on `dequeue` (for regular queues) or `enqueue` (for limited queues) are properly signaled and can complete.
|
67
|
+
|
68
|
+
<!-- end list -->
|
69
|
+
|
70
|
+
``` ruby
|
71
|
+
queue = Async::Queue.new
|
72
|
+
|
73
|
+
# Start a task waiting for items:
|
74
|
+
waiting_task = Async do
|
75
|
+
queue.dequeue
|
76
|
+
end
|
77
|
+
|
78
|
+
# Close the queue - this signals the waiting task
|
79
|
+
queue.close
|
80
|
+
|
81
|
+
# These will raise Async::Queue::ClosedError
|
82
|
+
queue.push(:item) # => raises ClosedError
|
83
|
+
queue.enqueue(:item) # => raises ClosedError
|
84
|
+
queue << :item # => raises ClosedError
|
85
|
+
|
86
|
+
# Dequeue returns nil when closed and empty
|
87
|
+
queue.dequeue # => nil
|
88
|
+
```
|
89
|
+
|
3
90
|
## v2.25.0
|
4
91
|
|
5
92
|
- Added support for `io_select` hook in the fiber scheduler, allowing non-blocking `IO.select` operations. This enables better integration with code that uses `IO.select` for multiplexing IO operations.
|
@@ -34,7 +121,7 @@ end
|
|
34
121
|
|
35
122
|
### Flexible Timeouts
|
36
123
|
|
37
|
-
When
|
124
|
+
When `Async::Scheduler#with_timeout` is invoked with a block, it can receive a `Async::Timeout` instance. This allows you to adjust or cancel the timeout while the block is executing. This is useful for long-running tasks that may need to adjust their timeout based on external factors.
|
38
125
|
|
39
126
|
``` ruby
|
40
127
|
Async do
|
@@ -108,7 +195,7 @@ To take advantage of this feature, you will need to introduce your own `config/t
|
|
108
195
|
|
109
196
|
## v2.19.0
|
110
197
|
|
111
|
-
### Async::Scheduler Debugging
|
198
|
+
### `Async::Scheduler` Debugging
|
112
199
|
|
113
200
|
Occasionally on issues, I encounter people asking for help and I need more information. Pressing Ctrl-C to exit a hung program is common, but it usually doesn't provide enough information to diagnose the problem. Setting the `CONSOLE_LEVEL=debug` environment variable will now print additional information about the scheduler when you interrupt it, including a backtrace of the current tasks.
|
114
201
|
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.27.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
|
+
- Shopify Inc.
|
8
9
|
- Bruno Sutic
|
9
10
|
- Jeremy Jung
|
10
11
|
- Olle Jonsson
|
@@ -32,7 +33,6 @@ authors:
|
|
32
33
|
- Salim Semaoune
|
33
34
|
- Shannon Skipper
|
34
35
|
- Shigeru Nakajima
|
35
|
-
- Shopify Inc.
|
36
36
|
- Sokolov Yura
|
37
37
|
- Stefan Wrobel
|
38
38
|
- Trevor Turk
|
@@ -143,6 +143,7 @@ executables: []
|
|
143
143
|
extensions: []
|
144
144
|
extra_rdoc_files: []
|
145
145
|
files:
|
146
|
+
- agent.md
|
146
147
|
- lib/async.rb
|
147
148
|
- lib/async/barrier.md
|
148
149
|
- lib/async/barrier.rb
|
@@ -160,12 +161,12 @@ files:
|
|
160
161
|
- lib/async/scheduler.rb
|
161
162
|
- lib/async/semaphore.md
|
162
163
|
- lib/async/semaphore.rb
|
164
|
+
- lib/async/stop.rb
|
163
165
|
- lib/async/task.md
|
164
166
|
- lib/async/task.rb
|
165
167
|
- lib/async/timeout.rb
|
166
168
|
- lib/async/variable.rb
|
167
169
|
- lib/async/version.rb
|
168
|
-
- lib/async/waiter.md
|
169
170
|
- lib/async/waiter.rb
|
170
171
|
- lib/kernel/async.rb
|
171
172
|
- lib/kernel/sync.rb
|
metadata.gz.sig
CHANGED
Binary file
|
data/lib/async/waiter.md
DELETED
@@ -1,50 +0,0 @@
|
|
1
|
-
A synchronization primitive, which allows you to wait for tasks to complete in order of completion. This is useful for implementing a task pool, where you want to wait for the first task to complete, and then cancel the rest.
|
2
|
-
|
3
|
-
If you try to wait for more things than you have added, you will deadlock.
|
4
|
-
|
5
|
-
## Example
|
6
|
-
|
7
|
-
~~~ ruby
|
8
|
-
require 'async'
|
9
|
-
require 'async/semaphore'
|
10
|
-
require 'async/barrier'
|
11
|
-
require 'async/waiter'
|
12
|
-
|
13
|
-
Sync do
|
14
|
-
barrier = Async::Barrier.new
|
15
|
-
waiter = Async::Waiter.new(parent: barrier)
|
16
|
-
semaphore = Async::Semaphore.new(2, parent: waiter)
|
17
|
-
|
18
|
-
# Sleep sort the numbers:
|
19
|
-
generator = Async do
|
20
|
-
while true
|
21
|
-
semaphore.async do |task|
|
22
|
-
number = rand(1..10)
|
23
|
-
sleep(number)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
numbers = []
|
29
|
-
|
30
|
-
4.times do
|
31
|
-
# Wait for all the numbers to be sorted:
|
32
|
-
numbers << waiter.wait
|
33
|
-
end
|
34
|
-
|
35
|
-
# Don't generate any more numbers:
|
36
|
-
generator.stop
|
37
|
-
|
38
|
-
# Stop all tasks which we don't care about:
|
39
|
-
barrier.stop
|
40
|
-
|
41
|
-
Console.info("Smallest", numbers)
|
42
|
-
end
|
43
|
-
~~~
|
44
|
-
|
45
|
-
### Output
|
46
|
-
|
47
|
-
~~~
|
48
|
-
0.0s info: Smallest
|
49
|
-
| [3, 3, 1, 2]
|
50
|
-
~~~
|