async 1.30.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,41 @@
1
+ A synchronization primitive, which limits access to a given resource, such as a limited number of database connections, open files, or network connections.
2
+
3
+ ## Example
4
+
5
+ ~~~ ruby
6
+ require 'async'
7
+ require 'async/semaphore'
8
+ require 'net/http'
9
+
10
+ Sync do
11
+ # Only allow two concurrent tasks at a time:
12
+ semaphore = Async::Semaphore.new(2)
13
+
14
+ # Generate an array of 10 numbers:
15
+ terms = ['ruby', 'python', 'go', 'java', 'c++']
16
+
17
+ # Search for the terms:
18
+ terms.each do |term|
19
+ semaphore.async do |task|
20
+ Console.logger.info("Searching for #{term}...")
21
+ response = Net::HTTP.get(URI "https://www.google.com/search?q=#{term}")
22
+ Console.logger.info("Got response #{response.size} bytes.")
23
+ end
24
+ end
25
+ end
26
+ ~~~
27
+
28
+ ### Output
29
+
30
+ ~~~
31
+ 0.0s info: Searching for ruby... [ec=0x3c] [pid=50523]
32
+ 0.04s info: Searching for python... [ec=0x21c] [pid=50523]
33
+ 1.7s info: Got response 182435 bytes. [ec=0x3c] [pid=50523]
34
+ 1.71s info: Searching for go... [ec=0x834] [pid=50523]
35
+ 3.0s info: Got response 204854 bytes. [ec=0x21c] [pid=50523]
36
+ 3.0s info: Searching for java... [ec=0xf64] [pid=50523]
37
+ 4.32s info: Got response 103235 bytes. [ec=0x834] [pid=50523]
38
+ 4.32s info: Searching for c++... [ec=0x12d4] [pid=50523]
39
+ 4.65s info: Got response 109697 bytes. [ec=0xf64] [pid=50523]
40
+ 6.64s info: Got response 87249 bytes. [ec=0x12d4] [pid=50523]
41
+ ~~~
@@ -21,8 +21,11 @@
21
21
  # THE SOFTWARE.
22
22
 
23
23
  module Async
24
- # A semaphore is used to control access to a common resource in a concurrent system. A useful way to think of a semaphore as used in the real-world systems is as a record of how many units of a particular resource are available, coupled with operations to adjust that record safely (i.e. to avoid race conditions) as units are required or become free, and, if necessary, wait until a unit of the resource becomes available.
24
+ # A synchronization primitive, which limits access to a given resource.
25
+ # @public Since `stable-v1`.
25
26
  class Semaphore
27
+ # @parameter limit [Integer] The maximum number of times the semaphore can be acquired before it blocks.
28
+ # @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks.
26
29
  def initialize(limit = 1, parent: nil)
27
30
  @count = 0
28
31
  @limit = limit
@@ -67,8 +70,8 @@ module Async
67
70
 
68
71
  # Acquire the semaphore, block if we are at the limit.
69
72
  # If no block is provided, you must call release manually.
70
- # @yield when the semaphore can be acquired
71
- # @return the result of the block if invoked
73
+ # @yields {...} When the semaphore can be acquired.
74
+ # @returns The result of the block if invoked.
72
75
  def acquire
73
76
  wait
74
77
 
@@ -89,7 +92,7 @@ module Async
89
92
 
90
93
  while (@limit - @count) > 0 and fiber = @waiting.shift
91
94
  if fiber.alive?
92
- fiber.resume
95
+ Fiber.scheduler.resume(fiber)
93
96
  end
94
97
  end
95
98
  end
@@ -102,7 +105,7 @@ module Async
102
105
 
103
106
  if blocking?
104
107
  @waiting << fiber
105
- Task.yield while blocking?
108
+ Fiber.scheduler.transfer while blocking?
106
109
  end
107
110
  rescue Exception
108
111
  @waiting.delete(fiber)
data/lib/async/task.rb CHANGED
@@ -21,7 +21,6 @@
21
21
  # THE SOFTWARE.
22
22
 
23
23
  require 'fiber'
24
- require 'forwardable'
25
24
 
26
25
  require_relative 'node'
27
26
  require_relative 'condition'
@@ -38,55 +37,45 @@ module Async
38
37
  true
39
38
  end
40
39
 
41
- def resume
40
+ def transfer
42
41
  @task.stop
43
42
  end
44
43
  end
45
44
  end
46
45
 
47
- # A task represents the state associated with the execution of an asynchronous
48
- # block.
46
+ # Raised if a timeout occurs on a specific Fiber. Handled gracefully by `Task`.
47
+ # @public Since `stable-v1`.
48
+ class TimeoutError < StandardError
49
+ def initialize(message = "execution expired")
50
+ super
51
+ end
52
+ end
53
+
54
+ # Encapsulates the state of a running task and it's result.
55
+ # @public Since `stable-v1`.
49
56
  class Task < Node
50
- extend Forwardable
51
-
52
- # Yield the unerlying `result` for the task. If the result
53
- # is an Exception, then that result will be raised an its
54
- # exception.
55
- # @return [Object] result of the task
56
- # @raise [Exception] if the result is an exception
57
- # @yield [result] result of the task if a block if given.
57
+ # @deprecated With no replacement.
58
58
  def self.yield
59
- if block_given?
60
- result = yield
61
- else
62
- result = Fiber.yield
63
- end
64
-
65
- if result.is_a? Exception
66
- raise result
67
- else
68
- return result
69
- end
59
+ Fiber.scheduler.transfer
70
60
  end
71
61
 
72
62
  # Create a new task.
73
- # @param reactor [Async::Reactor] the reactor this task will run within.
74
- # @param parent [Async::Task] the parent task.
75
- def initialize(reactor, parent = Task.current?, logger: nil, finished: nil, **options, &block)
76
- super(parent || reactor, **options)
77
-
78
- @reactor = reactor
63
+ # @parameter reactor [Reactor] the reactor this task will run within.
64
+ # @parameter parent [Task] the parent task.
65
+ def initialize(parent = Task.current?, finished: nil, **options, &block)
66
+ super(parent, **options)
79
67
 
80
68
  @status = :initialized
81
69
  @result = nil
82
70
  @finished = finished
83
71
 
84
- @logger = logger || @parent.logger
85
-
86
- @fiber = make_fiber(&block)
72
+ @block = block
73
+ @fiber = nil
87
74
  end
88
75
 
89
- attr :logger
76
+ def reactor
77
+ self.root
78
+ end
90
79
 
91
80
  if Fiber.current.respond_to?(:backtrace)
92
81
  def backtrace(*arguments)
@@ -98,14 +87,19 @@ module Async
98
87
  "\#<#{self.description} (#{@status})>"
99
88
  end
100
89
 
101
- # @attr ios [Reactor] The reactor the task was created within.
102
- attr :reactor
90
+ # @deprecated Prefer {Kernel#sleep} except when compatibility with `stable-v1` is required.
91
+ def sleep(duration = nil)
92
+ super
93
+ end
103
94
 
104
- def_delegators :@reactor, :with_timeout, :sleep
95
+ # @deprecated Replaced by {Scheduler#timeout_after}.
96
+ def with_timeout(timeout, exception = TimeoutError, message = "execution expired", &block)
97
+ Fiber.scheduler.timeout_after(timeout, exception, message, &block)
98
+ end
105
99
 
106
100
  # Yield back to the reactor and allow other fibers to execute.
107
101
  def yield
108
- Task.yield{reactor.yield}
102
+ Fiber.scheduler.yield
109
103
  end
110
104
 
111
105
  # @attr fiber [Fiber] The fiber which is being used for the execution of this task.
@@ -123,14 +117,14 @@ module Async
123
117
  if @status == :initialized
124
118
  @status = :running
125
119
 
126
- @fiber.resume(*arguments)
120
+ schedule(arguments)
127
121
  else
128
122
  raise RuntimeError, "Task already running!"
129
123
  end
130
124
  end
131
125
 
132
126
  def async(*arguments, **options, &block)
133
- task = Task.new(@reactor, self, **options, &block)
127
+ task = Task.new(self, **options, &block)
134
128
 
135
129
  task.run(*arguments)
136
130
 
@@ -138,22 +132,26 @@ module Async
138
132
  end
139
133
 
140
134
  # Retrieve the current result of the task. Will cause the caller to wait until result is available.
141
- # @raise [RuntimeError] if the task's fiber is the current fiber.
142
- # @return [Object] the final expression/result of the task's block.
135
+ # @raises[RuntimeError] If the task's fiber is the current fiber.
136
+ # @returns [Object] The final expression/result of the task's block.
143
137
  def wait
144
- raise RuntimeError, "Cannot wait on own fiber" if Fiber.current.equal?(@fiber)
138
+ raise "Cannot wait on own fiber" if Fiber.current.equal?(@fiber)
145
139
 
146
140
  if running?
147
141
  @finished ||= Condition.new
148
142
  @finished.wait
143
+ end
144
+
145
+ case @result
146
+ when Exception
147
+ raise @result
149
148
  else
150
- Task.yield{@result}
149
+ return @result
151
150
  end
152
151
  end
153
152
 
154
- # Deprecated.
155
- alias result wait
156
- # Soon to become attr :result
153
+ # Access the result of the task without waiting. May be nil if the task is not completed.
154
+ attr :result
157
155
 
158
156
  # Stop the task and all of its children.
159
157
  def stop(later = false)
@@ -165,15 +163,15 @@ module Async
165
163
  if self.running?
166
164
  if self.current?
167
165
  if later
168
- @reactor << Stop::Later.new(self)
166
+ Fiber.scheduler.push Stop::Later.new(self)
169
167
  else
170
168
  raise Stop, "Stopping current task!"
171
169
  end
172
170
  elsif @fiber&.alive?
173
171
  begin
174
- @fiber.resume(Stop.new)
172
+ Fiber.scheduler.raise(@fiber, Stop)
175
173
  rescue FiberError
176
- @reactor << Stop::Later.new(self)
174
+ Fiber.scheduler.push Stop::Later.new(self)
177
175
  end
178
176
  end
179
177
  else
@@ -183,14 +181,14 @@ module Async
183
181
  end
184
182
 
185
183
  # Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
186
- # @return [Async::Task]
187
- # @raise [RuntimeError] if task was not {set!} for the current fiber.
184
+ # @returns [Task]
185
+ # @raises[RuntimeError] If task was not {set!} for the current fiber.
188
186
  def self.current
189
187
  Thread.current[:async_task] or raise RuntimeError, "No async task available!"
190
188
  end
191
189
 
192
190
  # Check if there is a task defined for the current fiber.
193
- # @return [Async::Task, nil]
191
+ # @returns [Task | Nil]
194
192
  def self.current?
195
193
  Thread.current[:async_task]
196
194
  end
@@ -200,13 +198,13 @@ module Async
200
198
  end
201
199
 
202
200
  # Check if the task is running.
203
- # @return [Boolean]
201
+ # @returns [Boolean]
204
202
  def running?
205
203
  @status == :running
206
204
  end
207
205
 
208
206
  # Whether we can remove this node from the reactor graph.
209
- # @return [Boolean]
207
+ # @returns [Boolean]
210
208
  def finished?
211
209
  super && @status != :running
212
210
  end
@@ -246,18 +244,18 @@ module Async
246
244
  end
247
245
 
248
246
  def stop!
249
- # logger.debug(self) {"Task was stopped with #{@children&.size.inspect} children!"}
247
+ # Console.logger.info(self, self.annotation) {"Task was stopped with #{@children&.size.inspect} children!"}
250
248
  @status = :stopped
251
249
 
252
250
  stop_children(true)
253
251
  end
254
252
 
255
- def make_fiber(&block)
256
- Fiber.new do |*arguments|
253
+ def schedule(arguments)
254
+ @fiber = Fiber.new do
257
255
  set!
258
256
 
259
257
  begin
260
- @result = yield(self, *arguments)
258
+ @result = @block.call(self, *arguments)
261
259
  @status = :complete
262
260
  # Console.logger.debug(self) {"Task was completed with #{@children.size} children!"}
263
261
  rescue Stop
@@ -267,10 +265,12 @@ module Async
267
265
  rescue Exception => exception
268
266
  fail!(exception, true)
269
267
  ensure
270
- # Console.logger.debug(self) {"Task ensure $!=#{$!} with #{@children.size} children!"}
268
+ # Console.logger.info(self) {"Task ensure $! = #{$!} with #{@children&.size.inspect} children!"}
271
269
  finish!
272
270
  end
273
271
  end
272
+
273
+ self.root.resume(@fiber)
274
274
  end
275
275
 
276
276
  # Finish the current task, and all bound bound IO objects.
@@ -291,7 +291,6 @@ module Async
291
291
  def set!
292
292
  # This is actually fiber-local:
293
293
  Thread.current[:async_task] = self
294
- Console.logger = @logger if @logger
295
294
  end
296
295
  end
297
296
  end
data/lib/async/version.rb CHANGED
@@ -21,5 +21,5 @@
21
21
  # THE SOFTWARE.
22
22
 
23
23
  module Async
24
- VERSION = "1.30.2"
24
+ VERSION = "2.0.0"
25
25
  end
data/lib/async/wrapper.rb CHANGED
@@ -20,220 +20,60 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
21
  # THE SOFTWARE.
22
22
 
23
- require 'nio'
24
-
25
23
  module Async
26
24
  # Represents an asynchronous IO within a reactor.
25
+ # @deprecated With no replacement. Prefer native interfaces.
27
26
  class Wrapper
27
+ # An exception that occurs when the asynchronous operation was cancelled.
28
28
  class Cancelled < StandardError
29
- class From
30
- def initialize
31
- @backtrace = caller[5..-1]
32
- end
33
-
34
- attr :backtrace
35
-
36
- def cause
37
- nil
38
- end
39
-
40
- def message
41
- "Cancelled"
42
- end
43
- end
44
-
45
- def initialize
46
- super "The operation has been cancelled!"
47
-
48
- @cause = From.new
49
- end
50
-
51
- attr :cause
52
29
  end
53
30
 
54
- # wait_readable, wait_writable and wait_any are not re-entrant, and will raise this failure.
55
- class WaitError < StandardError
56
- def initialize
57
- super "A fiber is already waiting!"
58
- end
59
- end
60
-
61
- # @param io the native object to wrap.
62
- # @param reactor [Reactor] the reactor that is managing this wrapper, or not specified, it's looked up by way of {Task.current}.
31
+ # @parameter io the native object to wrap.
32
+ # @parameter reactor [Reactor] the reactor that is managing this wrapper, or not specified, it's looked up by way of {Task.current}.
63
33
  def initialize(io, reactor = nil)
64
34
  @io = io
65
-
66
35
  @reactor = reactor
67
- @monitor = nil
68
36
 
69
- @readable = nil
70
- @writable = nil
71
- @any = nil
37
+ @timeout = nil
72
38
  end
73
39
 
74
- def dup
75
- self.class.new(@io.dup, @reactor)
76
- end
40
+ attr_accessor :reactor
77
41
 
78
- def resume(*arguments)
79
- # It's possible that the monitor was closed before calling resume.
80
- return unless @monitor
81
-
82
- readiness = @monitor.readiness
83
-
84
- if @readable and (readiness == :r or readiness == :rw)
85
- @readable.resume(*arguments)
86
- end
87
-
88
- if @writable and (readiness == :w or readiness == :rw)
89
- @writable.resume(*arguments)
90
- end
91
-
92
- if @any
93
- @any.resume(*arguments)
94
- end
42
+ def dup
43
+ self.class.new(@io.dup)
95
44
  end
96
45
 
97
46
  # The underlying native `io`.
98
47
  attr :io
99
48
 
100
- # The reactor this wrapper is associated with, if any.
101
- attr :reactor
102
-
103
- # The monitor for this wrapper, if any.
104
- attr :monitor
105
-
106
- # Bind this wrapper to a different reactor. Assign nil to convert to an unbound wrapper (can be used from any reactor/task but with slightly increased overhead.)
107
- # Binding to a reactor is purely a performance consideration. Generally, I don't like APIs that exist only due to optimisations. This is borderline, so consider this functionality semi-private.
108
- def reactor= reactor
109
- return if @reactor.equal?(reactor)
110
-
111
- cancel_monitor
112
-
113
- @reactor = reactor
49
+ # Wait for the io to become readable.
50
+ def wait_readable(timeout = @timeout)
51
+ @io.to_io.wait_readable(timeout) or raise TimeoutError
114
52
  end
115
53
 
116
- # Wait for the io to become readable.
117
- def wait_readable(timeout = nil)
118
- raise WaitError if @readable
119
-
120
- self.reactor = Task.current.reactor
121
-
122
- begin
123
- @readable = Fiber.current
124
- wait_for(timeout)
125
- ensure
126
- @readable = nil
127
- @monitor.interests = interests if @monitor
128
- end
54
+ # Wait for the io to become writable.
55
+ def wait_priority(timeout = @timeout)
56
+ @io.to_io.wait_priority(timeout) or raise TimeoutError
129
57
  end
130
58
 
131
59
  # Wait for the io to become writable.
132
- def wait_writable(timeout = nil)
133
- raise WaitError if @writable
134
-
135
- self.reactor = Task.current.reactor
136
-
137
- begin
138
- @writable = Fiber.current
139
- wait_for(timeout)
140
- ensure
141
- @writable = nil
142
- @monitor.interests = interests if @monitor
143
- end
60
+ def wait_writable(timeout = @timeout)
61
+ @io.to_io.wait_writable(timeout) or raise TimeoutError
144
62
  end
145
63
 
146
64
  # Wait fo the io to become either readable or writable.
147
- # @param duration [Float] timeout after the given duration if not `nil`.
148
- def wait_any(timeout = nil)
149
- raise WaitError if @any
150
-
151
- self.reactor = Task.current.reactor
152
-
153
- begin
154
- @any = Fiber.current
155
- wait_for(timeout)
156
- ensure
157
- @any = nil
158
- @monitor.interests = interests if @monitor
159
- end
65
+ # @parameter duration [Float] timeout after the given duration if not `nil`.
66
+ def wait_any(timeout = @timeout)
67
+ @io.wait_any(timeout) or raise TimeoutError
160
68
  end
161
69
 
162
70
  # Close the io and monitor.
163
71
  def close
164
- cancel_monitor
165
-
166
72
  @io.close
167
73
  end
168
74
 
169
75
  def closed?
170
76
  @io.closed?
171
77
  end
172
-
173
- private
174
-
175
- # What an abomination.
176
- def interests
177
- if @any
178
- return :rw
179
- elsif @readable
180
- if @writable
181
- return :rw
182
- else
183
- return :r
184
- end
185
- elsif @writable
186
- return :w
187
- end
188
-
189
- return nil
190
- end
191
-
192
- def cancel_monitor
193
- if @readable
194
- readable = @readable
195
- @readable = nil
196
-
197
- readable.resume(Cancelled.new)
198
- end
199
-
200
- if @writable
201
- writable = @writable
202
- @writable = nil
203
-
204
- writable.resume(Cancelled.new)
205
- end
206
-
207
- if @any
208
- any = @any
209
- @any = nil
210
-
211
- any.resume(Cancelled.new)
212
- end
213
-
214
- if @monitor
215
- @monitor.close
216
- @monitor = nil
217
- end
218
- end
219
-
220
- def wait_for(timeout)
221
- if @monitor
222
- @monitor.interests = interests
223
- else
224
- @monitor = @reactor.register(@io, interests, self)
225
- end
226
-
227
- # If the user requested an explicit timeout for this operation:
228
- if timeout
229
- @reactor.with_timeout(timeout) do
230
- Task.yield
231
- end
232
- else
233
- Task.yield
234
- end
235
-
236
- return true
237
- end
238
78
  end
239
79
  end
data/lib/async.rb CHANGED
@@ -21,15 +21,10 @@
21
21
  # THE SOFTWARE.
22
22
 
23
23
  require_relative "async/version"
24
- require_relative "async/logger"
25
24
  require_relative "async/reactor"
26
25
 
27
26
  require_relative "kernel/async"
28
27
  require_relative "kernel/sync"
29
28
 
30
29
  module Async
31
- # Invoke `Reactor.run` with all arguments/block.
32
- def self.run(*arguments, &block)
33
- Reactor.run(*arguments, &block)
34
- end
35
30
  end
data/lib/kernel/async.rb CHANGED
@@ -24,7 +24,31 @@ require_relative "../async/reactor"
24
24
 
25
25
  module Kernel
26
26
  # Run the given block of code in a task, asynchronously, creating a reactor if necessary.
27
- def Async(*arguments, **options, &block)
28
- ::Async::Reactor.run(*arguments, **options, &block)
27
+ #
28
+ # The preferred method to invoke asynchronous behavior at the top level.
29
+ #
30
+ # - When invoked within an existing reactor task, it will run the given block
31
+ # asynchronously. Will return the task once it has been scheduled.
32
+ # - When invoked at the top level, will create and run a reactor, and invoke
33
+ # the block as an asynchronous task. Will block until the reactor finishes
34
+ # running.
35
+ #
36
+ # @yields {|task| ...} The block that will execute asynchronously.
37
+ # @parameter task [Async::Task] The task that is executing the given block.
38
+ #
39
+ # @public Since `stable-v1`.
40
+ # @asynchronous May block until given block completes executing.
41
+ def Async(...)
42
+ if current = ::Async::Task.current?
43
+ return current.async(...)
44
+ else
45
+ reactor = ::Async::Reactor.new
46
+
47
+ begin
48
+ return reactor.run(...)
49
+ ensure
50
+ Fiber.set_scheduler(nil)
51
+ end
52
+ end
29
53
  end
30
54
  end
data/lib/kernel/sync.rb CHANGED
@@ -22,16 +22,26 @@
22
22
 
23
23
  require_relative "../async/reactor"
24
24
 
25
+ # Extensions to all Ruby objects.
25
26
  module Kernel
26
27
  # Run the given block of code synchronously, but within a reactor if not already in one.
28
+ #
29
+ # @yields {|task| ...} The block that will execute asynchronously.
30
+ # @parameter task [Async::Task] The task that is executing the given block.
31
+ #
32
+ # @public Since `stable-v1`.
33
+ # @asynchronous Will block until given block completes executing.
27
34
  def Sync(&block)
28
35
  if task = ::Async::Task.current?
29
36
  yield task
30
37
  else
31
- ::Async::Reactor.run(
32
- finished: ::Async::Condition.new,
33
- &block
34
- ).wait
38
+ reactor = Async::Reactor.new
39
+
40
+ begin
41
+ return reactor.run(finished: ::Async::Condition.new, &block).wait
42
+ ensure
43
+ Fiber.set_scheduler(nil)
44
+ end
35
45
  end
36
46
  end
37
47
  end