async 2.12.1 → 2.22.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/lib/async/barrier.md +1 -1
- data/lib/async/barrier.rb +5 -5
- data/lib/async/clock.rb +10 -1
- data/lib/async/condition.md +1 -1
- data/lib/async/condition.rb +11 -6
- data/lib/async/console.rb +42 -0
- data/lib/async/idler.rb +21 -1
- data/lib/async/limited_queue.rb +7 -0
- data/lib/async/list.rb +7 -4
- data/lib/async/node.rb +48 -6
- data/lib/async/notification.rb +5 -3
- data/lib/async/queue.rb +82 -21
- data/lib/async/reactor.rb +4 -2
- data/lib/async/scheduler.rb +210 -75
- data/lib/async/semaphore.rb +3 -3
- data/lib/async/task.rb +114 -55
- data/lib/async/variable.rb +26 -5
- data/lib/async/version.rb +1 -1
- data/lib/async/waiter.rb +8 -3
- data/lib/async/worker_pool.rb +182 -0
- data/lib/async/wrapper.rb +6 -2
- data/lib/async.rb +2 -1
- data/lib/kernel/async.rb +4 -2
- data/lib/kernel/sync.rb +12 -5
- data/lib/metrics/provider/async/task.rb +22 -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 +2 -1
- data/readme.md +49 -29
- data/releases.md +97 -0
- data.tar.gz.sig +0 -0
- metadata +45 -24
- metadata.gz.sig +0 -0
data/lib/async/task.rb
CHANGED
@@ -7,24 +7,32 @@
|
|
7
7
|
# Copyright, 2020, by Patrik Wenger.
|
8
8
|
# Copyright, 2023, by Math Ieu.
|
9
9
|
|
10
|
-
require
|
11
|
-
require
|
10
|
+
require "fiber"
|
11
|
+
require "console"
|
12
12
|
|
13
|
-
require_relative
|
14
|
-
require_relative
|
13
|
+
require_relative "node"
|
14
|
+
require_relative "condition"
|
15
|
+
|
16
|
+
Fiber.attr_accessor :async_task
|
15
17
|
|
16
18
|
module Async
|
17
19
|
# Raised when a task is explicitly stopped.
|
18
20
|
class Stop < Exception
|
21
|
+
# Used to defer stopping the current task until later.
|
19
22
|
class Later
|
23
|
+
# Create a new stop later operation.
|
24
|
+
#
|
25
|
+
# @parameter task [Task] The task to stop later.
|
20
26
|
def initialize(task)
|
21
27
|
@task = task
|
22
28
|
end
|
23
29
|
|
30
|
+
# @returns [Boolean] Whether the task is alive.
|
24
31
|
def alive?
|
25
32
|
true
|
26
33
|
end
|
27
34
|
|
35
|
+
# Transfer control to the operation - this will stop the task.
|
28
36
|
def transfer
|
29
37
|
@task.stop
|
30
38
|
end
|
@@ -32,16 +40,23 @@ module Async
|
|
32
40
|
end
|
33
41
|
|
34
42
|
# Raised if a timeout occurs on a specific Fiber. Handled gracefully by `Task`.
|
35
|
-
# @public Since
|
43
|
+
# @public Since *Async v1*.
|
36
44
|
class TimeoutError < StandardError
|
45
|
+
# Create a new timeout error.
|
46
|
+
#
|
47
|
+
# @parameter message [String] The error message.
|
37
48
|
def initialize(message = "execution expired")
|
38
49
|
super
|
39
50
|
end
|
40
51
|
end
|
41
52
|
|
42
|
-
# @public Since
|
53
|
+
# @public Since *Async v1*.
|
43
54
|
class Task < Node
|
55
|
+
# Raised when a child task is created within a task that has finished execution.
|
44
56
|
class FinishedError < RuntimeError
|
57
|
+
# Create a new finished error.
|
58
|
+
#
|
59
|
+
# @parameter message [String] The error message.
|
45
60
|
def initialize(message = "Cannot create child task within a task that has finished execution!")
|
46
61
|
super
|
47
62
|
end
|
@@ -52,6 +67,13 @@ module Async
|
|
52
67
|
Fiber.scheduler.transfer
|
53
68
|
end
|
54
69
|
|
70
|
+
# Run the given block of code in a task, asynchronously, in the given scheduler.
|
71
|
+
def self.run(scheduler, *arguments, **options, &block)
|
72
|
+
self.new(scheduler, **options, &block).tap do |task|
|
73
|
+
task.run(*arguments)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
55
77
|
# Create a new task.
|
56
78
|
# @parameter reactor [Reactor] the reactor this task will run within.
|
57
79
|
# @parameter parent [Task] the parent task.
|
@@ -72,14 +94,21 @@ module Async
|
|
72
94
|
@defer_stop = nil
|
73
95
|
end
|
74
96
|
|
97
|
+
# @returns [Scheduler] The scheduler for this task.
|
75
98
|
def reactor
|
76
99
|
self.root
|
77
100
|
end
|
78
101
|
|
102
|
+
# @returns [Array(Thread::Backtrace::Location) | Nil] The backtrace of the task, if available.
|
79
103
|
def backtrace(*arguments)
|
80
104
|
@fiber&.backtrace(*arguments)
|
81
105
|
end
|
82
106
|
|
107
|
+
# Annotate the task with a description.
|
108
|
+
#
|
109
|
+
# This will internally try to annotate the fiber if it is running, otherwise it will annotate the task itself.
|
110
|
+
#
|
111
|
+
# @parameter annotation [String] The description to annotate the task with.
|
83
112
|
def annotate(annotation, &block)
|
84
113
|
if @fiber
|
85
114
|
@fiber.annotate(annotation, &block)
|
@@ -88,6 +117,7 @@ module Async
|
|
88
117
|
end
|
89
118
|
end
|
90
119
|
|
120
|
+
# @returns [Object] The annotation of the task.
|
91
121
|
def annotation
|
92
122
|
if @fiber
|
93
123
|
@fiber.annotation
|
@@ -96,6 +126,7 @@ module Async
|
|
96
126
|
end
|
97
127
|
end
|
98
128
|
|
129
|
+
# @returns [String] A description of the task and it's current status.
|
99
130
|
def to_s
|
100
131
|
"\#<#{self.description} (#{@status})>"
|
101
132
|
end
|
@@ -115,10 +146,10 @@ module Async
|
|
115
146
|
Fiber.scheduler.yield
|
116
147
|
end
|
117
148
|
|
118
|
-
# @
|
149
|
+
# @attribute [Fiber] The fiber which is being used for the execution of this task.
|
119
150
|
attr :fiber
|
120
151
|
|
121
|
-
# Whether the internal fiber is alive, i.e. it
|
152
|
+
# @returns [Boolean] Whether the internal fiber is alive, i.e. it is actively executing.
|
122
153
|
def alive?
|
123
154
|
@fiber&.alive?
|
124
155
|
end
|
@@ -130,38 +161,50 @@ module Async
|
|
130
161
|
super && @block.nil? && @fiber.nil?
|
131
162
|
end
|
132
163
|
|
133
|
-
# Whether the task is running.
|
134
|
-
# @returns [Boolean]
|
164
|
+
# @returns [Boolean] Whether the task is running.
|
135
165
|
def running?
|
136
166
|
@status == :running
|
137
167
|
end
|
138
168
|
|
169
|
+
# @returns [Boolean] Whether the task failed with an exception.
|
139
170
|
def failed?
|
140
171
|
@status == :failed
|
141
172
|
end
|
142
173
|
|
143
|
-
#
|
174
|
+
# @returns [Boolean] Whether the task has been stopped.
|
144
175
|
def stopped?
|
145
176
|
@status == :stopped
|
146
177
|
end
|
147
178
|
|
148
|
-
#
|
179
|
+
# @returns [Boolean] Whether the task has completed execution and generated a result.
|
149
180
|
def completed?
|
150
181
|
@status == :completed
|
151
182
|
end
|
152
183
|
|
153
|
-
|
184
|
+
# Alias for {#completed?}.
|
185
|
+
def complete?
|
186
|
+
self.completed?
|
187
|
+
end
|
154
188
|
|
155
|
-
# @
|
189
|
+
# @attribute [Symbol] The status of the execution of the task, one of `:initialized`, `:running`, `:complete`, `:stopped` or `:failed`.
|
156
190
|
attr :status
|
157
191
|
|
158
192
|
# Begin the execution of the task.
|
193
|
+
#
|
194
|
+
# @raises [RuntimeError] If the task is already running.
|
159
195
|
def run(*arguments)
|
160
196
|
if @status == :initialized
|
161
197
|
@status = :running
|
162
198
|
|
163
199
|
schedule do
|
164
200
|
@block.call(self, *arguments)
|
201
|
+
rescue => error
|
202
|
+
# I'm not completely happy with this overhead, but the alternative is to not log anything which makes debugging extremely difficult. Maybe we can introduce a debug wrapper which adds extra logging.
|
203
|
+
if @finished.nil?
|
204
|
+
warn(self, "Task may have ended with unhandled exception.", exception: error)
|
205
|
+
end
|
206
|
+
|
207
|
+
raise
|
165
208
|
end
|
166
209
|
else
|
167
210
|
raise RuntimeError, "Task already running!"
|
@@ -169,11 +212,25 @@ module Async
|
|
169
212
|
end
|
170
213
|
|
171
214
|
# Run an asynchronous task as a child of the current task.
|
215
|
+
#
|
216
|
+
# @public Since *Async v1*.
|
217
|
+
# @asynchronous May context switch immediately to the new task.
|
218
|
+
#
|
219
|
+
# @yields {|task| ...} in the context of the new task.
|
220
|
+
# @raises [FinishedError] If the task has already finished.
|
221
|
+
# @returns [Task] The child task.
|
172
222
|
def async(*arguments, **options, &block)
|
173
223
|
raise FinishedError if self.finished?
|
174
224
|
|
175
225
|
task = Task.new(self, **options, &block)
|
176
226
|
|
227
|
+
# When calling an async block, we deterministically execute it until the first blocking operation. We don't *have* to do this - we could schedule it for later execution, but it's useful to:
|
228
|
+
#
|
229
|
+
# - Fail at the point of the method call where possible.
|
230
|
+
# - Execute determinstically where possible.
|
231
|
+
# - Avoid scheduler overhead if no blocking operation is performed.
|
232
|
+
#
|
233
|
+
# There are different strategies (greedy vs non-greedy). We are currently using a greedy strategy.
|
177
234
|
task.run(*arguments)
|
178
235
|
|
179
236
|
return task
|
@@ -215,15 +272,18 @@ module Async
|
|
215
272
|
return stopped!
|
216
273
|
end
|
217
274
|
|
218
|
-
# If we are deferring stop...
|
219
|
-
if @defer_stop == false
|
220
|
-
# Don't stop now... but update the state so we know we need to stop later.
|
221
|
-
@defer_stop = true
|
222
|
-
return false
|
223
|
-
end
|
224
|
-
|
225
275
|
# If the fiber is alive, we need to stop it:
|
226
276
|
if @fiber&.alive?
|
277
|
+
# As the task is now exiting, we want to ensure the event loop continues to execute until the task finishes.
|
278
|
+
self.transient = false
|
279
|
+
|
280
|
+
# If we are deferring stop...
|
281
|
+
if @defer_stop == false
|
282
|
+
# Don't stop now... but update the state so we know we need to stop later.
|
283
|
+
@defer_stop = true
|
284
|
+
return false
|
285
|
+
end
|
286
|
+
|
227
287
|
if self.current?
|
228
288
|
# If the fiber is current, and later is `true`, we need to schedule the fiber to be stopped later, as it's currently invoking `stop`:
|
229
289
|
if later
|
@@ -238,7 +298,7 @@ module Async
|
|
238
298
|
begin
|
239
299
|
# There is a chance that this will stop the fiber that originally called stop. If that happens, the exception handling in `#stopped` will rescue the exception and re-raise it later.
|
240
300
|
Fiber.scheduler.raise(@fiber, Stop)
|
241
|
-
rescue FiberError
|
301
|
+
rescue FiberError => error
|
242
302
|
# In some cases, this can cause a FiberError (it might be resumed already), so we schedule it to be stopped later:
|
243
303
|
Fiber.scheduler.push(Stop::Later.new(self))
|
244
304
|
end
|
@@ -256,26 +316,30 @@ module Async
|
|
256
316
|
# If stop is invoked a second time, it will be immediately executed.
|
257
317
|
#
|
258
318
|
# @yields {} The block of code to execute.
|
259
|
-
# @public Since
|
319
|
+
# @public Since *Async v1*.
|
260
320
|
def defer_stop
|
261
321
|
# Tri-state variable for controlling stop:
|
262
322
|
# - nil: defer_stop has not been called.
|
263
323
|
# - false: defer_stop has been called and we are not stopping.
|
264
324
|
# - true: defer_stop has been called and we will stop when exiting the block.
|
265
325
|
if @defer_stop.nil?
|
266
|
-
# If we are not deferring stop already, we can defer it now:
|
267
|
-
@defer_stop = false
|
268
|
-
|
269
326
|
begin
|
327
|
+
# If we are not deferring stop already, we can defer it now:
|
328
|
+
@defer_stop = false
|
329
|
+
|
270
330
|
yield
|
271
331
|
rescue Stop
|
272
332
|
# If we are exiting due to a stop, we shouldn't try to invoke stop again:
|
273
333
|
@defer_stop = nil
|
274
334
|
raise
|
275
335
|
ensure
|
336
|
+
defer_stop = @defer_stop
|
337
|
+
|
338
|
+
# We need to ensure the state is reset before we exit the block:
|
339
|
+
@defer_stop = nil
|
340
|
+
|
276
341
|
# If we were asked to stop, we should do so now:
|
277
|
-
if
|
278
|
-
@defer_stop = nil
|
342
|
+
if defer_stop
|
279
343
|
raise Stop, "Stopping current task (was deferred)!"
|
280
344
|
end
|
281
345
|
end
|
@@ -285,25 +349,35 @@ module Async
|
|
285
349
|
end
|
286
350
|
end
|
287
351
|
|
352
|
+
# @returns [Boolean] Whether stop has been deferred.
|
353
|
+
def stop_deferred?
|
354
|
+
@defer_stop
|
355
|
+
end
|
356
|
+
|
288
357
|
# Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
|
289
358
|
# @returns [Task]
|
290
359
|
# @raises[RuntimeError] If task was not {set!} for the current fiber.
|
291
360
|
def self.current
|
292
|
-
|
361
|
+
Fiber.current.async_task or raise RuntimeError, "No async task available!"
|
293
362
|
end
|
294
363
|
|
295
364
|
# Check if there is a task defined for the current fiber.
|
296
|
-
# @returns [
|
365
|
+
# @returns [Interface(:async) | Nil]
|
297
366
|
def self.current?
|
298
|
-
|
367
|
+
Fiber.current.async_task
|
299
368
|
end
|
300
369
|
|
370
|
+
# @returns [Boolean] Whether this task is the currently executing task.
|
301
371
|
def current?
|
302
372
|
Fiber.current.equal?(@fiber)
|
303
373
|
end
|
304
374
|
|
305
375
|
private
|
306
376
|
|
377
|
+
def warn(...)
|
378
|
+
Console.warn(...)
|
379
|
+
end
|
380
|
+
|
307
381
|
# Finish the current task, moving any children to the parent.
|
308
382
|
def finish!
|
309
383
|
# Don't hold references to the fiber or block after the task has finished:
|
@@ -326,21 +400,10 @@ module Async
|
|
326
400
|
@status = :completed
|
327
401
|
end
|
328
402
|
|
329
|
-
#
|
330
|
-
def failed!(exception = false
|
403
|
+
# State transition into the failed state.
|
404
|
+
def failed!(exception = false)
|
331
405
|
@result = exception
|
332
406
|
@status = :failed
|
333
|
-
|
334
|
-
if exception
|
335
|
-
if propagate
|
336
|
-
raise exception
|
337
|
-
elsif @finished.nil?
|
338
|
-
# If no one has called wait, we log this as a warning:
|
339
|
-
Console::Event::Failure.for(exception).emit(self, "Task may have ended with unhandled exception.", severity: :warn)
|
340
|
-
else
|
341
|
-
Console::Event::Failure.for(exception).emit(self, severity: :debug)
|
342
|
-
end
|
343
|
-
end
|
344
407
|
end
|
345
408
|
|
346
409
|
def stopped!
|
@@ -371,30 +434,26 @@ module Async
|
|
371
434
|
|
372
435
|
def schedule(&block)
|
373
436
|
@fiber = Fiber.new(annotation: self.annotation) do
|
374
|
-
set!
|
375
|
-
|
376
437
|
begin
|
377
438
|
completed!(yield)
|
378
|
-
# Console.debug(self) {"Task was completed with #{@children.size} children!"}
|
379
439
|
rescue Stop
|
380
440
|
stopped!
|
381
441
|
rescue StandardError => error
|
382
|
-
failed!(error
|
442
|
+
failed!(error)
|
383
443
|
rescue Exception => exception
|
384
|
-
failed!(exception
|
444
|
+
failed!(exception)
|
445
|
+
|
446
|
+
# This is a critical failure, we should stop the reactor:
|
447
|
+
raise
|
385
448
|
ensure
|
386
449
|
# Console.info(self) {"Task ensure $! = #{$!} with #{@children&.size.inspect} children!"}
|
387
450
|
finish!
|
388
451
|
end
|
389
452
|
end
|
390
453
|
|
454
|
+
@fiber.async_task = self
|
455
|
+
|
391
456
|
self.root.resume(@fiber)
|
392
457
|
end
|
393
|
-
|
394
|
-
# Set the current fiber's `:async_task` to this task.
|
395
|
-
def set!
|
396
|
-
# This is actually fiber-local:
|
397
|
-
Thread.current[:async_task] = self
|
398
|
-
end
|
399
458
|
end
|
400
459
|
end
|
data/lib/async/variable.rb
CHANGED
@@ -1,17 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2021-
|
4
|
+
# Copyright, 2021-2024, by Samuel Williams.
|
5
5
|
|
6
|
-
require_relative
|
6
|
+
require_relative "condition"
|
7
7
|
|
8
8
|
module Async
|
9
|
+
# A synchronization primitive that allows one task to wait for another task to resolve a value.
|
9
10
|
class Variable
|
11
|
+
# Create a new variable.
|
12
|
+
#
|
13
|
+
# @parameter condition [Condition] The condition to use for synchronization.
|
10
14
|
def initialize(condition = Condition.new)
|
11
15
|
@condition = condition
|
12
16
|
@value = nil
|
13
17
|
end
|
14
18
|
|
19
|
+
# Resolve the value.
|
20
|
+
#
|
21
|
+
# Signals all waiting tasks.
|
22
|
+
#
|
23
|
+
# @parameter value [Object] The value to resolve.
|
15
24
|
def resolve(value = true)
|
16
25
|
@value = value
|
17
26
|
condition = @condition
|
@@ -22,17 +31,29 @@ module Async
|
|
22
31
|
condition.signal(value)
|
23
32
|
end
|
24
33
|
|
34
|
+
# Alias for {#resolve}.
|
35
|
+
def value=(value)
|
36
|
+
self.resolve(value)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Whether the value has been resolved.
|
40
|
+
#
|
41
|
+
# @returns [Boolean] Whether the value has been resolved.
|
25
42
|
def resolved?
|
26
43
|
@condition.nil?
|
27
44
|
end
|
28
45
|
|
29
|
-
|
46
|
+
# Wait for the value to be resolved.
|
47
|
+
#
|
48
|
+
# @returns [Object] The resolved value.
|
49
|
+
def wait
|
30
50
|
@condition&.wait
|
31
51
|
return @value
|
32
52
|
end
|
33
53
|
|
34
|
-
|
35
|
-
|
54
|
+
# Alias for {#wait}.
|
55
|
+
def value
|
56
|
+
self.wait
|
36
57
|
end
|
37
58
|
end
|
38
59
|
end
|
data/lib/async/version.rb
CHANGED
data/lib/async/waiter.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2022, by Samuel Williams.
|
4
|
+
# Copyright, 2022-2024, by Samuel Williams.
|
5
|
+
# Copyright, 2024, by Patrik Wenger.
|
5
6
|
|
6
7
|
module Async
|
7
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}.
|
8
9
|
class Waiter
|
10
|
+
# Create a waiter instance.
|
11
|
+
#
|
12
|
+
# @parameter parent [Interface(:async) | Nil] The parent task to use for asynchronous operations.
|
13
|
+
# @parameter finished [Async::Condition] The condition to signal when a task completes.
|
9
14
|
def initialize(parent: nil, finished: Async::Condition.new)
|
10
15
|
@finished = finished
|
11
16
|
@done = []
|
@@ -15,8 +20,8 @@ module Async
|
|
15
20
|
|
16
21
|
# Execute a child task and add it to the waiter.
|
17
22
|
# @asynchronous Executes the given block concurrently.
|
18
|
-
def async(parent: (@parent or Task.current), &block)
|
19
|
-
parent.async do |task|
|
23
|
+
def async(parent: (@parent or Task.current), **options, &block)
|
24
|
+
parent.async(**options) do |task|
|
20
25
|
yield(task)
|
21
26
|
ensure
|
22
27
|
@done << task
|
@@ -0,0 +1,182 @@
|
|
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.19* 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
|
data/lib/async/wrapper.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2017-
|
4
|
+
# Copyright, 2017-2024, by Samuel Williams.
|
5
5
|
# Copyright, 2017, by Kent Gruber.
|
6
6
|
|
7
|
+
warn "Async::Wrapper is deprecated and will be removed on 2025-03-31. Please use native interfaces instead.", uplevel: 1, category: :deprecated
|
8
|
+
|
7
9
|
module Async
|
8
10
|
# Represents an asynchronous IO within a reactor.
|
9
11
|
# @deprecated With no replacement. Prefer native interfaces.
|
@@ -23,6 +25,7 @@ module Async
|
|
23
25
|
|
24
26
|
attr_accessor :reactor
|
25
27
|
|
28
|
+
# Dup the underlying IO.
|
26
29
|
def dup
|
27
30
|
self.class.new(@io.dup)
|
28
31
|
end
|
@@ -51,11 +54,12 @@ module Async
|
|
51
54
|
@io.to_io.wait(::IO::READABLE|::IO::WRITABLE|::IO::PRIORITY, timeout) or raise TimeoutError
|
52
55
|
end
|
53
56
|
|
54
|
-
# Close the
|
57
|
+
# Close the underlying IO.
|
55
58
|
def close
|
56
59
|
@io.close
|
57
60
|
end
|
58
61
|
|
62
|
+
# Whether the underlying IO is closed.
|
59
63
|
def closed?
|
60
64
|
@io.closed?
|
61
65
|
end
|
data/lib/async.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2017-
|
4
|
+
# Copyright, 2017-2024, by Samuel Williams.
|
5
5
|
# Copyright, 2020, by Salim Semaoune.
|
6
6
|
|
7
7
|
require_relative "async/version"
|
@@ -10,5 +10,6 @@ require_relative "async/reactor"
|
|
10
10
|
require_relative "kernel/async"
|
11
11
|
require_relative "kernel/sync"
|
12
12
|
|
13
|
+
# Asynchronous programming framework.
|
13
14
|
module Async
|
14
15
|
end
|