async 1.32.1 → 2.0.0

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,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
- # 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
- 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
- # Interrupt the reactor at the earliest convenience. Can be called from a different thread safely.
181
- def interrupt
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
- if @ready.any?
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
- # Run the reactor until all tasks are finished. Proxies arguments to {#async} immediately before entering the loop, if a block is provided.
283
- def run(*arguments, **options, &block)
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
@@ -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
- 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
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
- def initialize(reactor)
36
- @reactor = reactor
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
- attr :wrappers
74
+ def to_s
75
+ "\#<#{self.description} #{@children&.size || 0} children (#{stopped? ? 'stopped' : 'running'})>"
76
+ end
40
77
 
41
- def set!
42
- Fiber.set_scheduler(self)
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
- def clear!
46
- Fiber.set_scheduler(nil)
84
+ # Transfer from the calling fiber to the event loop.
85
+ def transfer
86
+ @selector.transfer
47
87
  end
48
88
 
49
- private def from_io(io)
50
- Wrapper.new(io, @reactor)
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
- def io_wait(io, events, timeout = nil)
54
- wrapper = from_io(io)
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 timeout_after(timeout, exception = TimeoutError, message = "execution expired", &block)
286
+ fiber = Fiber.current
104
287
 
105
- fiber = task.fiber
106
-
107
- task.run
288
+ timer = @timers.after(timeout) 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
110
297
  end
111
298
  end
112
299
  end