async 1.30.2 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/async/reactor.rb CHANGED
@@ -1,6 +1,4 @@
1
- # frozen_string_literal: true
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,326 +18,22 @@
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
- # Raised if a timeout occurs on a specific Fiber. Handled gracefully by `Task`.
34
- class TimeoutError < StandardError
35
- end
36
-
37
- # An asynchronous, cooperatively scheduled event reactor.
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
- attr :scheduler
97
- attr :logger
98
-
99
- # @reentrant Not thread safe.
100
- def block(blocker, timeout)
101
- fiber = Fiber.current
102
-
103
- if timeout
104
- timer = @timers.after(timeout) do
105
- if fiber.alive?
106
- fiber.resume(false)
107
- end
108
- end
109
- end
110
-
111
- begin
112
- @blocked += 1
113
- Task.yield
114
- ensure
115
- @blocked -= 1
116
- end
117
- ensure
118
- timer&.cancel
119
- end
120
-
121
- # @reentrant Thread safe.
122
- def unblock(blocker, fiber)
123
- @guard.synchronize do
124
- @unblocked << fiber
125
- @selector.wakeup
126
- end
127
- end
128
-
129
- def fiber(&block)
130
- if @scheduler
131
- Fiber.new(blocking: false, &block)
132
- else
133
- Fiber.new(&block)
134
- end
135
- end
136
-
137
- def to_s
138
- "\#<#{self.description} #{@children&.size || 0} children (#{stopped? ? 'stopped' : 'running'})>"
139
- end
140
-
141
- def stopped?
142
- @children.nil?
143
- end
144
-
145
- # Start an asynchronous task within the specified reactor. The task will be
146
- # executed until the first blocking call, at which point it will yield and
147
- # and this method will return.
148
- #
149
- # This is the main entry point for scheduling asynchronus tasks.
150
- #
151
- # @yield [Task] Executed within the task.
152
- # @return [Task] The task that was scheduled into the reactor.
153
- def async(*arguments, **options, &block)
154
- task = Task.new(self, **options, &block)
155
-
156
- # I want to take a moment to explain the logic of this.
157
- # When calling an async block, we deterministically execute it until the
158
- # first blocking operation. We don't *have* to do this - we could schedule
159
- # it for later execution, but it's useful to:
160
- # - Fail at the point of the method call where possible.
161
- # - Execute determinstically where possible.
162
- # - Avoid scheduler overhead if no blocking operation is performed.
163
- task.run(*arguments)
164
-
165
- # Console.logger.debug "Initial execution of task #{fiber} complete (#{result} -> #{fiber.alive?})..."
166
- return task
167
- end
168
-
169
- def register(io, interest, value = Fiber.current)
170
- monitor = @selector.register(io, interest)
171
- monitor.value = value
172
-
173
- return monitor
174
- end
175
-
176
- # Interrupt the reactor at the earliest convenience. Can be called from a different thread safely.
177
- def interrupt
178
- @guard.synchronize do
179
- unless @interrupted
180
- @interrupted = true
181
- @selector.wakeup
182
- end
183
- end
184
- end
185
-
186
- # Schedule a fiber (or equivalent object) to be resumed on the next loop through the reactor.
187
- # @param fiber [#resume] The object to be resumed on the next iteration of the run-loop.
188
- def << fiber
189
- @ready << fiber
190
- end
191
-
192
- # Yield the current fiber and resume it on the next iteration of the event loop.
193
- def yield(fiber = Fiber.current)
194
- @ready << fiber
195
-
196
- Task.yield
197
- end
198
-
199
- def finished?
200
- # TODO I'm not sure if checking `@running.empty?` is really required.
201
- super && @ready.empty? && @running.empty? && @blocked.zero?
202
- end
203
-
204
- # Run one iteration of the event loop.
205
- # @param timeout [Float | nil] the maximum timeout, or if nil, indefinite.
206
- # @return [Boolean] whether there is more work to do.
207
- def run_once(timeout = nil)
208
- # Console.logger.debug(self) {"@ready = #{@ready} @running = #{@running}"}
209
-
210
- if @ready.any?
211
- # running used to correctly answer on `finished?`, and to reuse Array object.
212
- @running, @ready = @ready, @running
213
-
214
- @running.each do |fiber|
215
- fiber.resume if fiber.alive?
216
- end
217
-
218
- @running.clear
219
- end
220
-
221
- if @unblocked.any?
222
- unblocked = Array.new
223
-
224
- @guard.synchronize do
225
- unblocked, @unblocked = @unblocked, unblocked
226
- end
227
-
228
- while fiber = unblocked.pop
229
- fiber.resume if fiber.alive?
230
- end
231
- end
232
-
233
- if @ready.empty?
234
- interval = @timers.wait_interval
235
- else
236
- # if there are tasks ready to execute, don't sleep:
237
- interval = 0
238
- end
239
-
240
- # If we are finished, we stop the task tree and exit:
241
- if self.finished?
242
- return false
243
- end
244
-
245
- # If there is no interval to wait (thus no timers), and no tasks, we could be done:
246
- if interval.nil?
247
- # Allow the user to specify a maximum interval if we would otherwise be sleeping indefinitely:
248
- interval = timeout
249
- elsif interval < 0
250
- # We have timers ready to fire, don't sleep in the selctor:
251
- interval = 0
252
- elsif timeout and interval > timeout
253
- interval = timeout
254
- end
255
-
256
- # Console.logger.info(self) {"Selecting with #{@children&.size} children with interval = #{interval ? interval.round(2) : 'infinite'}..."}
257
- if monitors = @selector.select(interval)
258
- monitors.each do |monitor|
259
- monitor.value.resume
260
- end
261
- end
262
-
263
- @timers.fire
264
-
265
- # We check and clear the interrupted flag here:
266
- if @interrupted
267
- @guard.synchronize do
268
- @interrupted = false
269
- end
270
-
271
- return false
272
- end
273
-
274
- # The reactor still has work to do:
275
- return true
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(...)
276
29
  end
277
30
 
278
- # Run the reactor until all tasks are finished. Proxies arguments to {#async} immediately before entering the loop, if a block is provided.
279
- def run(*arguments, **options, &block)
280
- raise RuntimeError, 'Reactor has been closed' if @selector.nil?
281
-
282
- @scheduler&.set!
283
-
284
- initial_task = self.async(*arguments, **options, &block) if block_given?
285
-
286
- while self.run_once
287
- # Round and round we go!
288
- end
31
+ def initialize(...)
32
+ super
289
33
 
290
- return initial_task
291
- ensure
292
- @scheduler&.clear!
293
- Console.logger.debug(self) {"Exiting run-loop because #{$! ? $! : 'finished'}."}
34
+ Fiber.set_scheduler(self)
294
35
  end
295
36
 
296
- # Stop each of the children tasks and close the selector.
297
- def close
298
- # 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:
299
- self.terminate
300
-
301
- @selector.close
302
- @selector = nil
303
- end
304
-
305
- # Check if the selector has been closed.
306
- # @returns [Boolean]
307
- def closed?
308
- @selector.nil?
309
- end
310
-
311
- # Put the calling fiber to sleep for a given ammount of time.
312
- # @parameter duration [Numeric] The time in seconds, to sleep for.
313
- def sleep(duration)
314
- fiber = Fiber.current
315
-
316
- timer = @timers.after(duration) do
317
- if fiber.alive?
318
- fiber.resume
319
- end
320
- end
321
-
322
- Task.yield
323
- ensure
324
- timer.cancel if timer
325
- end
326
-
327
- # 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.
328
- # @param duration [Numeric] The time in seconds, in which the task should
329
- # complete.
330
- def with_timeout(timeout, exception = TimeoutError)
331
- fiber = Fiber.current
332
-
333
- timer = @timers.after(timeout) do
334
- if fiber.alive?
335
- error = exception.new("execution expired")
336
- fiber.resume(error)
337
- end
338
- end
339
-
340
- yield timer
341
- ensure
342
- timer.cancel if timer
343
- end
37
+ public :sleep
344
38
  end
345
39
  end
@@ -19,94 +19,287 @@
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
- class Scheduler
25
- if Fiber.respond_to?(:set_scheduler)
26
- def self.supported?
27
- true
28
- end
29
- else
30
- def self.supported?
31
- false
32
- end
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
33
37
  end
34
38
 
35
- def initialize(reactor)
36
- @reactor = reactor
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
37
48
  end
38
49
 
39
- attr :wrappers
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
66
+ end
40
67
 
41
- def set!
42
- Fiber.set_scheduler(self)
68
+ # @returns [Boolean] Whether the scheduler has been closed.
69
+ # @public Since `stable-v1`.
70
+ def closed?
71
+ @selector.nil?
43
72
  end
44
73
 
45
- def clear!
46
- Fiber.set_scheduler(nil)
74
+ def to_s
75
+ "\#<#{self.description} #{@children&.size || 0} children (#{stopped? ? 'stopped' : 'running'})>"
47
76
  end
48
77
 
49
- private def from_io(io)
50
- Wrapper.new(io, @reactor)
78
+ # Interrupt the event loop and cause it to exit.
79
+ def interrupt
80
+ @interrupted = true
81
+ @selector.wakeup
51
82
  end
52
83
 
53
- def io_wait(io, events, timeout = nil)
54
- wrapper = from_io(io)
84
+ # Transfer from the calling fiber to the event loop.
85
+ def transfer
86
+ @selector.transfer
87
+ end
88
+
89
+ # Yield the current fiber and resume it on the next iteration of the event loop.
90
+ def yield
91
+ @selector.yield
92
+ end
93
+
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 events == ::IO::READABLE
57
- if wrapper.wait_readable(timeout)
58
- return ::IO::READABLE
59
- end
60
- elsif events == ::IO::WRITABLE
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
- if wrapper.wait_any(timeout)
66
- return events
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
- return false
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 nil
175
+ return false
73
176
  ensure
74
- wrapper&.reactor = nil
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
- Thread.new do
83
- ::Process::Status.wait(pid, flags)
84
- end.value
196
+ return @selector.process_wait(Fiber.current, pid, flags)
85
197
  end
86
198
 
87
- def kernel_sleep(duration)
88
- self.block(nil, duration)
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
- def block(blocker, timeout)
92
- @reactor.block(blocker, timeout)
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
- def unblock(blocker, fiber)
96
- @reactor.unblock(blocker, fiber)
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 close
279
+ def fiber(...)
280
+ return async(...).fiber
100
281
  end
101
282
 
102
- def fiber(&block)
103
- task = Task.new(@reactor, &block)
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 with_timeout(duration, exception = TimeoutError, message = "execution expired", &block)
286
+ fiber = Fiber.current
104
287
 
105
- fiber = task.fiber
106
-
107
- task.run
288
+ timer = @timers.after(duration) do
289
+ if fiber.alive?
290
+ fiber.raise(exception, message)
291
+ end
292
+ end
108
293
 
109
- return fiber
294
+ yield timer
295
+ ensure
296
+ timer.cancel if timer
297
+ end
298
+
299
+ def timeout_after(duration, exception, message, &block)
300
+ with_timeout(duration, exception, message) do |timer|
301
+ yield duration
302
+ end
110
303
  end
111
304
  end
112
305
  end