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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44c11aa8f74922b9bc2ccce792fd3c419af4a3a261f97f2f2202e3d75a5bf8f5
4
- data.tar.gz: e2327b997b32a42e2ebfe9a4e4950d26a12ce698ce34a89d9fe1be94b0241b1f
3
+ metadata.gz: 4c9d36d758f8a197b7c00d3d2e4d83d280cfd8ddf4fb60bba1d7b9e95b64ec47
4
+ data.tar.gz: c0c19dcc509563b18a9385c1ff07a982054e6e984c9ed9c1fd715ee027c74f09
5
5
  SHA512:
6
- metadata.gz: 20961f535e5368753dffdb760d9bddf0fcf55e5910e5f6c71dba28ebb0405221d9328a8bfe5ef4e688ead71b4b5c4bde3949ce1edfa013b06af7db57adfe6d7f
7
- data.tar.gz: d58c654852cd650978fa14fef7eaba00f4784137075960e5ad5b0b6b20ae3523912578b32b660f7d107df318cdd34945124e83231c4e574e336c5c4862a6c87c
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
@@ -1,6 +1,5 @@
1
1
  A synchronization primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore}.
2
2
 
3
-
4
3
  ## Example
5
4
 
6
5
  ~~~ ruby
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-2024, by Samuel Williams.
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
- task = parent.async(*arguments, **options, &block)
46
+ waiting = nil
45
47
 
46
- @tasks.append(TaskNode.new(task))
47
-
48
- return task
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
- @tasks.each do |waiting|
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
- begin
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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2024, by Samuel Williams.
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
 
@@ -5,3 +5,9 @@
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-2024, by Samuel Williams.
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] Returns true if the list is empty.
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.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2024, by Samuel Williams.
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 nil
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-2024, by Samuel Williams.
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
 
@@ -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
- # warn "Async::Scheduler#async is deprecated. Use `run` or `Task#async` instead.", uplevel: 1, category: :deprecated
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
- def stop(later = false)
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 = true
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
- @defer_stop
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
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2017-2025, by Samuel Williams.
5
5
 
6
6
  module Async
7
- VERSION = "2.25.0"
7
+ VERSION = "2.27.0"
8
8
  end
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-2024, by Samuel Williams.
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
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024, by Samuel Williams.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "../../../async/barrier"
7
7
  require "traces/provider"
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 {ruby Async::Scheduler\#with\_timeout} is invoked with a block, it can receive a {ruby 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.
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.25.0
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
- ~~~