async 1.12.0 → 1.13.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -96,17 +96,17 @@ module Async
96
96
  def finished?
97
97
  @children.empty?
98
98
  end
99
-
99
+
100
100
  # If the node has a parent, and is {finished?}, then remove this node from
101
101
  # the parent.
102
102
  def consume
103
- if @parent && finished?
103
+ if @parent and finished?
104
104
  @parent.reap(self)
105
105
  @parent.consume
106
106
  @parent = nil
107
107
  end
108
108
  end
109
-
109
+
110
110
  # Remove a given child node.
111
111
  # @param child [Node]
112
112
  def reap(child)
@@ -54,6 +54,8 @@ module Async
54
54
  @full = Async::Condition.new
55
55
  end
56
56
 
57
+ attr :limit
58
+
57
59
  # @return [Boolean] Whether trying to enqueue an item would block.
58
60
  def limited?
59
61
  @items.size >= @limit
@@ -27,8 +27,8 @@ require 'timers'
27
27
  require 'forwardable'
28
28
 
29
29
  module Async
30
- # Raised if a timeout occurs on a specific Fiber. Handled gracefully by {Task}.
31
- class TimeoutError < Exception
30
+ # Raised if a timeout occurs on a specific Fiber. Handled gracefully by `Task`.
31
+ class TimeoutError < StandardError
32
32
  end
33
33
 
34
34
  # An asynchronous, cooperatively scheduled event reactor.
@@ -89,8 +89,8 @@ module Async
89
89
  #
90
90
  # This is the main entry point for scheduling asynchronus tasks.
91
91
  #
92
- # @yield [Task] Executed within the asynchronous task.
93
- # @return [Task] The task that was
92
+ # @yield [Task] Executed within the task.
93
+ # @return [Task] The task that was scheduled into the reactor.
94
94
  def async(*args, &block)
95
95
  task = Task.new(self, &block)
96
96
 
@@ -124,6 +124,7 @@ module Async
124
124
  end
125
125
 
126
126
  # Schedule a fiber (or equivalent object) to be resumed on the next loop through the reactor.
127
+ # @param fiber [#resume] The object to be resumed on the next iteration of the run-loop.
127
128
  def << fiber
128
129
  @ready << fiber
129
130
  end
@@ -146,8 +147,7 @@ module Async
146
147
 
147
148
  @stopped = false
148
149
 
149
- # Allow the user to kick of the initial async tasks.
150
- initial_task = async(*args, &block) if block_given?
150
+ initial_task = self.async(*args, &block) if block_given?
151
151
 
152
152
  @timers.wait do |interval|
153
153
  # running used to correctly answer on `finished?`, and to reuse Array object.
@@ -233,7 +233,7 @@ module Async
233
233
 
234
234
  # Invoke the block, but after the timeout, raise {TimeoutError} in any
235
235
  # currenly blocking operation.
236
- # @param duration [Integer] The time in seconds, in which the task should
236
+ # @param duration [Numeric] The time in seconds, in which the task should
237
237
  # complete.
238
238
  def timeout(duration, exception = TimeoutError)
239
239
  backtrace = caller
@@ -247,7 +247,7 @@ module Async
247
247
  end
248
248
  end
249
249
 
250
- yield
250
+ yield timer
251
251
  ensure
252
252
  timer.cancel if timer
253
253
  end
@@ -28,7 +28,7 @@ module Async
28
28
  # Raised when a task is explicitly stopped.
29
29
  class Stop < Exception
30
30
  end
31
-
31
+
32
32
  # A task represents the state associated with the execution of an asynchronous
33
33
  # block.
34
34
  class Task < Node
@@ -57,41 +57,21 @@ module Async
57
57
  # Create a new task.
58
58
  # @param reactor [Async::Reactor] the reactor this task will run within.
59
59
  # @param parent [Async::Task] the parent task.
60
- def initialize(reactor, parent = Task.current?)
60
+ # @param propagate_exceptions [Boolean] whether exceptions raised in the task will propagate up the reactor stack.
61
+ def initialize(reactor, parent = Task.current?, &block)
61
62
  super(parent || reactor)
62
63
 
63
64
  @reactor = reactor
64
65
 
65
66
  @status = :initialized
66
67
  @result = nil
67
-
68
68
  @finished = nil
69
69
 
70
- @fiber = Fiber.new do |*args|
71
- set!
72
-
73
- begin
74
- @result = yield(self, *args)
75
- @status = :complete
76
- # Async.logger.debug("Task #{self} completed normally.")
77
- rescue Stop
78
- @status = :stop
79
- # Async.logger.debug("Task #{self} stopped: #{$!}")
80
- Async.logger.debug(self) {$!}
81
- rescue Exception => error
82
- @result = error
83
- @status = :failed
84
- Async.logger.debug(self) {$!}
85
- raise
86
- ensure
87
- # Async.logger.debug("Task #{self} closing: #{$!}")
88
- finish!
89
- end
90
- end
70
+ @fiber = make_fiber(&block)
91
71
  end
92
72
 
93
73
  def to_s
94
- "<#{self.description} status=#{@status}>"
74
+ "<#{self.description} #{@status}>"
95
75
  end
96
76
 
97
77
  # @attr ios [Reactor] The reactor the task was created within.
@@ -107,7 +87,7 @@ module Async
107
87
  attr :fiber
108
88
  def_delegators :@fiber, :alive?
109
89
 
110
- # @attr status [Symbol] The status of the execution of the fiber, one of `:running`, `:complete`, `:stopped`, or `:failed`.
90
+ # @attr status [Symbol] The status of the execution of the fiber, one of `:initialized`, `:running`, `:complete`, `:stopped` or `:failed`.
111
91
  attr :status
112
92
 
113
93
  # Resume the execution of the task.
@@ -130,20 +110,21 @@ module Async
130
110
 
131
111
  # Retrieve the current result of the task. Will cause the caller to wait until result is available.
132
112
  # @raise [RuntimeError] if the task's fiber is the current fiber.
133
- # @return [Object]
134
- def result
135
- raise RuntimeError.new("Cannot wait on own fiber") if Fiber.current.equal?(@fiber)
113
+ # @return [Object] the final expression/result of the task's block.
114
+ def wait
115
+ raise RuntimeError, "Cannot wait on own fiber" if Fiber.current.equal?(@fiber)
136
116
 
137
117
  if running?
138
118
  @finished ||= Condition.new
139
119
  @finished.wait
140
120
  else
141
- Task.yield {@result}
121
+ Task.yield{@result}
142
122
  end
143
123
  end
144
124
 
145
- alias wait result
146
-
125
+ # Deprecated.
126
+ alias result wait
127
+
147
128
  # Stop the task and all of its children.
148
129
  # @return [void]
149
130
  def stop
@@ -178,9 +159,58 @@ module Async
178
159
  def finished?
179
160
  super && @status != :running
180
161
  end
181
-
162
+
163
+ def failed?
164
+ @status == :failed
165
+ end
166
+
167
+ def stopped?
168
+ @status == :stopped
169
+ end
170
+
182
171
  private
183
-
172
+
173
+ # This is a very tricky aspect of tasks to get right. I've modelled it after `Thread` but it's slightly different in that the exception can propagate back up through the reactor. If the user writes code which raises an exception, that exception should always be visible, i.e. cause a failure. If it's not visible, such code fails silently and can be very difficult to debug.
174
+ # As an explcit choice, the user can start a task which doesn't propagate exceptions. This only applies to `StandardError` and derived tasks. This allows tasks to internally capture their error state which is raised when invoking `Task#result` similar to how `Thread#join` works. This mode makes `Async::Task` behave more like a promise, and you would need to ensure that someone calls `Task#result` otherwise you might miss important errors.
175
+ def fail!(exception = nil, propagate = true)
176
+ @status = :failed
177
+ @result = exception
178
+
179
+ if propagate
180
+ raise
181
+ elsif @finished.nil?
182
+ # If no one has called wait, we log this as an error:
183
+ Async.logger.error(self) {$!}
184
+ else
185
+ Async.logger.debug(self) {$!}
186
+ end
187
+ end
188
+
189
+ def stop!
190
+ @status = :stopped
191
+ end
192
+
193
+ def make_fiber(&block)
194
+ Fiber.new do |*args|
195
+ set!
196
+
197
+ begin
198
+ @result = yield(self, *args)
199
+ @status = :complete
200
+ # Async.logger.debug("Task #{self} completed normally.")
201
+ rescue Stop
202
+ stop!
203
+ rescue StandardError => error
204
+ fail!(error, false)
205
+ rescue Exception => exception
206
+ fail!(exception, true)
207
+ ensure
208
+ # Async.logger.debug("Task #{self} closing: #{$!}")
209
+ finish!
210
+ end
211
+ end
212
+ end
213
+
184
214
  # Finish the current task, and all bound bound IO objects.
185
215
  def finish!
186
216
  # Attempt to remove this node from the task tree.
@@ -0,0 +1,99 @@
1
+ # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Async
22
+ # Styled terminal output. **Internal Use Only**
23
+ class Terminal
24
+ module Attributes
25
+ NORMAL = 0
26
+ BOLD = 1
27
+ FAINT = 2
28
+ ITALIC = 3
29
+ UNDERLINE = 4
30
+ BLINK = 5
31
+ REVERSE = 7
32
+ HIDDEN = 8
33
+ end
34
+
35
+ module Colors
36
+ BLACK = 0
37
+ RED = 1
38
+ GREEN = 2
39
+ YELLOW = 3
40
+ BLUE = 4
41
+ MAGENTA = 5
42
+ CYAN = 6
43
+ WHITE = 7
44
+ DEFAULT = 9
45
+ end
46
+
47
+ def initialize(output)
48
+ @output = output
49
+ end
50
+
51
+ def tty?
52
+ @output.isatty
53
+ end
54
+
55
+ def color(foreground, background = nil, attributes = nil)
56
+ return nil unless tty?
57
+
58
+ buffer = String.new
59
+
60
+ buffer << "\e["
61
+ first = true
62
+
63
+ if attributes
64
+ buffer << (attributes).to_s
65
+ first = false
66
+ end
67
+
68
+ if foreground
69
+ if !first
70
+ buffer << ";"
71
+ else
72
+ first = false
73
+ end
74
+
75
+ buffer << (30 + foreground).to_s
76
+ end
77
+
78
+ if background
79
+ if !first
80
+ buffer << ";"
81
+ else
82
+ first = false
83
+ end
84
+
85
+ buffer << (40 + background).to_s
86
+ end
87
+
88
+ buffer << 'm'
89
+
90
+ return buffer
91
+ end
92
+
93
+ def reset
94
+ return nil unless tty?
95
+
96
+ return "\e[0m"
97
+ end
98
+ end
99
+ end
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Async
22
- VERSION = "1.12.0"
22
+ VERSION = "1.13.0"
23
23
  end
@@ -205,11 +205,7 @@ module Async
205
205
  # If the user requested an explicit timeout for this operation:
206
206
  if duration
207
207
  @reactor.timeout(duration) do
208
- begin
209
- Task.yield
210
- rescue Async::TimeoutError
211
- return false
212
- end
208
+ Task.yield
213
209
  end
214
210
  else
215
211
  Task.yield
@@ -22,7 +22,7 @@ RSpec.shared_examples Async::Condition do
22
22
  it 'can signal waiting task' do
23
23
  state = nil
24
24
 
25
- task = reactor.async do
25
+ reactor.async do
26
26
  state = :waiting
27
27
  subject.wait
28
28
  state = :resumed
@@ -18,8 +18,52 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
+ RSpec.describe Async::Logger do
22
+ let(:output) {StringIO.new}
23
+ subject{described_class.new(output)}
24
+
25
+ let(:message) {"Hello World"}
26
+
27
+ context "default log level" do
28
+ it "logs info" do
29
+ subject.info(message)
30
+
31
+ expect(output.string).to include message
32
+ end
33
+
34
+ it "doesn't log debug" do
35
+ subject.debug(message)
36
+
37
+ expect(output.string).to_not include message
38
+ end
39
+ end
40
+
41
+ described_class::LEVELS.each do |name, level|
42
+ it "can log #{name} messages" do
43
+ subject.level = level
44
+ subject.log(name, message)
45
+
46
+ expect(output.string).to include message
47
+ end
48
+ end
49
+
50
+ describe '#enable' do
51
+ let(:object) {Async::Node.new}
52
+
53
+ it "can enable specific subjects" do
54
+ subject.warn!
55
+
56
+ subject.enable(object)
57
+ expect(subject).to be_enabled(object)
58
+
59
+ subject.debug(object, message)
60
+ expect(output.string).to include message
61
+ end
62
+ end
63
+ end
64
+
21
65
  RSpec.describe Async.logger do
22
- describe '::default_log_level' do
66
+ describe 'default_log_level' do
23
67
  let!(:debug) {$DEBUG}
24
68
  after {$DEBUG = debug}
25
69
 
@@ -30,20 +74,20 @@ RSpec.describe Async.logger do
30
74
  $DEBUG = false
31
75
  $VERBOSE = false
32
76
 
33
- expect(Async.default_log_level).to be == Logger::WARN
77
+ expect(Async.default_log_level).to be == Async::Logger::WARN
34
78
  end
35
79
 
36
80
  it 'should set default log level based on $DEBUG' do
37
81
  $DEBUG = true
38
82
 
39
- expect(Async.default_log_level).to be == Logger::DEBUG
83
+ expect(Async.default_log_level).to be == Async::Logger::DEBUG
40
84
  end
41
85
 
42
86
  it 'should set default log level based on $VERBOSE' do
43
87
  $DEBUG = false
44
88
  $VERBOSE = true
45
89
 
46
- expect(Async.default_log_level).to be == Logger::INFO
90
+ expect(Async.default_log_level).to be == Async::Logger::INFO
47
91
  end
48
92
  end
49
93
  end
@@ -10,7 +10,7 @@ RSpec.describe Async::Wrapper do
10
10
  it "should be fast to wait until readable" do
11
11
  Benchmark.ips do |x|
12
12
  x.report('Wrapper#wait_readable') do |repeats|
13
- Async::Reactor.run do |task|
13
+ Async do |task|
14
14
  input = Async::Wrapper.new(pipe.first, task.reactor)
15
15
  output = pipe.last
16
16
 
@@ -25,7 +25,7 @@ RSpec.describe Async::Wrapper do
25
25
  end
26
26
 
27
27
  x.report('Reactor#register') do |repeats|
28
- Async::Reactor.run do |task|
28
+ Async do |task|
29
29
  input = pipe.first
30
30
  monitor = task.reactor.register(input, :r)
31
31
  output = pipe.last