async 2.36.0 → 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: b514097c721290749f38fed73721c5cb6d17af2a8af189f5a8e537f7b30c14d3
4
- data.tar.gz: 3eba30b0a03bdb9f9da1d1e4eeb6b11ec3224c0f3d02f27cc07ac1e5e7d7a41e
3
+ metadata.gz: 59107a94e01d137ccdba59b8204cfbb0a223418e3ce5374386fe9c12bf7f455d
4
+ data.tar.gz: 7a2dfe8b7b15bbe059e898faa3fd59b1b024e4156744897246368763ed35a106
5
5
  SHA512:
6
- metadata.gz: 9ea8d371279ec9bf8feeaa85895f25733247c8ecb059d599f34e8ecc85b4b124c05dee5421414d28c24316d7c4e69bdff622c31b182e9f883456836a6aab17ed
7
- data.tar.gz: 55ddefa4f011c2dccf235043cad5ad334418c394ced59a960b993d18ead0850f7d60875a3edbdc55418d3a9d3045cef1d607170fea0ae484f4dc62bc13f15ca2
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
 
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,26 @@
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
-
34
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`.
35
25
  #
36
26
  # ```mermaid
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.36.0"
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,10 @@ 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
+
38
42
  ### v2.36.0
39
43
 
40
44
  - Introduce `Task#wait_all` which recursively waits for all children and self, excepting the current task.
@@ -73,10 +77,6 @@ Please see the [project releases](https://socketry.github.io/async/releases/inde
73
77
 
74
78
  - Introduce `Queue#waiting_count` and `PriorityQueue#waiting_count`. Generally for statistics/testing purposes only.
75
79
 
76
- ### v2.31.0
77
-
78
- - Introduce `Async::Deadline` for precise timeout management in compound operations.
79
-
80
80
  ## See Also
81
81
 
82
82
  - [async-http](https://github.com/socketry/async-http) — Asynchronous HTTP client/server.
data/releases.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v2.37.0
4
+
5
+ - Introduce `Async::Loop` for robust, time-aligned loops.
6
+
3
7
  ## v2.36.0
4
8
 
5
9
  - Introduce `Task#wait_all` which recursively waits for all children and self, excepting the current task.
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.36.0
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
metadata.gz.sig CHANGED
Binary file