async 1.32.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/async/barrier.md +36 -0
- data/lib/async/barrier.rb +14 -6
- data/lib/async/clock.rb +11 -0
- data/lib/async/condition.md +31 -0
- data/lib/async/condition.rb +27 -16
- data/lib/async/node.rb +27 -12
- data/lib/async/notification.rb +4 -4
- data/lib/async/queue.rb +8 -21
- data/lib/async/reactor.rb +11 -320
- data/lib/async/scheduler.rb +235 -48
- data/lib/async/semaphore.md +41 -0
- data/lib/async/semaphore.rb +8 -24
- data/lib/async/task.rb +68 -114
- data/lib/async/version.rb +1 -1
- data/lib/async/wrapper.rb +19 -179
- data/lib/async.rb +0 -5
- data/lib/kernel/async.rb +26 -2
- data/lib/kernel/sync.rb +14 -4
- metadata +10 -10
- data/lib/async/debug/monitor.rb +0 -47
- data/lib/async/debug/selector.rb +0 -82
- data/lib/async/logger.rb +0 -28
data/lib/async/reactor.rb
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
# Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
1
|
+
# Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
4
2
|
#
|
5
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
4
|
# of this software and associated documentation files (the "Software"), to deal
|
@@ -20,330 +18,23 @@
|
|
20
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
19
|
# THE SOFTWARE.
|
22
20
|
|
23
|
-
require_relative 'logger'
|
24
|
-
require_relative 'task'
|
25
|
-
require_relative 'wrapper'
|
26
21
|
require_relative 'scheduler'
|
27
22
|
|
28
|
-
require 'nio'
|
29
|
-
require 'timers'
|
30
|
-
require 'forwardable'
|
31
|
-
|
32
23
|
module Async
|
33
|
-
#
|
34
|
-
class
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
class Reactor < Node
|
39
|
-
extend Forwardable
|
40
|
-
|
41
|
-
# The preferred method to invoke asynchronous behavior at the top level.
|
42
|
-
#
|
43
|
-
# - When invoked within an existing reactor task, it will run the given block
|
44
|
-
# asynchronously. Will return the task once it has been scheduled.
|
45
|
-
# - When invoked at the top level, will create and run a reactor, and invoke
|
46
|
-
# the block as an asynchronous task. Will block until the reactor finishes
|
47
|
-
# running.
|
48
|
-
def self.run(*arguments, **options, &block)
|
49
|
-
if current = Task.current?
|
50
|
-
return current.async(*arguments, **options, &block)
|
51
|
-
else
|
52
|
-
reactor = self.new
|
53
|
-
|
54
|
-
begin
|
55
|
-
return reactor.run(*arguments, **options, &block)
|
56
|
-
ensure
|
57
|
-
reactor.close
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def self.selector
|
63
|
-
if backend = ENV['ASYNC_BACKEND']&.to_sym
|
64
|
-
if NIO::Selector.backends.include?(backend)
|
65
|
-
return NIO::Selector.new(backend)
|
66
|
-
else
|
67
|
-
warn "Could not find ASYNC_BACKEND=#{backend}!"
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
return NIO::Selector.new
|
72
|
-
end
|
73
|
-
|
74
|
-
def initialize(parent = nil, selector: self.class.selector, logger: nil)
|
75
|
-
super(parent)
|
76
|
-
|
77
|
-
@selector = selector
|
78
|
-
@timers = Timers::Group.new
|
79
|
-
@logger = logger
|
80
|
-
|
81
|
-
@ready = []
|
82
|
-
@running = []
|
83
|
-
|
84
|
-
if Scheduler.supported?
|
85
|
-
@scheduler = Scheduler.new(self)
|
86
|
-
else
|
87
|
-
@scheduler = nil
|
88
|
-
end
|
89
|
-
|
90
|
-
@interrupted = false
|
91
|
-
@guard = Mutex.new
|
92
|
-
@blocked = 0
|
93
|
-
@unblocked = []
|
94
|
-
end
|
95
|
-
|
96
|
-
def inspect
|
97
|
-
"#<#{self.class} children=#{@children&.size} stopped=#{stopped?}>"
|
98
|
-
end
|
99
|
-
|
100
|
-
attr :scheduler
|
101
|
-
attr :logger
|
102
|
-
|
103
|
-
# @reentrant Not thread safe.
|
104
|
-
def block(blocker, timeout)
|
105
|
-
fiber = Fiber.current
|
106
|
-
|
107
|
-
if timeout
|
108
|
-
timer = @timers.after(timeout) do
|
109
|
-
if fiber.alive?
|
110
|
-
fiber.resume(false)
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
begin
|
116
|
-
@blocked += 1
|
117
|
-
Task.yield
|
118
|
-
ensure
|
119
|
-
@blocked -= 1
|
120
|
-
end
|
121
|
-
ensure
|
122
|
-
timer&.cancel
|
123
|
-
end
|
124
|
-
|
125
|
-
# @reentrant Thread safe.
|
126
|
-
def unblock(blocker, fiber)
|
127
|
-
@guard.synchronize do
|
128
|
-
@unblocked << fiber
|
129
|
-
@selector.wakeup
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
def fiber(&block)
|
134
|
-
if @scheduler
|
135
|
-
Fiber.new(blocking: false, &block)
|
136
|
-
else
|
137
|
-
Fiber.new(&block)
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def to_s
|
142
|
-
"\#<#{self.description} #{@children&.size || 0} children (#{stopped? ? 'stopped' : 'running'})>"
|
143
|
-
end
|
144
|
-
|
145
|
-
def stopped?
|
146
|
-
@children.nil?
|
147
|
-
end
|
148
|
-
|
149
|
-
# Start an asynchronous task within the specified reactor. The task will be
|
150
|
-
# executed until the first blocking call, at which point it will yield and
|
151
|
-
# and this method will return.
|
152
|
-
#
|
153
|
-
# This is the main entry point for scheduling asynchronus tasks.
|
154
|
-
#
|
155
|
-
# @yield [Task] Executed within the task.
|
156
|
-
# @return [Task] The task that was scheduled into the reactor.
|
157
|
-
def async(*arguments, **options, &block)
|
158
|
-
task = Task.new(self, **options, &block)
|
159
|
-
|
160
|
-
# I want to take a moment to explain the logic of this.
|
161
|
-
# When calling an async block, we deterministically execute it until the
|
162
|
-
# first blocking operation. We don't *have* to do this - we could schedule
|
163
|
-
# it for later execution, but it's useful to:
|
164
|
-
# - Fail at the point of the method call where possible.
|
165
|
-
# - Execute determinstically where possible.
|
166
|
-
# - Avoid scheduler overhead if no blocking operation is performed.
|
167
|
-
task.run(*arguments)
|
168
|
-
|
169
|
-
# Console.logger.debug "Initial execution of task #{fiber} complete (#{result} -> #{fiber.alive?})..."
|
170
|
-
return task
|
171
|
-
end
|
172
|
-
|
173
|
-
def register(io, interest, value = Fiber.current)
|
174
|
-
monitor = @selector.register(io, interest)
|
175
|
-
monitor.value = value
|
176
|
-
|
177
|
-
return monitor
|
24
|
+
# A wrapper around the the scheduler which binds it to the current thread automatically.
|
25
|
+
class Reactor < Scheduler
|
26
|
+
# @deprecated Replaced by {Kernel::Async}.
|
27
|
+
def self.run(...)
|
28
|
+
Async(...)
|
178
29
|
end
|
179
30
|
|
180
|
-
|
181
|
-
|
182
|
-
@guard.synchronize do
|
183
|
-
unless @interrupted
|
184
|
-
@interrupted = true
|
185
|
-
@selector.wakeup
|
186
|
-
end
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
# Schedule a fiber (or equivalent object) to be resumed on the next loop through the reactor.
|
191
|
-
# @param fiber [#resume] The object to be resumed on the next iteration of the run-loop.
|
192
|
-
def << fiber
|
193
|
-
@ready << fiber
|
194
|
-
end
|
195
|
-
|
196
|
-
# Yield the current fiber and resume it on the next iteration of the event loop.
|
197
|
-
def yield(fiber = Fiber.current)
|
198
|
-
@ready << fiber
|
199
|
-
|
200
|
-
Task.yield
|
201
|
-
end
|
202
|
-
|
203
|
-
def finished?
|
204
|
-
# TODO I'm not sure if checking `@running.empty?` is really required.
|
205
|
-
super && @ready.empty? && @running.empty? && @blocked.zero?
|
206
|
-
end
|
207
|
-
|
208
|
-
# Run one iteration of the event loop.
|
209
|
-
# @param timeout [Float | nil] the maximum timeout, or if nil, indefinite.
|
210
|
-
# @return [Boolean] whether there is more work to do.
|
211
|
-
def run_once(timeout = nil)
|
212
|
-
# Console.logger.debug(self) {"@ready = #{@ready} @running = #{@running}"}
|
31
|
+
def initialize(...)
|
32
|
+
super
|
213
33
|
|
214
|
-
|
215
|
-
# running used to correctly answer on `finished?`, and to reuse Array object.
|
216
|
-
@running, @ready = @ready, @running
|
217
|
-
|
218
|
-
@running.each do |fiber|
|
219
|
-
fiber.resume if fiber.alive?
|
220
|
-
end
|
221
|
-
|
222
|
-
@running.clear
|
223
|
-
end
|
224
|
-
|
225
|
-
if @unblocked.any?
|
226
|
-
unblocked = Array.new
|
227
|
-
|
228
|
-
@guard.synchronize do
|
229
|
-
unblocked, @unblocked = @unblocked, unblocked
|
230
|
-
end
|
231
|
-
|
232
|
-
while fiber = unblocked.pop
|
233
|
-
fiber.resume if fiber.alive?
|
234
|
-
end
|
235
|
-
end
|
236
|
-
|
237
|
-
if @ready.empty?
|
238
|
-
interval = @timers.wait_interval
|
239
|
-
else
|
240
|
-
# if there are tasks ready to execute, don't sleep:
|
241
|
-
interval = 0
|
242
|
-
end
|
243
|
-
|
244
|
-
# If we are finished, we stop the task tree and exit:
|
245
|
-
if self.finished?
|
246
|
-
return false
|
247
|
-
end
|
248
|
-
|
249
|
-
# If there is no interval to wait (thus no timers), and no tasks, we could be done:
|
250
|
-
if interval.nil?
|
251
|
-
# Allow the user to specify a maximum interval if we would otherwise be sleeping indefinitely:
|
252
|
-
interval = timeout
|
253
|
-
elsif interval < 0
|
254
|
-
# We have timers ready to fire, don't sleep in the selctor:
|
255
|
-
interval = 0
|
256
|
-
elsif timeout and interval > timeout
|
257
|
-
interval = timeout
|
258
|
-
end
|
259
|
-
|
260
|
-
# Console.logger.info(self) {"Selecting with #{@children&.size} children with interval = #{interval ? interval.round(2) : 'infinite'}..."}
|
261
|
-
if monitors = @selector.select(interval)
|
262
|
-
monitors.each do |monitor|
|
263
|
-
monitor.value.resume
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
@timers.fire
|
268
|
-
|
269
|
-
# We check and clear the interrupted flag here:
|
270
|
-
if @interrupted
|
271
|
-
@guard.synchronize do
|
272
|
-
@interrupted = false
|
273
|
-
end
|
274
|
-
|
275
|
-
return false
|
276
|
-
end
|
277
|
-
|
278
|
-
# The reactor still has work to do:
|
279
|
-
return true
|
34
|
+
Fiber.set_scheduler(self)
|
280
35
|
end
|
281
36
|
|
282
|
-
|
283
|
-
|
284
|
-
raise RuntimeError, 'Reactor has been closed' if @selector.nil?
|
285
|
-
|
286
|
-
@scheduler&.set!
|
287
|
-
|
288
|
-
initial_task = self.async(*arguments, **options, &block) if block_given?
|
289
|
-
|
290
|
-
while self.run_once
|
291
|
-
# Round and round we go!
|
292
|
-
end
|
293
|
-
|
294
|
-
return initial_task
|
295
|
-
ensure
|
296
|
-
@scheduler&.clear!
|
297
|
-
Console.logger.debug(self) {"Exiting run-loop because #{$! ? $! : 'finished'}."}
|
298
|
-
end
|
299
|
-
|
300
|
-
# Stop each of the children tasks and close the selector.
|
301
|
-
def close
|
302
|
-
# This is a critical step. Because tasks could be stored as instance variables, and since the reactor is (probably) going out of scope, we need to ensure they are stopped. Otherwise, the tasks will belong to a reactor that will never run again and are not stopped:
|
303
|
-
self.terminate
|
304
|
-
|
305
|
-
@selector.close
|
306
|
-
@selector = nil
|
307
|
-
end
|
308
|
-
|
309
|
-
# Check if the selector has been closed.
|
310
|
-
# @returns [Boolean]
|
311
|
-
def closed?
|
312
|
-
@selector.nil?
|
313
|
-
end
|
314
|
-
|
315
|
-
# Put the calling fiber to sleep for a given ammount of time.
|
316
|
-
# @parameter duration [Numeric] The time in seconds, to sleep for.
|
317
|
-
def sleep(duration)
|
318
|
-
fiber = Fiber.current
|
319
|
-
|
320
|
-
timer = @timers.after(duration) do
|
321
|
-
if fiber.alive?
|
322
|
-
fiber.resume
|
323
|
-
end
|
324
|
-
end
|
325
|
-
|
326
|
-
Task.yield
|
327
|
-
ensure
|
328
|
-
timer.cancel if timer
|
329
|
-
end
|
330
|
-
|
331
|
-
# Invoke the block, but after the specified timeout, raise {TimeoutError} in any currenly blocking operation. If the block runs to completion before the timeout occurs or there are no non-blocking operations after the timeout expires, the code will complete without any exception.
|
332
|
-
# @param duration [Numeric] The time in seconds, in which the task should
|
333
|
-
# complete.
|
334
|
-
def with_timeout(timeout, exception = TimeoutError)
|
335
|
-
fiber = Fiber.current
|
336
|
-
|
337
|
-
timer = @timers.after(timeout) do
|
338
|
-
if fiber.alive?
|
339
|
-
error = exception.new("execution expired")
|
340
|
-
fiber.resume(error)
|
341
|
-
end
|
342
|
-
end
|
343
|
-
|
344
|
-
yield timer
|
345
|
-
ensure
|
346
|
-
timer.cancel if timer
|
347
|
-
end
|
37
|
+
alias with_timeout timeout_after
|
38
|
+
public :sleep
|
348
39
|
end
|
349
40
|
end
|
data/lib/async/scheduler.rb
CHANGED
@@ -19,94 +19,281 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
require_relative 'clock'
|
22
|
+
require_relative 'task'
|
23
|
+
|
24
|
+
require 'io/event'
|
25
|
+
|
26
|
+
require 'console'
|
27
|
+
require 'timers'
|
28
|
+
require 'resolv'
|
22
29
|
|
23
30
|
module Async
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
31
|
+
# Handles scheduling of fibers. Implements the fiber scheduler interface.
|
32
|
+
class Scheduler < Node
|
33
|
+
# Whether the fiber scheduler is supported.
|
34
|
+
# @public Since `stable-v1`.
|
35
|
+
def self.supported?
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(parent = nil, selector: nil)
|
40
|
+
super(parent)
|
41
|
+
|
42
|
+
@selector = selector || ::IO::Event::Selector.new(Fiber.current)
|
43
|
+
@interrupted = false
|
44
|
+
|
45
|
+
@blocked = 0
|
46
|
+
|
47
|
+
@timers = ::Timers::Group.new
|
48
|
+
end
|
49
|
+
|
50
|
+
# @public Since `stable-v1`.
|
51
|
+
def close
|
52
|
+
# This is a critical step. Because tasks could be stored as instance variables, and since the reactor is (probably) going out of scope, we need to ensure they are stopped. Otherwise, the tasks will belong to a reactor that will never run again and are not stopped.
|
53
|
+
self.terminate
|
54
|
+
|
55
|
+
Kernel::raise "Closing scheduler with blocked operations!" if @blocked > 0
|
56
|
+
|
57
|
+
# We depend on GVL for consistency:
|
58
|
+
# @guard.synchronize do
|
59
|
+
|
60
|
+
@selector&.close
|
61
|
+
@selector = nil
|
62
|
+
|
63
|
+
# end
|
64
|
+
|
65
|
+
consume
|
33
66
|
end
|
34
67
|
|
35
|
-
|
36
|
-
|
68
|
+
# @returns [Boolean] Whether the scheduler has been closed.
|
69
|
+
# @public Since `stable-v1`.
|
70
|
+
def closed?
|
71
|
+
@selector.nil?
|
37
72
|
end
|
38
73
|
|
39
|
-
|
74
|
+
def to_s
|
75
|
+
"\#<#{self.description} #{@children&.size || 0} children (#{stopped? ? 'stopped' : 'running'})>"
|
76
|
+
end
|
40
77
|
|
41
|
-
|
42
|
-
|
78
|
+
# Interrupt the event loop and cause it to exit.
|
79
|
+
def interrupt
|
80
|
+
@interrupted = true
|
81
|
+
@selector.wakeup
|
43
82
|
end
|
44
83
|
|
45
|
-
|
46
|
-
|
84
|
+
# Transfer from the calling fiber to the event loop.
|
85
|
+
def transfer
|
86
|
+
@selector.transfer
|
47
87
|
end
|
48
88
|
|
49
|
-
|
50
|
-
|
89
|
+
# Yield the current fiber and resume it on the next iteration of the event loop.
|
90
|
+
def yield
|
91
|
+
@selector.yield
|
51
92
|
end
|
52
93
|
|
53
|
-
|
54
|
-
|
94
|
+
# Schedule a fiber (or equivalent object) to be resumed on the next loop through the reactor.
|
95
|
+
# @parameter fiber [Fiber | Object] The object to be resumed on the next iteration of the run-loop.
|
96
|
+
def push(fiber)
|
97
|
+
@selector.push(fiber)
|
98
|
+
end
|
99
|
+
|
100
|
+
def raise(*arguments)
|
101
|
+
@selector.raise(*arguments)
|
102
|
+
end
|
103
|
+
|
104
|
+
def resume(fiber, *arguments)
|
105
|
+
if Fiber.scheduler
|
106
|
+
@selector.resume(fiber, *arguments)
|
107
|
+
else
|
108
|
+
@selector.push(fiber)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Invoked when a fiber tries to perform a blocking operation which cannot continue. A corresponding call {unblock} must be performed to allow this fiber to continue.
|
113
|
+
# @asynchronous May only be called on same thread as fiber scheduler.
|
114
|
+
def block(blocker, timeout)
|
115
|
+
# $stderr.puts "block(#{blocker}, #{Fiber.current}, #{timeout})"
|
116
|
+
fiber = Fiber.current
|
55
117
|
|
56
|
-
if
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
if wrapper.wait_writable(timeout)
|
62
|
-
return ::IO::WRITABLE
|
118
|
+
if timeout
|
119
|
+
timer = @timers.after(timeout) do
|
120
|
+
if fiber.alive?
|
121
|
+
fiber.transfer(false)
|
122
|
+
end
|
63
123
|
end
|
124
|
+
end
|
125
|
+
|
126
|
+
begin
|
127
|
+
@blocked += 1
|
128
|
+
@selector.transfer
|
129
|
+
ensure
|
130
|
+
@blocked -= 1
|
131
|
+
end
|
132
|
+
ensure
|
133
|
+
timer&.cancel
|
134
|
+
end
|
135
|
+
|
136
|
+
# @asynchronous May be called from any thread.
|
137
|
+
def unblock(blocker, fiber)
|
138
|
+
# $stderr.puts "unblock(#{blocker}, #{fiber})"
|
139
|
+
|
140
|
+
# This operation is protected by the GVL:
|
141
|
+
@selector.push(fiber)
|
142
|
+
@selector.wakeup
|
143
|
+
end
|
144
|
+
|
145
|
+
# @asynchronous May be non-blocking..
|
146
|
+
def kernel_sleep(duration = nil)
|
147
|
+
if duration
|
148
|
+
self.block(nil, duration)
|
64
149
|
else
|
65
|
-
|
66
|
-
|
150
|
+
self.transfer
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# @asynchronous May be non-blocking..
|
155
|
+
def address_resolve(hostname)
|
156
|
+
::Resolv.getaddresses(hostname)
|
157
|
+
end
|
158
|
+
|
159
|
+
# @asynchronous May be non-blocking..
|
160
|
+
def io_wait(io, events, timeout = nil)
|
161
|
+
fiber = Fiber.current
|
162
|
+
|
163
|
+
if timeout
|
164
|
+
timer = @timers.after(timeout) do
|
165
|
+
fiber.raise(TimeoutError)
|
67
166
|
end
|
68
167
|
end
|
69
168
|
|
70
|
-
|
169
|
+
# Console.logger.info(self, "-> io_wait", fiber, io, events)
|
170
|
+
events = @selector.io_wait(fiber, io, events)
|
171
|
+
# Console.logger.info(self, "<- io_wait", fiber, io, events)
|
172
|
+
|
173
|
+
return events
|
71
174
|
rescue TimeoutError
|
72
|
-
return
|
175
|
+
return false
|
73
176
|
ensure
|
74
|
-
|
177
|
+
timer&.cancel
|
178
|
+
end
|
179
|
+
|
180
|
+
if IO.const_defined?(:Buffer)
|
181
|
+
def io_read(io, buffer, length)
|
182
|
+
@selector.io_read(Fiber.current, io, buffer, length)
|
183
|
+
end
|
184
|
+
|
185
|
+
def io_write(io, buffer, length)
|
186
|
+
@selector.io_write(Fiber.current, io, buffer, length)
|
187
|
+
end
|
75
188
|
end
|
76
189
|
|
77
190
|
# Wait for the specified process ID to exit.
|
78
191
|
# @parameter pid [Integer] The process ID to wait for.
|
79
192
|
# @parameter flags [Integer] A bit-mask of flags suitable for `Process::Status.wait`.
|
80
193
|
# @returns [Process::Status] A process status instance.
|
194
|
+
# @asynchronous May be non-blocking..
|
81
195
|
def process_wait(pid, flags)
|
82
|
-
|
83
|
-
::Process::Status.wait(pid, flags)
|
84
|
-
end.value
|
196
|
+
return @selector.process_wait(Fiber.current, pid, flags)
|
85
197
|
end
|
86
198
|
|
87
|
-
|
88
|
-
|
199
|
+
# Run one iteration of the event loop.
|
200
|
+
# @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite.
|
201
|
+
# @returns [Boolean] Whether there is more work to do.
|
202
|
+
def run_once(timeout = nil)
|
203
|
+
Kernel::raise "Running scheduler on non-blocking fiber!" unless Fiber.blocking?
|
204
|
+
|
205
|
+
# If we are finished, we stop the task tree and exit:
|
206
|
+
if self.finished?
|
207
|
+
return false
|
208
|
+
end
|
209
|
+
|
210
|
+
interval = @timers.wait_interval
|
211
|
+
|
212
|
+
# If there is no interval to wait (thus no timers), and no tasks, we could be done:
|
213
|
+
if interval.nil?
|
214
|
+
# Allow the user to specify a maximum interval if we would otherwise be sleeping indefinitely:
|
215
|
+
interval = timeout
|
216
|
+
elsif interval < 0
|
217
|
+
# We have timers ready to fire, don't sleep in the selctor:
|
218
|
+
interval = 0
|
219
|
+
elsif timeout and interval > timeout
|
220
|
+
interval = timeout
|
221
|
+
end
|
222
|
+
|
223
|
+
begin
|
224
|
+
@selector.select(interval)
|
225
|
+
rescue Errno::EINTR
|
226
|
+
# Ignore.
|
227
|
+
end
|
228
|
+
|
229
|
+
@timers.fire
|
230
|
+
|
231
|
+
# The reactor still has work to do:
|
232
|
+
return true
|
89
233
|
end
|
90
234
|
|
91
|
-
|
92
|
-
|
235
|
+
# Run the reactor until all tasks are finished. Proxies arguments to {#async} immediately before entering the loop, if a block is provided.
|
236
|
+
def run(...)
|
237
|
+
Kernel::raise RuntimeError, 'Reactor has been closed' if @selector.nil?
|
238
|
+
|
239
|
+
initial_task = self.async(...) if block_given?
|
240
|
+
|
241
|
+
@interrupted = false
|
242
|
+
|
243
|
+
while self.run_once
|
244
|
+
if @interrupted
|
245
|
+
break
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
return initial_task
|
250
|
+
ensure
|
251
|
+
Console.logger.debug(self) {"Exiting run-loop because #{$! ? $! : 'finished'}."}
|
93
252
|
end
|
94
253
|
|
95
|
-
|
96
|
-
|
254
|
+
# Start an asynchronous task within the specified reactor. The task will be
|
255
|
+
# executed until the first blocking call, at which point it will yield and
|
256
|
+
# and this method will return.
|
257
|
+
#
|
258
|
+
# This is the main entry point for scheduling asynchronus tasks.
|
259
|
+
#
|
260
|
+
# @yields {|task| ...} Executed within the task.
|
261
|
+
# @returns [Task] The task that was scheduled into the reactor.
|
262
|
+
# @deprecated With no replacement.
|
263
|
+
def async(*arguments, **options, &block)
|
264
|
+
task = Task.new(Task.current? || self, **options, &block)
|
265
|
+
|
266
|
+
# I want to take a moment to explain the logic of this.
|
267
|
+
# When calling an async block, we deterministically execute it until the
|
268
|
+
# first blocking operation. We don't *have* to do this - we could schedule
|
269
|
+
# it for later execution, but it's useful to:
|
270
|
+
# - Fail at the point of the method call where possible.
|
271
|
+
# - Execute determinstically where possible.
|
272
|
+
# - Avoid scheduler overhead if no blocking operation is performed.
|
273
|
+
task.run(*arguments)
|
274
|
+
|
275
|
+
# Console.logger.debug "Initial execution of task #{fiber} complete (#{result} -> #{fiber.alive?})..."
|
276
|
+
return task
|
97
277
|
end
|
98
278
|
|
99
|
-
def
|
279
|
+
def fiber(...)
|
280
|
+
return async(...).fiber
|
100
281
|
end
|
101
282
|
|
102
|
-
|
103
|
-
|
283
|
+
# Invoke the block, but after the specified timeout, raise {TimeoutError} in any currenly blocking operation. If the block runs to completion before the timeout occurs or there are no non-blocking operations after the timeout expires, the code will complete without any exception.
|
284
|
+
# @parameter duration [Numeric] The time in seconds, in which the task should complete.
|
285
|
+
def timeout_after(timeout, exception = TimeoutError, message = "execution expired", &block)
|
286
|
+
fiber = Fiber.current
|
104
287
|
|
105
|
-
|
106
|
-
|
107
|
-
|
288
|
+
timer = @timers.after(timeout) do
|
289
|
+
if fiber.alive?
|
290
|
+
fiber.raise(exception, message)
|
291
|
+
end
|
292
|
+
end
|
108
293
|
|
109
|
-
|
294
|
+
yield timer
|
295
|
+
ensure
|
296
|
+
timer.cancel if timer
|
110
297
|
end
|
111
298
|
end
|
112
299
|
end
|