async 2.12.1 → 2.16.1
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/condition.rb +8 -3
- data/lib/async/idler.rb +20 -1
- data/lib/async/list.rb +7 -4
- data/lib/async/node.rb +46 -4
- data/lib/async/notification.rb +3 -1
- data/lib/async/queue.rb +67 -16
- data/lib/async/reactor.rb +3 -1
- data/lib/async/scheduler.rb +86 -50
- data/lib/async/task.rb +90 -47
- data/lib/async/variable.rb +20 -5
- data/lib/async/version.rb +1 -1
- data/lib/async/waiter.rb +8 -3
- data/lib/async/wrapper.rb +4 -2
- data/lib/async.rb +2 -1
- data/lib/kernel/async.rb +2 -0
- data/lib/kernel/sync.rb +2 -0
- data/license.md +2 -1
- data/readme.md +28 -30
- data/releases.md +24 -0
- data.tar.gz.sig +0 -0
- metadata +7 -11
- metadata.gz.sig +0 -0
data/lib/async/scheduler.rb
CHANGED
@@ -16,7 +16,11 @@ require 'resolv'
|
|
16
16
|
module Async
|
17
17
|
# Handles scheduling of fibers. Implements the fiber scheduler interface.
|
18
18
|
class Scheduler < Node
|
19
|
+
# Raised when an operation is attempted on a closed scheduler.
|
19
20
|
class ClosedError < RuntimeError
|
21
|
+
# Create a new error.
|
22
|
+
#
|
23
|
+
# @parameter message [String] The error message.
|
20
24
|
def initialize(message = "Scheduler is closed!")
|
21
25
|
super
|
22
26
|
end
|
@@ -28,6 +32,11 @@ module Async
|
|
28
32
|
true
|
29
33
|
end
|
30
34
|
|
35
|
+
# Create a new scheduler.
|
36
|
+
#
|
37
|
+
# @public Since `stable-v1`.
|
38
|
+
# @parameter parent [Node | Nil] The parent node to use for task hierarchy.
|
39
|
+
# @parameter selector [IO::Event::Selector] The selector to use for event handling.
|
31
40
|
def initialize(parent = nil, selector: nil)
|
32
41
|
super(parent)
|
33
42
|
|
@@ -62,43 +71,46 @@ module Async
|
|
62
71
|
return @busy_time / total_time
|
63
72
|
end
|
64
73
|
end
|
65
|
-
|
66
|
-
|
74
|
+
|
75
|
+
# Invoked when the fiber scheduler is being closed.
|
76
|
+
#
|
77
|
+
# Executes the run loop until all tasks are finished, then closes the scheduler.
|
78
|
+
def scheduler_close(error = $!)
|
67
79
|
# If the execution context (thread) was handling an exception, we want to exit as quickly as possible:
|
68
|
-
unless
|
80
|
+
unless error
|
69
81
|
self.run
|
70
82
|
end
|
71
83
|
ensure
|
72
84
|
self.close
|
73
85
|
end
|
74
86
|
|
75
|
-
# Terminate
|
87
|
+
# Terminate all child tasks.
|
76
88
|
def terminate
|
77
|
-
|
78
|
-
|
89
|
+
# If that doesn't work, take more serious action:
|
90
|
+
@children&.each do |child|
|
91
|
+
child.terminate
|
79
92
|
end
|
93
|
+
|
94
|
+
return @children.nil?
|
80
95
|
end
|
81
96
|
|
97
|
+
# Terminate all child tasks and close the scheduler.
|
82
98
|
# @public Since `stable-v1`.
|
83
99
|
def close
|
84
|
-
|
85
|
-
|
86
|
-
|
100
|
+
self.run_loop do
|
101
|
+
until self.terminate
|
102
|
+
self.run_once!
|
103
|
+
end
|
87
104
|
end
|
88
105
|
|
89
106
|
Kernel.raise "Closing scheduler with blocked operations!" if @blocked > 0
|
90
|
-
|
91
|
-
# We depend on GVL for consistency:
|
92
|
-
# @guard.synchronize do
|
93
|
-
|
107
|
+
ensure
|
94
108
|
# We want `@selector = nil` to be a visible side effect from this point forward, specifically in `#interrupt` and `#unblock`. If the selector is closed, then we don't want to push any fibers to it.
|
95
109
|
selector = @selector
|
96
110
|
@selector = nil
|
97
111
|
|
98
112
|
selector&.close
|
99
113
|
|
100
|
-
# end
|
101
|
-
|
102
114
|
consume
|
103
115
|
end
|
104
116
|
|
@@ -108,6 +120,7 @@ module Async
|
|
108
120
|
@selector.nil?
|
109
121
|
end
|
110
122
|
|
123
|
+
# @returns [String] A description of the scheduler.
|
111
124
|
def to_s
|
112
125
|
"\#<#{self.description} #{@children&.size || 0} children (#{stopped? ? 'stopped' : 'running'})>"
|
113
126
|
end
|
@@ -135,10 +148,20 @@ module Async
|
|
135
148
|
@selector.push(fiber)
|
136
149
|
end
|
137
150
|
|
138
|
-
|
139
|
-
|
151
|
+
# Raise an exception on a specified fiber with the given arguments.
|
152
|
+
#
|
153
|
+
# This internally schedules the current fiber to be ready, before raising the exception, so that it will later resume execution.
|
154
|
+
#
|
155
|
+
# @parameter fiber [Fiber] The fiber to raise the exception on.
|
156
|
+
# @parameter *arguments [Array] The arguments to pass to the fiber.
|
157
|
+
def raise(...)
|
158
|
+
@selector.raise(...)
|
140
159
|
end
|
141
160
|
|
161
|
+
# Resume execution of the specified fiber.
|
162
|
+
#
|
163
|
+
# @parameter fiber [Fiber] The fiber to resume.
|
164
|
+
# @parameter arguments [Array] The arguments to pass to the fiber.
|
142
165
|
def resume(fiber, *arguments)
|
143
166
|
@selector.resume(fiber, *arguments)
|
144
167
|
end
|
@@ -218,7 +241,7 @@ module Async
|
|
218
241
|
elsif timeout = get_timeout(io)
|
219
242
|
# Otherwise, if we default to the io's timeout, we raise an exception:
|
220
243
|
timer = @timers.after(timeout) do
|
221
|
-
fiber.raise(::IO::TimeoutError, "Timeout while waiting for IO to become ready!")
|
244
|
+
fiber.raise(::IO::TimeoutError, "Timeout (#{timeout}s) while waiting for IO to become ready!")
|
222
245
|
end
|
223
246
|
end
|
224
247
|
|
@@ -233,7 +256,7 @@ module Async
|
|
233
256
|
|
234
257
|
if timeout = get_timeout(io)
|
235
258
|
timer = @timers.after(timeout) do
|
236
|
-
fiber.raise(::IO::TimeoutError, "Timeout while waiting for IO to become readable!")
|
259
|
+
fiber.raise(::IO::TimeoutError, "Timeout (#{timeout}s) while waiting for IO to become readable!")
|
237
260
|
end
|
238
261
|
end
|
239
262
|
|
@@ -248,7 +271,7 @@ module Async
|
|
248
271
|
|
249
272
|
if timeout = get_timeout(io)
|
250
273
|
timer = @timers.after(timeout) do
|
251
|
-
fiber.raise(::IO::TimeoutError, "Timeout while waiting for IO to become writable!")
|
274
|
+
fiber.raise(::IO::TimeoutError, "Timeout (#{timeout}s) while waiting for IO to become writable!")
|
252
275
|
end
|
253
276
|
end
|
254
277
|
|
@@ -268,28 +291,13 @@ module Async
|
|
268
291
|
return @selector.process_wait(Fiber.current, pid, flags)
|
269
292
|
end
|
270
293
|
|
271
|
-
# Run one iteration of the event loop.
|
272
|
-
# Does not handle interrupts.
|
273
|
-
# @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite.
|
274
|
-
# @returns [Boolean] Whether there is more work to do.
|
275
|
-
def run_once(timeout = nil)
|
276
|
-
Kernel::raise "Running scheduler on non-blocking fiber!" unless Fiber.blocking?
|
277
|
-
|
278
|
-
# If we are finished, we stop the task tree and exit:
|
279
|
-
if self.finished?
|
280
|
-
return false
|
281
|
-
end
|
282
|
-
|
283
|
-
return run_once!(timeout)
|
284
|
-
end
|
285
|
-
|
286
294
|
# Run one iteration of the event loop.
|
287
295
|
#
|
288
296
|
# 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.
|
289
297
|
#
|
290
298
|
# @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite.
|
291
299
|
# @returns [Boolean] Whether there is more work to do.
|
292
|
-
private def run_once!(timeout =
|
300
|
+
private def run_once!(timeout = nil)
|
293
301
|
start_time = Async::Clock.now
|
294
302
|
|
295
303
|
interval = @timers.wait_interval
|
@@ -326,6 +334,25 @@ module Async
|
|
326
334
|
return true
|
327
335
|
end
|
328
336
|
|
337
|
+
# Run one iteration of the event loop.
|
338
|
+
# Does not handle interrupts.
|
339
|
+
# @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite.
|
340
|
+
# @returns [Boolean] Whether there is more work to do.
|
341
|
+
def run_once(timeout = nil)
|
342
|
+
Kernel.raise "Running scheduler on non-blocking fiber!" unless Fiber.blocking?
|
343
|
+
|
344
|
+
if self.finished?
|
345
|
+
self.stop
|
346
|
+
end
|
347
|
+
|
348
|
+
# If we are finished, we stop the task tree and exit:
|
349
|
+
if @children.nil?
|
350
|
+
return false
|
351
|
+
end
|
352
|
+
|
353
|
+
return run_once!(timeout)
|
354
|
+
end
|
355
|
+
|
329
356
|
# Checks and clears the interrupted state of the scheduler.
|
330
357
|
# @returns [Boolean] Whether the reactor has been interrupted.
|
331
358
|
private def interrupted?
|
@@ -341,22 +368,22 @@ module Async
|
|
341
368
|
return false
|
342
369
|
end
|
343
370
|
|
344
|
-
#
|
345
|
-
def
|
346
|
-
|
347
|
-
|
348
|
-
|
371
|
+
# Stop all children, including transient children, ignoring any signals.
|
372
|
+
def stop
|
373
|
+
@children&.each do |child|
|
374
|
+
child.stop
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
private def run_loop(&block)
|
349
379
|
interrupt = nil
|
350
380
|
|
351
381
|
begin
|
352
382
|
# In theory, we could use Exception here to be a little bit safer, but we've only shown the case for SignalException to be a problem, so let's not over-engineer this.
|
353
383
|
Thread.handle_interrupt(::SignalException => :never) do
|
354
|
-
|
355
|
-
# If we are interrupted, we need to exit:
|
356
|
-
break if self.interrupted?
|
357
|
-
|
384
|
+
until self.interrupted?
|
358
385
|
# If we are finished, we need to exit:
|
359
|
-
break unless
|
386
|
+
break unless yield
|
360
387
|
end
|
361
388
|
end
|
362
389
|
rescue Interrupt => interrupt
|
@@ -368,11 +395,20 @@ module Async
|
|
368
395
|
end
|
369
396
|
|
370
397
|
# If the event loop was interrupted, and we finished exiting normally (due to the interrupt), we need to re-raise the interrupt so that the caller can handle it too.
|
371
|
-
Kernel.raise
|
398
|
+
Kernel.raise(interrupt) if interrupt
|
399
|
+
end
|
400
|
+
|
401
|
+
# Run the reactor until all tasks are finished. Proxies arguments to {#async} immediately before entering the loop, if a block is provided.
|
402
|
+
def run(...)
|
403
|
+
Kernel.raise ClosedError if @selector.nil?
|
404
|
+
|
405
|
+
initial_task = self.async(...) if block_given?
|
406
|
+
|
407
|
+
self.run_loop do
|
408
|
+
run_once
|
409
|
+
end
|
372
410
|
|
373
411
|
return initial_task
|
374
|
-
ensure
|
375
|
-
Console.debug(self) {"Exiting run-loop because #{$! ? $! : 'finished'}."}
|
376
412
|
end
|
377
413
|
|
378
414
|
# Start an asynchronous task within the specified reactor. The task will be
|
@@ -385,7 +421,7 @@ module Async
|
|
385
421
|
# @returns [Task] The task that was scheduled into the reactor.
|
386
422
|
# @deprecated With no replacement.
|
387
423
|
def async(*arguments, **options, &block)
|
388
|
-
Kernel
|
424
|
+
Kernel.raise ClosedError if @selector.nil?
|
389
425
|
|
390
426
|
task = Task.new(Task.current? || self, **options, &block)
|
391
427
|
|
data/lib/async/task.rb
CHANGED
@@ -13,18 +13,26 @@ require 'console/event/failure'
|
|
13
13
|
require_relative 'node'
|
14
14
|
require_relative 'condition'
|
15
15
|
|
16
|
+
Fiber.attr_accessor :async_task
|
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
|
@@ -34,6 +42,9 @@ module Async
|
|
34
42
|
# Raised if a timeout occurs on a specific Fiber. Handled gracefully by `Task`.
|
35
43
|
# @public Since `stable-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
|
@@ -41,7 +52,11 @@ module Async
|
|
41
52
|
|
42
53
|
# @public Since `stable-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,49 @@ 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 complete? completed?
|
154
185
|
|
155
|
-
# @
|
186
|
+
# @attribute [Symbol] The status of the execution of the task, one of `:initialized`, `:running`, `:complete`, `:stopped` or `:failed`.
|
156
187
|
attr :status
|
157
188
|
|
158
189
|
# Begin the execution of the task.
|
190
|
+
#
|
191
|
+
# @raises [RuntimeError] If the task is already running.
|
159
192
|
def run(*arguments)
|
160
193
|
if @status == :initialized
|
161
194
|
@status = :running
|
162
195
|
|
163
196
|
schedule do
|
164
197
|
@block.call(self, *arguments)
|
198
|
+
rescue => error
|
199
|
+
# 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.
|
200
|
+
if @finished.nil?
|
201
|
+
Console::Event::Failure.for(error).emit(self, "Task may have ended with unhandled exception.", severity: :warn)
|
202
|
+
else
|
203
|
+
# Console::Event::Failure.for(error).emit(self, severity: :debug)
|
204
|
+
end
|
205
|
+
|
206
|
+
raise
|
165
207
|
end
|
166
208
|
else
|
167
209
|
raise RuntimeError, "Task already running!"
|
@@ -169,6 +211,9 @@ module Async
|
|
169
211
|
end
|
170
212
|
|
171
213
|
# Run an asynchronous task as a child of the current task.
|
214
|
+
#
|
215
|
+
# @raises [FinishedError] If the task has already finished.
|
216
|
+
# @returns [Task] The child task.
|
172
217
|
def async(*arguments, **options, &block)
|
173
218
|
raise FinishedError if self.finished?
|
174
219
|
|
@@ -215,15 +260,18 @@ module Async
|
|
215
260
|
return stopped!
|
216
261
|
end
|
217
262
|
|
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
263
|
# If the fiber is alive, we need to stop it:
|
226
264
|
if @fiber&.alive?
|
265
|
+
# As the task is now exiting, we want to ensure the event loop continues to execute until the task finishes.
|
266
|
+
self.transient = false
|
267
|
+
|
268
|
+
# If we are deferring stop...
|
269
|
+
if @defer_stop == false
|
270
|
+
# Don't stop now... but update the state so we know we need to stop later.
|
271
|
+
@defer_stop = true
|
272
|
+
return false
|
273
|
+
end
|
274
|
+
|
227
275
|
if self.current?
|
228
276
|
# 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
277
|
if later
|
@@ -238,7 +286,7 @@ module Async
|
|
238
286
|
begin
|
239
287
|
# 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
288
|
Fiber.scheduler.raise(@fiber, Stop)
|
241
|
-
rescue FiberError
|
289
|
+
rescue FiberError => error
|
242
290
|
# In some cases, this can cause a FiberError (it might be resumed already), so we schedule it to be stopped later:
|
243
291
|
Fiber.scheduler.push(Stop::Later.new(self))
|
244
292
|
end
|
@@ -263,19 +311,23 @@ module Async
|
|
263
311
|
# - false: defer_stop has been called and we are not stopping.
|
264
312
|
# - true: defer_stop has been called and we will stop when exiting the block.
|
265
313
|
if @defer_stop.nil?
|
266
|
-
# If we are not deferring stop already, we can defer it now:
|
267
|
-
@defer_stop = false
|
268
|
-
|
269
314
|
begin
|
315
|
+
# If we are not deferring stop already, we can defer it now:
|
316
|
+
@defer_stop = false
|
317
|
+
|
270
318
|
yield
|
271
319
|
rescue Stop
|
272
320
|
# If we are exiting due to a stop, we shouldn't try to invoke stop again:
|
273
321
|
@defer_stop = nil
|
274
322
|
raise
|
275
323
|
ensure
|
324
|
+
defer_stop = @defer_stop
|
325
|
+
|
326
|
+
# We need to ensure the state is reset before we exit the block:
|
327
|
+
@defer_stop = nil
|
328
|
+
|
276
329
|
# If we were asked to stop, we should do so now:
|
277
|
-
if
|
278
|
-
@defer_stop = nil
|
330
|
+
if defer_stop
|
279
331
|
raise Stop, "Stopping current task (was deferred)!"
|
280
332
|
end
|
281
333
|
end
|
@@ -285,19 +337,25 @@ module Async
|
|
285
337
|
end
|
286
338
|
end
|
287
339
|
|
340
|
+
# @returns [Boolean] Whether stop has been deferred.
|
341
|
+
def stop_deferred?
|
342
|
+
@defer_stop
|
343
|
+
end
|
344
|
+
|
288
345
|
# Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
|
289
346
|
# @returns [Task]
|
290
347
|
# @raises[RuntimeError] If task was not {set!} for the current fiber.
|
291
348
|
def self.current
|
292
|
-
|
349
|
+
Fiber.current.async_task or raise RuntimeError, "No async task available!"
|
293
350
|
end
|
294
351
|
|
295
352
|
# Check if there is a task defined for the current fiber.
|
296
|
-
# @returns [
|
353
|
+
# @returns [Interface(:async) | Nil]
|
297
354
|
def self.current?
|
298
|
-
|
355
|
+
Fiber.current.async_task
|
299
356
|
end
|
300
357
|
|
358
|
+
# @returns [Boolean] Whether this task is the currently executing task.
|
301
359
|
def current?
|
302
360
|
Fiber.current.equal?(@fiber)
|
303
361
|
end
|
@@ -326,21 +384,10 @@ module Async
|
|
326
384
|
@status = :completed
|
327
385
|
end
|
328
386
|
|
329
|
-
#
|
330
|
-
def failed!(exception = false
|
387
|
+
# State transition into the failed state.
|
388
|
+
def failed!(exception = false)
|
331
389
|
@result = exception
|
332
390
|
@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
391
|
end
|
345
392
|
|
346
393
|
def stopped!
|
@@ -371,30 +418,26 @@ module Async
|
|
371
418
|
|
372
419
|
def schedule(&block)
|
373
420
|
@fiber = Fiber.new(annotation: self.annotation) do
|
374
|
-
set!
|
375
|
-
|
376
421
|
begin
|
377
422
|
completed!(yield)
|
378
|
-
# Console.debug(self) {"Task was completed with #{@children.size} children!"}
|
379
423
|
rescue Stop
|
380
424
|
stopped!
|
381
425
|
rescue StandardError => error
|
382
|
-
failed!(error
|
426
|
+
failed!(error)
|
383
427
|
rescue Exception => exception
|
384
|
-
failed!(exception
|
428
|
+
failed!(exception)
|
429
|
+
|
430
|
+
# This is a critical failure, we should stop the reactor:
|
431
|
+
raise
|
385
432
|
ensure
|
386
433
|
# Console.info(self) {"Task ensure $! = #{$!} with #{@children&.size.inspect} children!"}
|
387
434
|
finish!
|
388
435
|
end
|
389
436
|
end
|
390
437
|
|
438
|
+
@fiber.async_task = self
|
439
|
+
|
391
440
|
self.root.resume(@fiber)
|
392
441
|
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
442
|
end
|
400
443
|
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
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,23 @@ module Async
|
|
22
31
|
condition.signal(value)
|
23
32
|
end
|
24
33
|
|
34
|
+
alias value= resolve
|
35
|
+
|
36
|
+
# Whether the value has been resolved.
|
37
|
+
#
|
38
|
+
# @returns [Boolean] Whether the value has been resolved.
|
25
39
|
def resolved?
|
26
40
|
@condition.nil?
|
27
41
|
end
|
28
42
|
|
29
|
-
|
43
|
+
# Wait for the value to be resolved.
|
44
|
+
#
|
45
|
+
# @returns [Object] The resolved value.
|
46
|
+
def wait
|
30
47
|
@condition&.wait
|
31
48
|
return @value
|
32
49
|
end
|
33
50
|
|
34
|
-
|
35
|
-
self.value
|
36
|
-
end
|
51
|
+
alias value wait
|
37
52
|
end
|
38
53
|
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
|