async 2.35.3 → 2.37.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: 7c439d8ba1591623b695ccd1dd4fe68f822fedd589291a7bdd79a53ce9c430d7
4
- data.tar.gz: cc8902bc5b15f446df404967e1832c9847eb0bcad879565508f6688bd0a5c92a
3
+ metadata.gz: 59107a94e01d137ccdba59b8204cfbb0a223418e3ce5374386fe9c12bf7f455d
4
+ data.tar.gz: 7a2dfe8b7b15bbe059e898faa3fd59b1b024e4156744897246368763ed35a106
5
5
  SHA512:
6
- metadata.gz: c2bfeb61c9f9ddad7576c66be0b637bee5934159e9388fe21cd0b1e8e5df804b5818194ef812d6249570411591d232a4c570fc6c6bcd02d67496e7aaf8950467
7
- data.tar.gz: 2454b411f93e6e86e25030bdd358eba7ed38536928ab64dc64b1dc10e3dff152ac4005ad27bdf3d4e801c49d69c2b386f7ca08961d90015dbe86fa2e9bccc5cf
6
+ metadata.gz: 03c75631a638f69c5e3bbd720c75c9edc1bb7f9a71a296fdc5f089532d0eb86d91babc5760cd97afdbe60ae1aabd226de2ae325b5d8f458538376faa18f3d820
7
+ data.tar.gz: c328e8223cd12b1e44329737156163fc152f981caedd7af7bb5b1f3920ed96cf6a93f18564a49d9d2e52f03bdd2e61aec39b519e5c13c9830553e2c709c0d486
checksums.yaml.gz.sig CHANGED
Binary file
data/lib/async/clock.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2025, by Samuel Williams.
4
+ # Copyright, 2018-2026, by Samuel Williams.
5
+ # Copyright, 2026, by Shopify Inc.
5
6
 
6
7
  module Async
7
8
  # A convenient wrapper around the internal monotonic clock.
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ module Async
7
+ # Raised if a timeout occurs on a specific Fiber. Handled gracefully by `Task`.
8
+ # @public Since *Async v1*.
9
+ class TimeoutError < StandardError
10
+ # Create a new timeout error.
11
+ #
12
+ # @parameter message [String] The error message.
13
+ def initialize(message = "execution expired")
14
+ super
15
+ end
16
+ end
17
+ end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2025, by Shopify Inc.
5
- # Copyright, 2025, by Samuel Williams.
4
+ # Copyright, 2025-2026, by Shopify Inc.
5
+ # Copyright, 2025-2026, by Samuel Williams.
6
6
 
7
7
  module Async
8
8
  # Private module that hooks into Process._fork to handle fork events.
data/lib/async/loop.rb ADDED
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "console"
7
+
8
+ module Async
9
+ # @namespace
10
+ module Loop
11
+ # Execute a block repeatedly at quantized (time-aligned) intervals.
12
+ #
13
+ # The alignment is computed modulo the current clock time in seconds. For example, with
14
+ # `interval: 60`, executions will occur at 00:00, 01:00, 02:00, etc., regardless of when
15
+ # the loop is started. With `interval: 300` (5 minutes), executions align to 00:00, 00:05,
16
+ # 00:10, etc.
17
+ #
18
+ # This is particularly useful for tasks that should run at predictable wall-clock times,
19
+ # such as metrics collection, periodic cleanup, or scheduled jobs that need to align
20
+ # across multiple processes.
21
+ #
22
+ # If an error occurs during block execution, it is logged and the loop continues.
23
+ #
24
+ # @example Run every minute at :00 seconds:
25
+ # Async::Loop.quantized(interval: 60) do
26
+ # puts "Current time: #{Time.now}"
27
+ # end
28
+ #
29
+ # @example Run every 5 minutes aligned to the hour:
30
+ # Async::Loop.quantized(interval: 300) do
31
+ # collect_metrics
32
+ # end
33
+ #
34
+ # @parameter interval [Numeric] The interval in seconds. Executions will align to multiples of this interval based on the current time.
35
+ # @yields The block to execute at each interval.
36
+ #
37
+ # @public Since *Async v2.37*.
38
+ def self.quantized(interval: 60, &block)
39
+ while true
40
+ # Compute the wait time to the next interval:
41
+ wait = interval - (Time.now.to_f % interval)
42
+ if wait.positive?
43
+ # Sleep until the next interval boundary:
44
+ sleep(wait)
45
+ end
46
+
47
+ begin
48
+ yield
49
+ rescue => error
50
+ Console.error(self, "Loop error:", error)
51
+ end
52
+ end
53
+ end
54
+
55
+ # Execute a block repeatedly with a fixed delay between executions.
56
+ #
57
+ # Unlike {quantized}, this method waits for the specified interval *after* each execution
58
+ # completes. This means the actual time between the start of successive executions will be
59
+ # `interval + execution_time`.
60
+ #
61
+ # If an error occurs during block execution, it is logged and the loop continues.
62
+ #
63
+ # @example Run every 5 seconds (plus execution time):
64
+ # Async::Loop.periodic(interval: 5) do
65
+ # process_queue
66
+ # end
67
+ #
68
+ # @parameter interval [Numeric] The delay in seconds between executions.
69
+ # @yields The block to execute periodically.
70
+ #
71
+ # @public Since *Async v2.37*.
72
+ def self.periodic(interval: 60, &block)
73
+ while true
74
+ begin
75
+ yield
76
+ rescue => error
77
+ Console.error(self, "Loop error:", error)
78
+ end
79
+
80
+ sleep(interval)
81
+ end
82
+ end
83
+ end
84
+ end
data/lib/async/node.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2025, by Samuel Williams.
4
+ # Copyright, 2017-2026, by Samuel Williams.
5
5
  # Copyright, 2017, by Kent Gruber.
6
6
  # Copyright, 2022, by Shannon Skipper.
7
- # Copyright, 2025, by Shopify Inc.
7
+ # Copyright, 2025-2026, by Shopify Inc.
8
8
 
9
9
  require "fiber/annotation"
10
10
 
@@ -295,6 +295,12 @@ module Async
295
295
  end
296
296
  end
297
297
 
298
+ # Wait for this node to complete. By default, nodes cannot be waited on.
299
+ # Subclasses like Task override this method to provide waiting functionality.
300
+ def wait
301
+ nil
302
+ end
303
+
298
304
  # Whether the node has been stopped.
299
305
  def stopped?
300
306
  @children.nil?
data/lib/async/promise.rb CHANGED
@@ -4,6 +4,9 @@
4
4
  # Copyright, 2025, by Shopify Inc.
5
5
  # Copyright, 2025-2026, by Samuel Williams.
6
6
 
7
+ require_relative "error"
8
+ require_relative "deadline"
9
+
7
10
  module Async
8
11
  # A promise represents a value that will be available in the future.
9
12
  # Unlike Condition, once resolved (or rejected), all future waits return immediately
@@ -77,17 +80,23 @@ module Async
77
80
  # Wait for the promise to be resolved and return the value.
78
81
  # If already resolved, returns immediately. If rejected, raises the stored exception.
79
82
  #
83
+ # @parameter timeout [Numeric | Nil] Maximum time to wait. If nil, waits indefinitely. If 0, raises immediately if not resolved.
80
84
  # @returns [Object] The resolved value.
81
85
  # @raises [Exception] The rejected or cancelled exception.
82
- def wait
86
+ # @raises [Async::TimeoutError] If timeout expires before the promise is resolved.
87
+ def wait(timeout: nil)
83
88
  @mutex.synchronize do
84
89
  # Increment waiting count:
85
90
  @waiting += 1
86
91
 
87
92
  begin
88
93
  # Wait for resolution if not already resolved:
89
- until @resolved
90
- @condition.wait(@mutex)
94
+ unless @resolved
95
+ if timeout.nil?
96
+ wait_indefinitely
97
+ else
98
+ wait_with_timeout(timeout)
99
+ end
91
100
  end
92
101
 
93
102
  # Return value or raise exception based on resolution type:
@@ -104,6 +113,39 @@ module Async
104
113
  end
105
114
  end
106
115
 
116
+ # Wait indefinitely for the promise to be resolved.
117
+ private def wait_indefinitely
118
+ until @resolved
119
+ @condition.wait(@mutex)
120
+ end
121
+ end
122
+
123
+ # Wait for the promise to be resolved, respecting the deadline timeout.
124
+ # @parameter timeout [Numeric] The timeout duration.
125
+ # @raises [Async::TimeoutError] If the timeout expires before resolution.
126
+ private def wait_with_timeout(timeout)
127
+ # Create deadline for timeout tracking:
128
+ deadline = Deadline.start(timeout)
129
+
130
+ # Handle immediate timeout (non-blocking):
131
+ if deadline == Deadline::Zero && !@resolved
132
+ raise Async::TimeoutError, "Promise wait not resolved!"
133
+ end
134
+
135
+ # Wait with deadline tracking:
136
+ until @resolved
137
+ # Get remaining time for this wait iteration:
138
+ remaining = deadline.remaining
139
+
140
+ # Check if deadline has expired before waiting:
141
+ if remaining <= 0
142
+ raise Async::TimeoutError, "Promise wait timed out!"
143
+ end
144
+
145
+ @condition.wait(@mutex, remaining)
146
+ end
147
+ end
148
+
107
149
  # Resolve the promise with a value.
108
150
  # All current and future waiters will receive this value.
109
151
  # Can only be called once - subsequent calls are ignored.
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2025, by Samuel Williams.
4
+ # Copyright, 2020-2026, by Samuel Williams.
5
5
  # Copyright, 2020, by Jun Jiang.
6
6
  # Copyright, 2021, by Julien Portalier.
7
- # Copyright, 2025, by Shopify Inc.
7
+ # Copyright, 2025-2026, by Shopify Inc.
8
8
 
9
9
  require_relative "clock"
10
10
  require_relative "task"
data/lib/async/task.rb CHANGED
@@ -1,36 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2025, by Samuel Williams.
4
+ # Copyright, 2017-2026, by Samuel Williams.
5
5
  # Copyright, 2017, by Kent Gruber.
6
6
  # Copyright, 2017, by Devin Christensen.
7
7
  # Copyright, 2020, by Patrik Wenger.
8
8
  # Copyright, 2023, by Math Ieu.
9
9
  # Copyright, 2025, by Shigeru Nakajima.
10
- # Copyright, 2025, by Shopify Inc.
10
+ # Copyright, 2025-2026, by Shopify Inc.
11
11
 
12
12
  require "fiber"
13
13
  require "console"
14
14
 
15
15
  require_relative "node"
16
16
  require_relative "condition"
17
+ require_relative "error"
17
18
  require_relative "promise"
18
19
  require_relative "stop"
19
20
 
20
21
  Fiber.attr_accessor :async_task
21
22
 
22
23
  module Async
23
- # Raised if a timeout occurs on a specific Fiber. Handled gracefully by `Task`.
24
- # @public Since *Async v1*.
25
- class TimeoutError < StandardError
26
- # Create a new timeout error.
27
- #
28
- # @parameter message [String] The error message.
29
- def initialize(message = "execution expired")
30
- super
31
- end
32
- end
33
-
24
+ # Represents a sequential unit of work, defined by a block, which is executed concurrently with other tasks. A task can be in one of the following states: `initialized`, `running`, `completed`, `failed`, `cancelled` or `stopped`.
25
+ #
26
+ # ```mermaid
27
+ # stateDiagram-v2
28
+ # [*] --> Initialized
29
+ # Initialized --> Running : Run
30
+ #
31
+ # Running --> Completed : Return Value
32
+ # Running --> Failed : Exception
33
+ #
34
+ # Completed --> [*]
35
+ # Failed --> [*]
36
+ #
37
+ # Running --> Stopped : Stop
38
+ # Stopped --> [*]
39
+ # Completed --> Stopped : Stop
40
+ # Failed --> Stopped : Stop
41
+ # Initialized --> Stopped : Stop
42
+ # ```
43
+ #
44
+ # @example Creating a task that sleeps for 1 second.
45
+ # require "async"
46
+ # Async do |task|
47
+ # sleep(1)
48
+ # end
49
+ #
34
50
  # @public Since *Async v1*.
35
51
  class Task < Node
36
52
  # Raised when a child task is created within a task that has finished execution.
@@ -258,11 +274,43 @@ module Async
258
274
  begin
259
275
  @promise.wait
260
276
  rescue Promise::Cancel
261
- # For backward compatibility, stopped tasks return nil
277
+ # For backward compatibility, stopped tasks return nil:
262
278
  return nil
263
279
  end
264
280
  end
265
281
 
282
+ # For compatibility with `Thread#join` and similar interfaces.
283
+ alias join wait
284
+
285
+ # Wait on all non-transient children to complete, recursively, then wait on the task itself, if it is not the current task.
286
+ #
287
+ # If any child task fails with an exception, that exception will be raised immediately, and remaining children may not be waited on.
288
+ #
289
+ # @example Waiting on all children.
290
+ # Async do |task|
291
+ # child = task.async do
292
+ # sleep(0.01)
293
+ # end
294
+ # task.wait_all # Will wait on the child task.
295
+ # end
296
+ #
297
+ # @raises [StandardError] If any child task failed with an exception, that exception will be raised.
298
+ # @returns [Object | Nil] The final expression/result of the task's block, or nil if called from within the task.
299
+ # @asynchronous This method is thread-safe.
300
+ def wait_all
301
+ @children&.each do |child|
302
+ # Skip transient tasks
303
+ next if child.transient?
304
+
305
+ child.wait_all
306
+ end
307
+
308
+ # Only wait on the task if we're not waiting on ourselves:
309
+ unless self.current?
310
+ return self.wait
311
+ end
312
+ end
313
+
266
314
  # Access the result of the task without waiting. May be nil if the task is not completed. Does not raise exceptions.
267
315
  def result
268
316
  value = @promise.value
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-2025, by Samuel Williams.
4
+ # Copyright, 2017-2026, by Samuel Williams.
5
5
 
6
6
  module Async
7
- VERSION = "2.35.3"
7
+ VERSION = "2.37.0"
8
8
  end
data/lib/async.rb CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  require_relative "async/version"
8
8
  require_relative "async/reactor"
9
+ require_relative "async/loop"
9
10
 
10
11
  require_relative "kernel/async"
11
12
  require_relative "kernel/sync"
data/license.md CHANGED
@@ -31,7 +31,7 @@ Copyright, 2025, by Jahfer Husain.
31
31
  Copyright, 2025, by Mark Montroy.
32
32
  Copyright, 2025, by Shigeru Nakajima.
33
33
  Copyright, 2025, by Alan Wu.
34
- Copyright, 2025, by Shopify Inc.
34
+ Copyright, 2025-2026, by Shopify Inc.
35
35
  Copyright, 2025, by Josh Teeter.
36
36
  Copyright, 2025, by Jatin Goyal.
37
37
  Copyright, 2025, by Yuhi Sato.
data/readme.md CHANGED
@@ -35,6 +35,15 @@ 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.37.0
39
+
40
+ - Introduce `Async::Loop` for robust, time-aligned loops.
41
+
42
+ ### v2.36.0
43
+
44
+ - Introduce `Task#wait_all` which recursively waits for all children and self, excepting the current task.
45
+ - Introduce `Task#join` as an alias for `Task#wait` for compatibility with `Thread#join` and similar interfaces.
46
+
38
47
  ### v2.35.3
39
48
 
40
49
  - `Async::Clock` now implements `#as_json` and `#to_json` for nicer log formatting.
@@ -68,17 +77,6 @@ Please see the [project releases](https://socketry.github.io/async/releases/inde
68
77
 
69
78
  - Introduce `Queue#waiting_count` and `PriorityQueue#waiting_count`. Generally for statistics/testing purposes only.
70
79
 
71
- ### v2.31.0
72
-
73
- - Introduce `Async::Deadline` for precise timeout management in compound operations.
74
-
75
- ### v2.30.0
76
-
77
- - Add timeout support to `Async::Queue#dequeue` and `Async::Queue#pop` methods.
78
- - Add timeout support to `Async::PriorityQueue#dequeue` and `Async::PriorityQueue#pop` methods.
79
- - Add `closed?` method to `Async::PriorityQueue` for full queue interface compatibility.
80
- - Support non-blocking operations using `timeout: 0` parameter.
81
-
82
80
  ## See Also
83
81
 
84
82
  - [async-http](https://github.com/socketry/async-http) — Asynchronous HTTP client/server.
data/releases.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Releases
2
2
 
3
+ ## v2.37.0
4
+
5
+ - Introduce `Async::Loop` for robust, time-aligned loops.
6
+
7
+ ## v2.36.0
8
+
9
+ - Introduce `Task#wait_all` which recursively waits for all children and self, excepting the current task.
10
+ - Introduce `Task#join` as an alias for `Task#wait` for compatibility with `Thread#join` and similar interfaces.
11
+
3
12
  ## v2.35.3
4
13
 
5
14
  - `Async::Clock` now implements `#as_json` and `#to_json` for nicer log formatting.
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.35.3
4
+ version: 2.37.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -161,10 +161,12 @@ files:
161
161
  - lib/async/condition.rb
162
162
  - lib/async/console.rb
163
163
  - lib/async/deadline.rb
164
+ - lib/async/error.rb
164
165
  - lib/async/fork_handler.rb
165
166
  - lib/async/idler.rb
166
167
  - lib/async/limited_queue.rb
167
168
  - lib/async/list.rb
169
+ - lib/async/loop.rb
168
170
  - lib/async/node.rb
169
171
  - lib/async/notification.rb
170
172
  - lib/async/priority_queue.rb
@@ -175,7 +177,6 @@ files:
175
177
  - lib/async/semaphore.md
176
178
  - lib/async/semaphore.rb
177
179
  - lib/async/stop.rb
178
- - lib/async/task.md
179
180
  - lib/async/task.rb
180
181
  - lib/async/timeout.rb
181
182
  - lib/async/variable.rb
metadata.gz.sig CHANGED
Binary file
data/lib/async/task.md DELETED
@@ -1,30 +0,0 @@
1
- A sequence of instructions, defined by a block, which is executed sequentially and managed by the scheduler. A task can be in one of the following states: `initialized`, `running`, `completed`, `failed`, `cancelled` or `stopped`.
2
-
3
- ```mermaid
4
- stateDiagram-v2
5
- [*] --> Initialized
6
- Initialized --> Running : Run
7
-
8
- Running --> Completed : Return Value
9
- Running --> Failed : Exception
10
-
11
- Completed --> [*]
12
- Failed --> [*]
13
-
14
- Running --> Stopped : Stop
15
- Stopped --> [*]
16
- Completed --> Stopped : Stop
17
- Failed --> Stopped : Stop
18
- Initialized --> Stopped : Stop
19
- ```
20
-
21
- ## Example
22
-
23
- ```ruby
24
- require "async"
25
-
26
- # Create an asynchronous task that sleeps for 1 second:
27
- Async do |task|
28
- sleep(1)
29
- end
30
- ```