async 2.24.0 → 2.26.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: 95c1315007ff80d78bdd1538d2b2963ed951db5aa92906ea6f22560612540cbc
4
- data.tar.gz: 03ffadcf7c2523827bd6c49340df0a846f8ab3214593d897b4359b548a765cef
3
+ metadata.gz: 9c593ddf06be9954e63b4f85eb8c0fe022be6cf1e241a37f86620a82fdf860b3
4
+ data.tar.gz: 023b1544a36d3bb8ff85f0b8cc3ccc0b4694ed3b0542ed7dafd02330f05ee663
5
5
  SHA512:
6
- metadata.gz: 62c15e0c19e7f3277ca9e4981b0304d5f039367fb0d6b996a8c562be170b3436f66e97f33fcfd5eb0ed8ec84e760ae2b565a022604e026f3b2c797613534fd59
7
- data.tar.gz: 79c7148d35f3e06243b3845721b8a1ebcf5fd2e6244eab18ac5a0f42894088334df307c4da52d9d9c59b0d9499e457f0990022d570abf109202416cf168211a1
6
+ metadata.gz: 65c3fee1ef33362307c4c74dd2451384de1e1747041ffd68cec0049465a317dada93083198254b905e89758a112800894436fe4cc8a2c713b0c0f5de1a36402e
7
+ data.tar.gz: a90fa15643b4a491599e95429c0d79da4363d0778455a50893ee9d6f34217ebd442bf99085f981807e024dfec561f545ea86d74e06f2874b34c1bd191a1b79fa
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
data/lib/async/clock.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2022, by Samuel Williams.
4
+ # Copyright, 2018-2025, by Samuel Williams.
5
5
 
6
6
  module Async
7
7
  # A convenient wrapper around the internal monotonic clock.
@@ -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
 
@@ -1,7 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024, by Samuel Williams.
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,7 @@
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
 
6
6
  module Async
7
7
  # A general doublely linked list. This is used internally by {Async::Barrier} and {Async::Condition} to manage child tasks.
@@ -18,6 +18,7 @@ module Async
18
18
  sprintf("#<%s:0x%x size=%d>", self.class.name, object_id, @size)
19
19
  end
20
20
 
21
+ # @returns [String] A short summary of the list.
21
22
  alias inspect to_s
22
23
 
23
24
  # Fast, safe, unbounded accumulation of children.
@@ -134,7 +135,7 @@ module Async
134
135
  return removed(node)
135
136
  end
136
137
 
137
- # @returns [Boolean] Returns true if the list is empty.
138
+ # @returns [Boolean] True if the list is empty.
138
139
  def empty?
139
140
  @size == 0
140
141
  end
@@ -238,6 +239,12 @@ module Async
238
239
  attr_accessor :head
239
240
  attr_accessor :tail
240
241
 
242
+ # @returns [String] A string representation of the node.
243
+ def to_s
244
+ sprintf("#<%s:0x%x>", self.class.name, object_id)
245
+ end
246
+
247
+ # @returns [String] A string representation of the node.
241
248
  alias inspect to_s
242
249
  end
243
250
 
data/lib/async/node.rb CHANGED
@@ -180,6 +180,7 @@ module Async
180
180
  "\#<#{self.description}>"
181
181
  end
182
182
 
183
+ # @returns [String] A description of the node.
183
184
  alias inspect to_s
184
185
 
185
186
  # 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
@@ -1,9 +1,10 @@
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
  # Copyright, 2019, by Ryan Musgrave.
6
6
  # Copyright, 2020-2022, by Bruno Sutic.
7
+ # Copyright, 2025, by Jahfer Husain.
7
8
 
8
9
  require_relative "notification"
9
10
 
@@ -14,16 +15,31 @@ module Async
14
15
  #
15
16
  # @public Since *Async v1*.
16
17
  class Queue
18
+ # An error raised when trying to enqueue items to a closed queue.
19
+ # @public Since *Async v2.24*.
20
+ class ClosedError < RuntimeError
21
+ end
22
+
17
23
  # Create a new queue.
18
24
  #
19
25
  # @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
20
26
  # @parameter available [Notification] The notification to use for signaling when items are available.
21
27
  def initialize(parent: nil, available: Notification.new)
22
28
  @items = []
29
+ @closed = false
23
30
  @parent = parent
24
31
  @available = available
25
32
  end
26
33
 
34
+ # Close the queue, causing all waiting tasks to return `nil`. Any subsequent calls to {enqueue} will raise an exception.
35
+ def close
36
+ @closed = true
37
+
38
+ while @available.waiting?
39
+ @available.signal(nil)
40
+ end
41
+ end
42
+
27
43
  # @attribute [Array] The items in the queue.
28
44
  attr :items
29
45
 
@@ -39,6 +55,10 @@ module Async
39
55
 
40
56
  # Add an item to the queue.
41
57
  def push(item)
58
+ if @closed
59
+ raise ClosedError, "Cannot push items to a closed queue."
60
+ end
61
+
42
62
  @items << item
43
63
 
44
64
  @available.signal unless self.empty?
@@ -51,6 +71,10 @@ module Async
51
71
 
52
72
  # Add multiple items to the queue.
53
73
  def enqueue(*items)
74
+ if @closed
75
+ raise ClosedError, "Cannot enqueue items to a closed queue."
76
+ end
77
+
54
78
  @items.concat(items)
55
79
 
56
80
  @available.signal unless self.empty?
@@ -59,6 +83,10 @@ module Async
59
83
  # Remove and return the next item from the queue.
60
84
  def dequeue
61
85
  while @items.empty?
86
+ if @closed
87
+ return nil
88
+ end
89
+
62
90
  @available.wait
63
91
  end
64
92
 
@@ -105,6 +133,13 @@ module Async
105
133
  # A queue which limits the number of items that can be enqueued.
106
134
  # @public Since *Async v1*.
107
135
  class LimitedQueue < Queue
136
+ # @private This exists purely for emitting a warning.
137
+ def self.new(...)
138
+ warn("`require 'async/limited_queue'` to use `Async::LimitedQueue`.", uplevel: 1, category: :deprecated) if $VERBOSE
139
+
140
+ super
141
+ end
142
+
108
143
  # Create a new limited queue.
109
144
  #
110
145
  # @parameter limit [Integer] The maximum number of items that can be enqueued.
@@ -119,9 +154,19 @@ module Async
119
154
  # @attribute [Integer] The maximum number of items that can be enqueued.
120
155
  attr :limit
121
156
 
157
+ # Close the queue, causing all waiting tasks to return `nil`. Any subsequent calls to {enqueue} will raise an exception.
158
+ # Also signals all tasks waiting for the queue to be full.
159
+ def close
160
+ super
161
+
162
+ while @full.waiting?
163
+ @full.signal(nil)
164
+ end
165
+ end
166
+
122
167
  # @returns [Boolean] Whether trying to enqueue an item would block.
123
168
  def limited?
124
- @items.size >= @limit
169
+ !@closed && @items.size >= @limit
125
170
  end
126
171
 
127
172
  # Add an item to the queue.
@@ -148,6 +193,10 @@ module Async
148
193
  @full.wait
149
194
  end
150
195
 
196
+ if @closed
197
+ raise ClosedError, "Cannot enqueue items to a closed queue."
198
+ end
199
+
151
200
  available = @limit - @items.size
152
201
  @items.concat(items.shift(available))
153
202
 
data/lib/async/reactor.rb CHANGED
@@ -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
 
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2024, by Samuel Williams.
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
 
@@ -45,7 +45,29 @@ module Async
45
45
  def self.supported?
46
46
  true
47
47
  end
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
48
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.new
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(WorkerPool::BlockingOperationWait)
98
+ self.singleton_class.prepend(BlockingOperationWait)
76
99
  end
77
100
  end
78
101
 
@@ -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
- # 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
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/task.rb CHANGED
@@ -1,11 +1,12 @@
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, 2017, by Devin Christensen.
7
7
  # Copyright, 2020, by Patrik Wenger.
8
8
  # Copyright, 2023, by Math Ieu.
9
+ # Copyright, 2025, by Shigeru Nakajima.
9
10
 
10
11
  require "fiber"
11
12
  require "console"
@@ -64,6 +65,8 @@ module Async
64
65
 
65
66
  # @deprecated With no replacement.
66
67
  def self.yield
68
+ warn("`Async::Task.yield` is deprecated with no replacement.", uplevel: 1, category: :deprecated) if $VERBOSE
69
+
67
70
  Fiber.scheduler.transfer
68
71
  end
69
72
 
@@ -133,6 +136,8 @@ module Async
133
136
 
134
137
  # @deprecated Prefer {Kernel#sleep} except when compatibility with `stable-v1` is required.
135
138
  def sleep(duration = nil)
139
+ Kernel.warn("`Async::Task#sleep` is deprecated, use `Kernel#sleep` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
140
+
136
141
  super
137
142
  end
138
143
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2024, by Samuel Williams.
4
+ # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "condition"
7
7
 
data/lib/async/version.rb CHANGED
@@ -1,8 +1,8 @@
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
 
6
6
  module Async
7
- VERSION = "2.24.0"
7
+ VERSION = "2.26.0"
8
8
  end
data/lib/async/waiter.rb CHANGED
@@ -6,12 +6,15 @@
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/task"
7
7
  require "metrics/provider"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022, by Samuel Williams.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "../../../async/barrier"
7
7
  require "traces/provider"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022, by Samuel Williams.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "../../../async/task"
7
7
  require "traces/provider"
data/license.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2017-2024, by Samuel Williams.
3
+ Copyright, 2017-2025, by Samuel Williams.
4
4
  Copyright, 2017, by Kent Gruber.
5
5
  Copyright, 2017, by Devin Christensen.
6
6
  Copyright, 2018, by Sokolov Yura.
@@ -27,6 +27,11 @@ Copyright, 2023, by Emil Tin.
27
27
  Copyright, 2023, by Gert Goet.
28
28
  Copyright, 2024, by Dimitar Peychinov.
29
29
  Copyright, 2024, by Jamie McCarthy.
30
+ Copyright, 2025, by Jahfer Husain.
31
+ Copyright, 2025, by Mark Montroy.
32
+ Copyright, 2025, by Shigeru Nakajima.
33
+ Copyright, 2025, by Alan Wu.
34
+ Copyright, 2025, by Shopify Inc.
30
35
 
31
36
  Permission is hereby granted, free of charge, to any person obtaining a copy
32
37
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -35,6 +35,23 @@ 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.26.0
39
+
40
+ - `Async::Notification#signal` now returns `true` if a task was signaled, `false` otherwise, providing better feedback for notification operations.
41
+ - `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.
42
+ - `Async::Task#sleep` is deprecated with no replacement.
43
+ - `Async::Task.yield` is deprecated with no replacement.
44
+ - `Async::Scheduler#async` is deprecated, use `Async{}`, `Sync{}` or `Async::Task#async` instead.
45
+ - Agent context is now available, via the [`agent-context` gem](https://github.com/ioquatix/agent-context).
46
+ - [`Async::Barrier` Improvements](https://socketry.github.io/async/releases/index#async::barrier-improvements)
47
+ - [Introduce `Async::Queue#close`](https://socketry.github.io/async/releases/index#introduce-async::queue#close)
48
+
49
+ ### v2.25.0
50
+
51
+ - 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.
52
+ - [Use `IO::Event::WorkerPool` for Blocking Operations](https://socketry.github.io/async/releases/index#use-io::event::workerpool-for-blocking-operations)
53
+ - [Better handling of `IO#close` using `fiber_interrupt`](https://socketry.github.io/async/releases/index#better-handling-of-io#close-using-fiber_interrupt)
54
+
38
55
  ### v2.24.0
39
56
 
40
57
  - Ruby v3.1 support is dropped.
@@ -56,7 +73,7 @@ Please see the [project releases](https://socketry.github.io/async/releases/inde
56
73
 
57
74
  ### v2.19.0
58
75
 
59
- - [Async::Scheduler Debugging](https://socketry.github.io/async/releases/index#async::scheduler-debugging)
76
+ - [`Async::Scheduler` Debugging](https://socketry.github.io/async/releases/index#async::scheduler-debugging)
60
77
  - [Console Shims](https://socketry.github.io/async/releases/index#console-shims)
61
78
 
62
79
  ### v2.18.0
data/releases.md CHANGED
@@ -1,5 +1,114 @@
1
1
  # Releases
2
2
 
3
+ ## v2.26.0
4
+
5
+ - `Async::Notification#signal` now returns `true` if a task was signaled, `false` otherwise, providing better feedback for notification operations.
6
+ - `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.
7
+ - `Async::Task#sleep` is deprecated with no replacement.
8
+ - `Async::Task.yield` is deprecated with no replacement.
9
+ - `Async::Scheduler#async` is deprecated, use `Async{}`, `Sync{}` or `Async::Task#async` instead.
10
+ - Agent context is now available, via the [`agent-context` gem](https://github.com/ioquatix/agent-context).
11
+
12
+ ### `Async::Barrier` Improvements
13
+
14
+ `Async::Barrier` now provides more flexible and predictable behavior for waiting on task completion:
15
+
16
+ - **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.
17
+ - **Block-based waiting**: `barrier.wait` now accepts an optional block that yields each task as it completes, allowing for custom handling of individual tasks:
18
+
19
+ <!-- end list -->
20
+
21
+ ``` ruby
22
+ barrier = Async::Barrier.new
23
+
24
+ # Start several tasks
25
+ 3.times do |i|
26
+ barrier.async do |task|
27
+ sleep(rand * 0.1) # Random completion time
28
+ "result_#{i}"
29
+ end
30
+ end
31
+
32
+ # Wait for all tasks, processing them as they complete
33
+ barrier.wait do |task|
34
+ result = task.wait
35
+ puts "Task completed with: #{result}"
36
+ end
37
+ ```
38
+
39
+ - **Partial completion support**: The new block-based interface allows you to wait for only the first N tasks to complete:
40
+
41
+ <!-- end list -->
42
+
43
+ ``` ruby
44
+ # Wait for only the first 3 tasks to complete
45
+ count = 0
46
+ barrier.wait do |task|
47
+ task.wait
48
+ count += 1
49
+ break if count >= 3
50
+ end
51
+ ```
52
+
53
+ This makes `Async::Barrier` a superset of `Async::Waiter` functionality, providing more flexible task coordination patterns, and therrefore, `Async::Waiter` is now deprecated.
54
+
55
+ ### Introduce `Async::Queue#close`
56
+
57
+ `Async::Queue` and `Async::LimitedQueue` can now be closed, which provides better resource management and error handling:
58
+
59
+ - **New `close` method**: Both queue types now have a `close` method that prevents further items from being added and signals any waiting tasks.
60
+ - **Consistent error handling**: All queue modification methods (`push`, `enqueue`, `<<`) now raise `Async::Queue::ClosedError` when called on a closed queue.
61
+ - **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.
62
+
63
+ <!-- end list -->
64
+
65
+ ``` ruby
66
+ queue = Async::Queue.new
67
+
68
+ # Start a task waiting for items:
69
+ waiting_task = Async do
70
+ queue.dequeue
71
+ end
72
+
73
+ # Close the queue - this signals the waiting task
74
+ queue.close
75
+
76
+ # These will raise Async::Queue::ClosedError
77
+ queue.push(:item) # => raises ClosedError
78
+ queue.enqueue(:item) # => raises ClosedError
79
+ queue << :item # => raises ClosedError
80
+
81
+ # Dequeue returns nil when closed and empty
82
+ queue.dequeue # => nil
83
+ ```
84
+
85
+ ## v2.25.0
86
+
87
+ - 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.
88
+
89
+ ### Use `IO::Event::WorkerPool` for Blocking Operations
90
+
91
+ The `Async::WorkerPool` implementation has been removed in favor of using `IO::Event::WorkerPool` directly. This change simplifies the codebase by delegating worker pool functionality to the `io-event` gem, which provides a more efficient and well-tested implementation.
92
+
93
+ To enable the worker pool, you can set the `ASYNC_SCHEDULER_WORKER_POOL` environment variable to `true`. This will allow the scheduler to use a worker pool for blocking operations, which can help improve performance in applications that perform a lot of CPU-bound operations (e.g. `rb_nogvl`).
94
+
95
+ ### Better handling of `IO#close` using `fiber_interrupt`
96
+
97
+ `IO#close` interrupts fibers that are waiting on the IO using the new `fiber_interrupt` hook introduced in Ruby 3.5/4.0. This means that if you close an IO while a fiber is waiting on it, the fiber will be interrupted and will raise an `IOError`. This is a change from previous versions of Ruby, where closing an IO would not interrupt fibers waiting on it, and would instead interrupt the entire event loop (essentially a bug).
98
+
99
+ ``` ruby
100
+ r, w = IO.pipe
101
+
102
+ Async do
103
+ child = Async do
104
+ r.gets
105
+ end
106
+
107
+ r.close # This will interrupt the child fiber.
108
+ child.wait # This will raise an `IOError` because the IO was closed.
109
+ end
110
+ ```
111
+
3
112
  ## v2.24.0
4
113
 
5
114
  - Ruby v3.1 support is dropped.
@@ -7,7 +116,7 @@
7
116
 
8
117
  ### Flexible Timeouts
9
118
 
10
- 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.
119
+ 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.
11
120
 
12
121
  ``` ruby
13
122
  Async do
@@ -81,7 +190,7 @@ To take advantage of this feature, you will need to introduce your own `config/t
81
190
 
82
191
  ## v2.19.0
83
192
 
84
- ### Async::Scheduler Debugging
193
+ ### `Async::Scheduler` Debugging
85
194
 
86
195
  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.
87
196
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.24.0
4
+ version: 2.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -10,24 +10,29 @@ authors:
10
10
  - Olle Jonsson
11
11
  - Patrik Wenger
12
12
  - Devin Christensen
13
+ - Shopify Inc.
13
14
  - Emil Tin
14
15
  - Jamie McCarthy
15
16
  - Kent Gruber
17
+ - Alan Wu
16
18
  - Brian Morearty
17
19
  - Colin Kelley
18
20
  - Dimitar Peychinov
19
21
  - Gert Goet
22
+ - Jahfer Husain
20
23
  - Jiang Jinyang
21
24
  - Julien Portalier
22
25
  - Jun Jiang
23
26
  - Ken Muryoi
24
27
  - Leon Löchner
28
+ - Mark Montroy
25
29
  - Masafumi Okura
26
30
  - Masayuki Yamamoto
27
31
  - Math Ieu
28
32
  - Ryan Musgrave
29
33
  - Salim Semaoune
30
34
  - Shannon Skipper
35
+ - Shigeru Nakajima
31
36
  - Sokolov Yura
32
37
  - Stefan Wrobel
33
38
  - Trevor Turk
@@ -62,7 +67,7 @@ cert_chain:
62
67
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
63
68
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
64
69
  -----END CERTIFICATE-----
65
- date: 2025-05-03 00:00:00.000000000 Z
70
+ date: 1980-01-02 00:00:00.000000000 Z
66
71
  dependencies:
67
72
  - !ruby/object:Gem::Dependency
68
73
  name: console
@@ -98,46 +103,47 @@ dependencies:
98
103
  requirements:
99
104
  - - "~>"
100
105
  - !ruby/object:Gem::Version
101
- version: '1.9'
106
+ version: '1.12'
102
107
  type: :runtime
103
108
  prerelease: false
104
109
  version_requirements: !ruby/object:Gem::Requirement
105
110
  requirements:
106
111
  - - "~>"
107
112
  - !ruby/object:Gem::Version
108
- version: '1.9'
113
+ version: '1.12'
109
114
  - !ruby/object:Gem::Dependency
110
- name: traces
115
+ name: metrics
111
116
  requirement: !ruby/object:Gem::Requirement
112
117
  requirements:
113
118
  - - "~>"
114
119
  - !ruby/object:Gem::Version
115
- version: '0.15'
120
+ version: '0.12'
116
121
  type: :runtime
117
122
  prerelease: false
118
123
  version_requirements: !ruby/object:Gem::Requirement
119
124
  requirements:
120
125
  - - "~>"
121
126
  - !ruby/object:Gem::Version
122
- version: '0.15'
127
+ version: '0.12'
123
128
  - !ruby/object:Gem::Dependency
124
- name: metrics
129
+ name: traces
125
130
  requirement: !ruby/object:Gem::Requirement
126
131
  requirements:
127
132
  - - "~>"
128
133
  - !ruby/object:Gem::Version
129
- version: '0.12'
134
+ version: '0.15'
130
135
  type: :runtime
131
136
  prerelease: false
132
137
  version_requirements: !ruby/object:Gem::Requirement
133
138
  requirements:
134
139
  - - "~>"
135
140
  - !ruby/object:Gem::Version
136
- version: '0.12'
141
+ version: '0.15'
137
142
  executables: []
138
143
  extensions: []
139
144
  extra_rdoc_files: []
140
145
  files:
146
+ - agent.md
141
147
  - lib/async.rb
142
148
  - lib/async/barrier.md
143
149
  - lib/async/barrier.rb
@@ -160,9 +166,7 @@ files:
160
166
  - lib/async/timeout.rb
161
167
  - lib/async/variable.rb
162
168
  - lib/async/version.rb
163
- - lib/async/waiter.md
164
169
  - lib/async/waiter.rb
165
- - lib/async/worker_pool.rb
166
170
  - lib/kernel/async.rb
167
171
  - lib/kernel/sync.rb
168
172
  - lib/metrics/provider/async.rb
@@ -187,14 +191,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
187
191
  requirements:
188
192
  - - ">="
189
193
  - !ruby/object:Gem::Version
190
- version: '3.1'
194
+ version: '3.2'
191
195
  required_rubygems_version: !ruby/object:Gem::Requirement
192
196
  requirements:
193
197
  - - ">="
194
198
  - !ruby/object:Gem::Version
195
199
  version: '0'
196
200
  requirements: []
197
- rubygems_version: 3.6.2
201
+ rubygems_version: 3.6.7
198
202
  specification_version: 4
199
203
  summary: A concurrency framework for Ruby.
200
204
  test_files: []
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
- ~~~
@@ -1,182 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2024, by Samuel Williams.
5
-
6
- require "etc"
7
-
8
- module Async
9
- # A simple work pool that offloads work to a background thread.
10
- #
11
- # @private
12
- class WorkerPool
13
- # Used to augment the scheduler to add support for blocking operations.
14
- module BlockingOperationWait
15
- # Wait for the given work to be executed.
16
- #
17
- # @public Since *Async v2.21* and *Ruby v3.4*.
18
- # @asynchronous May be non-blocking.
19
- #
20
- # @parameter work [Proc] The work to execute on a background thread.
21
- # @returns [Object] The result of the work.
22
- def blocking_operation_wait(work)
23
- @worker_pool.call(work)
24
- end
25
- end
26
-
27
- # Execute the given work in a background thread.
28
- class Promise
29
- # Create a new promise.
30
- #
31
- # @parameter work [Proc] The work to be done.
32
- def initialize(work)
33
- @work = work
34
- @state = :pending
35
- @value = nil
36
- @guard = ::Mutex.new
37
- @condition = ::ConditionVariable.new
38
- @thread = nil
39
- end
40
-
41
- # Execute the work and resolve the promise.
42
- def call
43
- work = nil
44
-
45
- @guard.synchronize do
46
- @thread = ::Thread.current
47
-
48
- return unless work = @work
49
- end
50
-
51
- resolve(work.call)
52
- rescue Exception => error
53
- reject(error)
54
- end
55
-
56
- private def resolve(value)
57
- @guard.synchronize do
58
- @work = nil
59
- @thread = nil
60
- @value = value
61
- @state = :resolved
62
- @condition.broadcast
63
- end
64
- end
65
-
66
- private def reject(error)
67
- @guard.synchronize do
68
- @work = nil
69
- @thread = nil
70
- @value = error
71
- @state = :failed
72
- @condition.broadcast
73
- end
74
- end
75
-
76
- # Cancel the work and raise an exception in the background thread.
77
- def cancel
78
- return unless @work
79
-
80
- @guard.synchronize do
81
- @work = nil
82
- @state = :cancelled
83
- @thread&.raise(Interrupt)
84
- end
85
- end
86
-
87
- # Wait for the work to be done.
88
- #
89
- # @returns [Object] The result of the work.
90
- def wait
91
- @guard.synchronize do
92
- while @state == :pending
93
- @condition.wait(@guard)
94
- end
95
-
96
- if @state == :failed
97
- raise @value
98
- else
99
- return @value
100
- end
101
- end
102
- end
103
- end
104
-
105
- # A background worker thread.
106
- class Worker
107
- # Create a new worker.
108
- def initialize
109
- @work = ::Thread::Queue.new
110
- @thread = ::Thread.new(&method(:run))
111
- end
112
-
113
- # Execute work until the queue is closed.
114
- def run
115
- while work = @work.pop
116
- work.call
117
- end
118
- end
119
-
120
- # Close the worker thread.
121
- def close
122
- if thread = @thread
123
- @thread = nil
124
- thread.kill
125
- end
126
- end
127
-
128
- # Call the work and notify the scheduler when it is done.
129
- def call(work)
130
- promise = Promise.new(work)
131
-
132
- @work.push(promise)
133
-
134
- begin
135
- return promise.wait
136
- ensure
137
- promise.cancel
138
- end
139
- end
140
- end
141
-
142
- # Create a new work pool.
143
- #
144
- # @parameter size [Integer] The number of threads to use.
145
- def initialize(size: Etc.nprocessors)
146
- @ready = ::Thread::Queue.new
147
-
148
- size.times do
149
- @ready.push(Worker.new)
150
- end
151
- end
152
-
153
- # Close the work pool. Kills all outstanding work.
154
- def close
155
- if ready = @ready
156
- @ready = nil
157
- ready.close
158
-
159
- while worker = ready.pop
160
- worker.close
161
- end
162
- end
163
- end
164
-
165
- # Offload work to a thread.
166
- #
167
- # @parameter work [Proc] The work to be done.
168
- def call(work)
169
- if ready = @ready
170
- worker = ready.pop
171
-
172
- begin
173
- worker.call(work)
174
- ensure
175
- ready.push(worker)
176
- end
177
- else
178
- raise RuntimeError, "No worker available!"
179
- end
180
- end
181
- end
182
- end