async 2.17.0 → 2.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/best-practices.md +188 -0
- data/context/debugging.md +63 -0
- data/context/getting-started.md +177 -0
- data/context/index.yaml +29 -0
- data/context/scheduler.md +109 -0
- data/context/tasks.md +448 -0
- data/context/thread-safety.md +651 -0
- data/lib/async/barrier.md +1 -2
- data/lib/async/barrier.rb +35 -12
- data/lib/async/clock.rb +11 -2
- data/lib/async/condition.md +1 -1
- data/lib/async/condition.rb +18 -34
- data/lib/async/console.rb +42 -0
- data/lib/async/deadline.rb +70 -0
- data/lib/async/idler.rb +2 -1
- data/lib/async/limited_queue.rb +13 -0
- data/lib/async/list.rb +16 -8
- data/lib/async/node.rb +5 -3
- data/lib/async/notification.rb +13 -9
- data/lib/async/priority_queue.rb +253 -0
- data/lib/async/promise.rb +188 -0
- data/lib/async/queue.rb +70 -82
- data/lib/async/reactor.rb +4 -2
- data/lib/async/scheduler.rb +233 -54
- data/lib/async/semaphore.rb +3 -3
- data/lib/async/stop.rb +82 -0
- data/lib/async/task.rb +111 -81
- data/lib/async/timeout.rb +88 -0
- data/lib/async/variable.rb +15 -4
- data/lib/async/version.rb +2 -2
- data/lib/async/waiter.rb +6 -1
- data/lib/kernel/async.rb +1 -1
- data/lib/kernel/sync.rb +14 -5
- data/lib/metrics/provider/async/task.rb +20 -0
- data/lib/metrics/provider/async.rb +6 -0
- data/lib/traces/provider/async/barrier.rb +17 -0
- data/lib/traces/provider/async/task.rb +40 -0
- data/lib/traces/provider/async.rb +7 -0
- data/license.md +8 -1
- data/readme.md +50 -7
- data/releases.md +357 -0
- data.tar.gz.sig +0 -0
- metadata +61 -20
- metadata.gz.sig +0 -0
- data/lib/async/waiter.md +0 -50
- data/lib/async/wrapper.rb +0 -65
@@ -0,0 +1,253 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Shopify Inc.
|
5
|
+
# Copyright, 2025, by Samuel Williams.
|
6
|
+
|
7
|
+
require "io/event/priority_heap"
|
8
|
+
require "thread"
|
9
|
+
|
10
|
+
require_relative "queue"
|
11
|
+
|
12
|
+
module Async
|
13
|
+
# A queue which allows items to be processed in priority order of consumers.
|
14
|
+
#
|
15
|
+
# Unlike a traditional priority queue where items have priorities, this queue
|
16
|
+
# assigns priorities to consumers (fibers waiting to dequeue). Higher priority
|
17
|
+
# consumers are served first when items become available.
|
18
|
+
#
|
19
|
+
# @public Since *Async v2*.
|
20
|
+
class PriorityQueue
|
21
|
+
ClosedError = Queue::ClosedError
|
22
|
+
|
23
|
+
# A waiter represents a fiber waiting to dequeue with a given priority.
|
24
|
+
Waiter = Struct.new(:fiber, :priority, :sequence, :condition, :value) do
|
25
|
+
include Comparable
|
26
|
+
|
27
|
+
def <=>(other)
|
28
|
+
# Higher priority comes first, then FIFO for equal priorities:
|
29
|
+
if priority == other.priority
|
30
|
+
# Use sequence for FIFO behavior (lower sequence = earlier):
|
31
|
+
sequence <=> other.sequence
|
32
|
+
else
|
33
|
+
other.priority <=> priority # Reverse for max-heap behavior
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def signal(value)
|
38
|
+
self.value = value
|
39
|
+
condition.signal
|
40
|
+
end
|
41
|
+
|
42
|
+
def wait_for_value(mutex, timeout = nil)
|
43
|
+
condition.wait(mutex, timeout)
|
44
|
+
return self.value
|
45
|
+
end
|
46
|
+
|
47
|
+
# Invalidate this waiter, making it unusable and detectable as abandoned.
|
48
|
+
def invalidate!
|
49
|
+
self.fiber = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
# Check if this waiter has been invalidated.
|
53
|
+
def valid?
|
54
|
+
self.fiber&.alive?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private_constant :Waiter
|
59
|
+
|
60
|
+
# Create a new priority queue.
|
61
|
+
#
|
62
|
+
# @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
|
63
|
+
def initialize(parent: nil)
|
64
|
+
@items = []
|
65
|
+
@closed = false
|
66
|
+
@parent = parent
|
67
|
+
@waiting = IO::Event::PriorityHeap.new
|
68
|
+
@sequence = 0
|
69
|
+
|
70
|
+
@mutex = Mutex.new
|
71
|
+
end
|
72
|
+
|
73
|
+
# Close the queue, causing all waiting tasks to return `nil`.
|
74
|
+
# Any subsequent calls to {enqueue} will raise an exception.
|
75
|
+
def close
|
76
|
+
@mutex.synchronize do
|
77
|
+
@closed = true
|
78
|
+
|
79
|
+
# Signal all waiting fibers with nil, skipping dead/invalid ones:
|
80
|
+
while waiter = @waiting.pop
|
81
|
+
waiter.signal(nil)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# @returns [Boolean] Whether the queue is closed.
|
87
|
+
def closed?
|
88
|
+
@closed
|
89
|
+
end
|
90
|
+
|
91
|
+
# @attribute [Array] The items in the queue.
|
92
|
+
attr :items
|
93
|
+
|
94
|
+
# @returns [Integer] The number of items in the queue.
|
95
|
+
def size
|
96
|
+
@items.size
|
97
|
+
end
|
98
|
+
|
99
|
+
# @returns [Boolean] Whether the queue is empty.
|
100
|
+
def empty?
|
101
|
+
@items.empty?
|
102
|
+
end
|
103
|
+
|
104
|
+
# @returns [Integer] The number of fibers waiting to dequeue.
|
105
|
+
def waiting_count
|
106
|
+
@mutex.synchronize do
|
107
|
+
@waiting.size
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# @deprecated Use {#waiting_count} instead.
|
112
|
+
alias waiting waiting_count
|
113
|
+
|
114
|
+
# Add an item to the queue.
|
115
|
+
#
|
116
|
+
# @parameter item [Object] The item to add to the queue.
|
117
|
+
def push(item)
|
118
|
+
@mutex.synchronize do
|
119
|
+
if @closed
|
120
|
+
raise ClosedError, "Cannot push items to a closed queue."
|
121
|
+
end
|
122
|
+
|
123
|
+
@items << item
|
124
|
+
|
125
|
+
# Wake up the highest priority waiter if any, skipping dead/invalid waiters:
|
126
|
+
while waiter = @waiting.pop
|
127
|
+
if waiter.valid?
|
128
|
+
value = @items.shift
|
129
|
+
waiter.signal(value)
|
130
|
+
break
|
131
|
+
end
|
132
|
+
# Dead/invalid waiter discarded, try next one.
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Compatibility with {::Queue#push}.
|
138
|
+
def <<(item)
|
139
|
+
self.push(item)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Add multiple items to the queue.
|
143
|
+
#
|
144
|
+
# @parameter items [Array] The items to add to the queue.
|
145
|
+
def enqueue(*items)
|
146
|
+
@mutex.synchronize do
|
147
|
+
if @closed
|
148
|
+
raise ClosedError, "Cannot enqueue items to a closed queue."
|
149
|
+
end
|
150
|
+
|
151
|
+
@items.concat(items)
|
152
|
+
|
153
|
+
# Wake up waiting fibers in priority order, skipping dead/invalid waiters:
|
154
|
+
while !@items.empty? && (waiter = @waiting.pop)
|
155
|
+
if waiter.valid?
|
156
|
+
value = @items.shift
|
157
|
+
waiter.signal(value)
|
158
|
+
end
|
159
|
+
# Dead/invalid waiter discarded, continue to next one.
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Remove and return the next item from the queue.
|
165
|
+
#
|
166
|
+
# If the queue is empty, this method will block until an item is available or timeout expires.
|
167
|
+
# Fibers are served in priority order, with higher priority fibers receiving
|
168
|
+
# items first.
|
169
|
+
#
|
170
|
+
# @parameter priority [Numeric] The priority of this consumer (higher = served first).
|
171
|
+
# @parameter timeout [Numeric, nil] Maximum time to wait for an item. If nil, waits indefinitely. If 0, returns immediately.
|
172
|
+
# @returns [Object, nil] The next item in the queue, or nil if timeout expires.
|
173
|
+
def dequeue(priority: 0, timeout: nil)
|
174
|
+
@mutex.synchronize do
|
175
|
+
# If queue is closed and empty, return nil immediately:
|
176
|
+
if @closed && @items.empty?
|
177
|
+
return nil
|
178
|
+
end
|
179
|
+
|
180
|
+
# Fast path: if items available and either no waiters or we have higher priority:
|
181
|
+
unless @items.empty?
|
182
|
+
head = @waiting.peek
|
183
|
+
if head.nil? or priority > head.priority
|
184
|
+
return @items.shift
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Handle immediate timeout (non-blocking)
|
189
|
+
return nil if timeout == 0
|
190
|
+
|
191
|
+
# Need to wait - create our own condition variable and add to waiting queue:
|
192
|
+
sequence = @sequence
|
193
|
+
@sequence += 1
|
194
|
+
|
195
|
+
condition = ConditionVariable.new
|
196
|
+
|
197
|
+
begin
|
198
|
+
waiter = Waiter.new(Fiber.current, priority, sequence, condition, nil)
|
199
|
+
@waiting.push(waiter)
|
200
|
+
|
201
|
+
# Wait for our specific condition variable to be signaled:
|
202
|
+
return waiter.wait_for_value(@mutex, timeout)
|
203
|
+
ensure
|
204
|
+
waiter&.invalidate!
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Compatibility with {::Queue#pop}.
|
210
|
+
#
|
211
|
+
# @parameter priority [Numeric] The priority of this consumer.
|
212
|
+
# @parameter timeout [Numeric, nil] Maximum time to wait for an item. If nil, waits indefinitely. If 0, returns immediately.
|
213
|
+
# @returns [Object, nil] The dequeued item, or nil if timeout expires.
|
214
|
+
def pop(priority: 0, timeout: nil)
|
215
|
+
self.dequeue(priority: priority, timeout: timeout)
|
216
|
+
end
|
217
|
+
|
218
|
+
# Process each item in the queue.
|
219
|
+
#
|
220
|
+
# @asynchronous Executes the given block concurrently for each item.
|
221
|
+
#
|
222
|
+
# @parameter priority [Numeric] The priority for processing items.
|
223
|
+
# @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
|
224
|
+
# @parameter options [Hash] The options to pass to the task.
|
225
|
+
# @yields {|task| ...} When the system is idle, the block will be executed in a new task.
|
226
|
+
def async(priority: 0, parent: (@parent or Task.current), **options, &block)
|
227
|
+
while item = self.dequeue(priority: priority)
|
228
|
+
parent.async(item, **options, &block)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Enumerate each item in the queue.
|
233
|
+
#
|
234
|
+
# @parameter priority [Numeric] The priority for dequeuing items.
|
235
|
+
def each(priority: 0)
|
236
|
+
while item = self.dequeue(priority: priority)
|
237
|
+
yield item
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Signal the queue with a value, the same as {#enqueue}.
|
242
|
+
def signal(value = nil)
|
243
|
+
self.enqueue(value)
|
244
|
+
end
|
245
|
+
|
246
|
+
# Wait for an item to be available, the same as {#dequeue}.
|
247
|
+
#
|
248
|
+
# @parameter priority [Numeric] The priority of this consumer.
|
249
|
+
def wait(priority: 0)
|
250
|
+
self.dequeue(priority: priority)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Shopify Inc.
|
5
|
+
# Copyright, 2025, by Samuel Williams.
|
6
|
+
|
7
|
+
module Async
|
8
|
+
# A promise represents a value that will be available in the future.
|
9
|
+
# Unlike Condition, once resolved (or rejected), all future waits return immediately
|
10
|
+
# with the stored value or raise the stored exception.
|
11
|
+
#
|
12
|
+
# This is thread-safe and integrates with the fiber scheduler.
|
13
|
+
#
|
14
|
+
# @public Since *Async v2*.
|
15
|
+
class Promise
|
16
|
+
# Create a new promise.
|
17
|
+
def initialize
|
18
|
+
# nil = pending, :completed = success, :failed = failure, :cancelled = cancelled:
|
19
|
+
@resolved = nil
|
20
|
+
|
21
|
+
# Stores either the result value or the exception:
|
22
|
+
@value = nil
|
23
|
+
|
24
|
+
# Track how many fibers are currently waiting:
|
25
|
+
@waiting = 0
|
26
|
+
|
27
|
+
@mutex = Mutex.new
|
28
|
+
@condition = ConditionVariable.new
|
29
|
+
end
|
30
|
+
|
31
|
+
# @returns [Boolean] Whether the promise has been resolved or rejected.
|
32
|
+
def resolved?
|
33
|
+
@mutex.synchronize {!!@resolved}
|
34
|
+
end
|
35
|
+
|
36
|
+
# @returns [Symbol | Nil] The internal resolved state (:completed, :failed, :cancelled, or nil if pending).
|
37
|
+
# @private For internal use by Task.
|
38
|
+
def resolved
|
39
|
+
@mutex.synchronize {@resolved}
|
40
|
+
end
|
41
|
+
|
42
|
+
# @returns [Boolean] Whether the promise has been cancelled.
|
43
|
+
def cancelled?
|
44
|
+
@mutex.synchronize {@resolved == :cancelled}
|
45
|
+
end
|
46
|
+
|
47
|
+
# @returns [Boolean] Whether the promise failed with an exception.
|
48
|
+
def failed?
|
49
|
+
@mutex.synchronize {@resolved == :failed}
|
50
|
+
end
|
51
|
+
|
52
|
+
# @returns [Boolean] Whether the promise has completed successfully.
|
53
|
+
def completed?
|
54
|
+
@mutex.synchronize {@resolved == :completed}
|
55
|
+
end
|
56
|
+
|
57
|
+
# @returns [Boolean] Whether any fibers are currently waiting for this promise.
|
58
|
+
def waiting?
|
59
|
+
@mutex.synchronize {@waiting > 0}
|
60
|
+
end
|
61
|
+
|
62
|
+
# Artificially mark that someone is waiting (useful for suppressing warnings).
|
63
|
+
# @private Internal use only.
|
64
|
+
def suppress_warnings!
|
65
|
+
@mutex.synchronize {@waiting += 1}
|
66
|
+
end
|
67
|
+
|
68
|
+
# Non-blocking access to the current value. Returns nil if not yet resolved.
|
69
|
+
# Does not raise exceptions even if the promise was rejected or cancelled.
|
70
|
+
# For resolved promises, returns the raw stored value (result, exception, or cancel exception).
|
71
|
+
#
|
72
|
+
# @returns [Object | Nil] The stored value, or nil if pending.
|
73
|
+
def value
|
74
|
+
@mutex.synchronize {@resolved ? @value : nil}
|
75
|
+
end
|
76
|
+
|
77
|
+
# Wait for the promise to be resolved and return the value.
|
78
|
+
# If already resolved, returns immediately. If rejected, raises the stored exception.
|
79
|
+
#
|
80
|
+
# @returns [Object] The resolved value.
|
81
|
+
# @raises [Exception] The rejected or cancelled exception.
|
82
|
+
def wait
|
83
|
+
@mutex.synchronize do
|
84
|
+
# Increment waiting count:
|
85
|
+
@waiting += 1
|
86
|
+
|
87
|
+
begin
|
88
|
+
# Wait for resolution if not already resolved:
|
89
|
+
@condition.wait(@mutex) unless @resolved
|
90
|
+
|
91
|
+
# Return value or raise exception based on resolution type:
|
92
|
+
if @resolved == :completed
|
93
|
+
return @value
|
94
|
+
else
|
95
|
+
# Both :failed and :cancelled store exceptions in @value
|
96
|
+
raise @value
|
97
|
+
end
|
98
|
+
ensure
|
99
|
+
# Decrement waiting count when done:
|
100
|
+
@waiting -= 1
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Resolve the promise with a value.
|
106
|
+
# All current and future waiters will receive this value.
|
107
|
+
# Can only be called once - subsequent calls are ignored.
|
108
|
+
#
|
109
|
+
# @parameter value [Object] The value to resolve the promise with.
|
110
|
+
def resolve(value)
|
111
|
+
@mutex.synchronize do
|
112
|
+
return if @resolved
|
113
|
+
|
114
|
+
@value = value
|
115
|
+
@resolved = :completed
|
116
|
+
|
117
|
+
# Wake up all waiting fibers:
|
118
|
+
@condition.broadcast
|
119
|
+
end
|
120
|
+
|
121
|
+
return value
|
122
|
+
end
|
123
|
+
|
124
|
+
# Reject the promise with an exception.
|
125
|
+
# All current and future waiters will receive this exception.
|
126
|
+
# Can only be called once - subsequent calls are ignored.
|
127
|
+
#
|
128
|
+
# @parameter exception [Exception] The exception to reject the promise with.
|
129
|
+
def reject(exception)
|
130
|
+
@mutex.synchronize do
|
131
|
+
return if @resolved
|
132
|
+
|
133
|
+
@value = exception
|
134
|
+
@resolved = :failed
|
135
|
+
|
136
|
+
# Wake up all waiting fibers:
|
137
|
+
@condition.broadcast
|
138
|
+
end
|
139
|
+
|
140
|
+
return nil
|
141
|
+
end
|
142
|
+
|
143
|
+
# Exception used to indicate cancellation.
|
144
|
+
class Cancel < Exception
|
145
|
+
end
|
146
|
+
|
147
|
+
# Cancel the promise, indicating cancellation.
|
148
|
+
# All current and future waiters will receive nil.
|
149
|
+
# Can only be called on pending promises - no-op if already resolved.
|
150
|
+
def cancel(exception = Cancel.new("Promise was cancelled!"))
|
151
|
+
@mutex.synchronize do
|
152
|
+
# No-op if already in any final state
|
153
|
+
return if @resolved
|
154
|
+
|
155
|
+
@value = exception
|
156
|
+
@resolved = :cancelled
|
157
|
+
|
158
|
+
# Wake up all waiting fibers:
|
159
|
+
@condition.broadcast
|
160
|
+
end
|
161
|
+
|
162
|
+
return nil
|
163
|
+
end
|
164
|
+
|
165
|
+
# Resolve the promise with the result of the block.
|
166
|
+
# If the block raises an exception, the promise will be rejected.
|
167
|
+
# If the promise was already resolved, the block will not be called.
|
168
|
+
# @yields {...} The block to call to resolve the promise.
|
169
|
+
# @returns [Object] The result of the block.
|
170
|
+
def fulfill(&block)
|
171
|
+
raise "Promise already resolved!" if @resolved
|
172
|
+
|
173
|
+
begin
|
174
|
+
return self.resolve(yield)
|
175
|
+
rescue Cancel => exception
|
176
|
+
return self.cancel(exception)
|
177
|
+
rescue => error
|
178
|
+
return self.reject(error)
|
179
|
+
rescue Exception => exception
|
180
|
+
self.reject(exception)
|
181
|
+
raise
|
182
|
+
ensure
|
183
|
+
# Handle non-local exits (throw, etc.) that bypass normal flow:
|
184
|
+
self.resolve(nil) unless @resolved
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
data/lib/async/queue.rb
CHANGED
@@ -1,70 +1,95 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2019, by Ryan Musgrave.
|
6
6
|
# Copyright, 2020-2022, by Bruno Sutic.
|
7
|
+
# Copyright, 2025, by Jahfer Husain.
|
8
|
+
# Copyright, 2025, by Shopify Inc.
|
7
9
|
|
8
|
-
require_relative
|
10
|
+
require_relative "notification"
|
9
11
|
|
10
12
|
module Async
|
11
|
-
# A queue which allows items to be processed in order.
|
13
|
+
# A thread-safe queue which allows items to be processed in order.
|
14
|
+
#
|
15
|
+
# This implementation uses Thread::Queue internally for thread safety while
|
16
|
+
# maintaining compatibility with the fiber scheduler.
|
12
17
|
#
|
13
18
|
# It has a compatible interface with {Notification} and {Condition}, except that it's multi-value.
|
14
19
|
#
|
15
|
-
# @
|
20
|
+
# @asynchronous This class is thread-safe.
|
21
|
+
# @public Since *Async v1*.
|
16
22
|
class Queue
|
17
|
-
#
|
23
|
+
# An error raised when trying to enqueue items to a closed queue.
|
24
|
+
# @public Since *Async v2.24*.
|
25
|
+
class ClosedError < RuntimeError
|
26
|
+
end
|
27
|
+
|
28
|
+
# Create a new thread-safe queue.
|
18
29
|
#
|
19
30
|
# @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
|
20
|
-
|
21
|
-
|
22
|
-
@items = []
|
31
|
+
def initialize(parent: nil, delegate: Thread::Queue.new)
|
32
|
+
@delegate = delegate
|
23
33
|
@parent = parent
|
24
|
-
@available = available
|
25
34
|
end
|
26
35
|
|
27
|
-
# @
|
28
|
-
|
36
|
+
# @returns [Boolean] Whether the queue is closed.
|
37
|
+
def closed?
|
38
|
+
@delegate.closed?
|
39
|
+
end
|
40
|
+
|
41
|
+
# Close the queue, causing all waiting tasks to return `nil`. Any subsequent calls to {enqueue} will raise an exception.
|
42
|
+
def close
|
43
|
+
@delegate.close
|
44
|
+
end
|
29
45
|
|
30
46
|
# @returns [Integer] The number of items in the queue.
|
31
47
|
def size
|
32
|
-
@
|
48
|
+
@delegate.size
|
33
49
|
end
|
34
50
|
|
35
51
|
# @returns [Boolean] Whether the queue is empty.
|
36
52
|
def empty?
|
37
|
-
@
|
53
|
+
@delegate.empty?
|
54
|
+
end
|
55
|
+
|
56
|
+
# @returns [Integer] The number of tasks waiting for an item.
|
57
|
+
def waiting_count
|
58
|
+
@delegate.num_waiting
|
38
59
|
end
|
39
60
|
|
40
61
|
# Add an item to the queue.
|
41
62
|
def push(item)
|
42
|
-
@
|
43
|
-
|
44
|
-
|
63
|
+
@delegate.push(item)
|
64
|
+
rescue ClosedQueueError
|
65
|
+
raise ClosedError, "Cannot enqueue items to a closed queue!"
|
45
66
|
end
|
46
67
|
|
47
68
|
# Compatibility with {::Queue#push}.
|
48
|
-
|
69
|
+
def <<(item)
|
70
|
+
self.push(item)
|
71
|
+
end
|
49
72
|
|
50
73
|
# Add multiple items to the queue.
|
51
74
|
def enqueue(*items)
|
52
|
-
|
53
|
-
|
54
|
-
|
75
|
+
items.each {|item| @delegate.push(item)}
|
76
|
+
rescue ClosedQueueError
|
77
|
+
raise ClosedError, "Cannot enqueue items to a closed queue!"
|
55
78
|
end
|
56
79
|
|
57
80
|
# Remove and return the next item from the queue.
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
@items.shift
|
81
|
+
# @parameter timeout [Numeric, nil] Maximum time to wait for an item. If nil, waits indefinitely. If 0, returns immediately.
|
82
|
+
# @returns [Object, nil] The dequeued item, or nil if timeout expires.
|
83
|
+
def dequeue(timeout: nil)
|
84
|
+
@delegate.pop(timeout: timeout)
|
64
85
|
end
|
65
86
|
|
66
87
|
# Compatibility with {::Queue#pop}.
|
67
|
-
|
88
|
+
# @parameter timeout [Numeric, nil] Maximum time to wait for an item. If nil, waits indefinitely. If 0, returns immediately.
|
89
|
+
# @returns [Object, nil] The dequeued item, or nil if timeout expires.
|
90
|
+
def pop(timeout: nil)
|
91
|
+
@delegate.pop(timeout: timeout)
|
92
|
+
end
|
68
93
|
|
69
94
|
# Process each item in the queue.
|
70
95
|
#
|
@@ -88,7 +113,7 @@ module Async
|
|
88
113
|
end
|
89
114
|
|
90
115
|
# Signal the queue with a value, the same as {#enqueue}.
|
91
|
-
def signal(value)
|
116
|
+
def signal(value = nil)
|
92
117
|
self.enqueue(value)
|
93
118
|
end
|
94
119
|
|
@@ -98,70 +123,33 @@ module Async
|
|
98
123
|
end
|
99
124
|
end
|
100
125
|
|
101
|
-
# A queue which limits the number of items that can be enqueued.
|
102
|
-
#
|
126
|
+
# A thread-safe queue which limits the number of items that can be enqueued.
|
127
|
+
#
|
128
|
+
# @public Since *Async v1*.
|
103
129
|
class LimitedQueue < Queue
|
130
|
+
# @private This exists purely for emitting a warning.
|
131
|
+
def self.new(...)
|
132
|
+
warn("`require 'async/limited_queue'` to use `Async::LimitedQueue`.", uplevel: 1, category: :deprecated) if $VERBOSE
|
133
|
+
|
134
|
+
super
|
135
|
+
end
|
136
|
+
|
104
137
|
# Create a new limited queue.
|
105
138
|
#
|
106
139
|
# @parameter limit [Integer] The maximum number of items that can be enqueued.
|
107
|
-
# @parameter full [Notification] The notification to use for signaling when the queue is full.
|
108
|
-
def initialize(limit = 1,
|
109
|
-
super(**options)
|
110
|
-
|
111
|
-
@limit = limit
|
112
|
-
@full = full
|
140
|
+
# @parameter full [Notification] The notification to use for signaling when the queue is full. (ignored, for compatibility)
|
141
|
+
def initialize(limit = 1, **options)
|
142
|
+
super(**options, delegate: Thread::SizedQueue.new(limit))
|
113
143
|
end
|
114
144
|
|
115
145
|
# @attribute [Integer] The maximum number of items that can be enqueued.
|
116
|
-
|
146
|
+
def limit
|
147
|
+
@delegate.max
|
148
|
+
end
|
117
149
|
|
118
150
|
# @returns [Boolean] Whether trying to enqueue an item would block.
|
119
151
|
def limited?
|
120
|
-
@
|
121
|
-
end
|
122
|
-
|
123
|
-
# Add an item to the queue.
|
124
|
-
#
|
125
|
-
# If the queue is full, this method will block until there is space available.
|
126
|
-
#
|
127
|
-
# @parameter item [Object] The item to add to the queue.
|
128
|
-
def <<(item)
|
129
|
-
while limited?
|
130
|
-
@full.wait
|
131
|
-
end
|
132
|
-
|
133
|
-
super
|
134
|
-
end
|
135
|
-
|
136
|
-
# Add multiple items to the queue.
|
137
|
-
#
|
138
|
-
# If the queue is full, this method will block until there is space available.
|
139
|
-
#
|
140
|
-
# @parameter items [Array] The items to add to the queue.
|
141
|
-
def enqueue(*items)
|
142
|
-
while !items.empty?
|
143
|
-
while limited?
|
144
|
-
@full.wait
|
145
|
-
end
|
146
|
-
|
147
|
-
available = @limit - @items.size
|
148
|
-
@items.concat(items.shift(available))
|
149
|
-
|
150
|
-
@available.signal unless self.empty?
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
# Remove and return the next item from the queue.
|
155
|
-
#
|
156
|
-
# If the queue is empty, this method will block until an item is available.
|
157
|
-
#
|
158
|
-
# @returns [Object] The next item in the queue.
|
159
|
-
def dequeue
|
160
|
-
item = super
|
161
|
-
|
162
|
-
@full.signal
|
163
|
-
|
164
|
-
return item
|
152
|
+
!@delegate.closed? && @delegate.size >= @delegate.max
|
165
153
|
end
|
166
154
|
end
|
167
155
|
end
|
data/lib/async/reactor.rb
CHANGED
@@ -1,17 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2017-
|
4
|
+
# Copyright, 2017-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2017, by Kent Gruber.
|
6
6
|
# Copyright, 2018, by Sokolov Yura.
|
7
7
|
|
8
|
-
require_relative
|
8
|
+
require_relative "scheduler"
|
9
9
|
|
10
10
|
module Async
|
11
11
|
# A wrapper around the the scheduler which binds it to the current thread automatically.
|
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
|
|