async 1.12.0 → 1.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -1
- data/README.md +207 -18
- data/Rakefile +1 -0
- data/lib/async.rb +7 -1
- data/lib/async/logger.rb +131 -2
- data/lib/async/node.rb +3 -3
- data/lib/async/queue.rb +2 -0
- data/lib/async/reactor.rb +8 -8
- data/lib/async/task.rb +64 -34
- data/lib/async/terminal.rb +99 -0
- data/lib/async/version.rb +1 -1
- data/lib/async/wrapper.rb +1 -5
- data/spec/async/condition_examples.rb +1 -1
- data/spec/async/logger_spec.rb +48 -4
- data/spec/async/performance_spec.rb +2 -2
- data/spec/async/reactor/nested_spec.rb +4 -4
- data/spec/async/reactor_spec.rb +8 -8
- data/spec/async/task_spec.rb +91 -19
- data/spec/async/wrapper_spec.rb +11 -9
- data/spec/enumerator_spec.rb +5 -5
- metadata +4 -3
data/lib/async/node.rb
CHANGED
@@ -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
|
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)
|
data/lib/async/queue.rb
CHANGED
data/lib/async/reactor.rb
CHANGED
@@ -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
|
31
|
-
class TimeoutError <
|
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
|
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
|
-
|
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 [
|
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
|
data/lib/async/task.rb
CHANGED
@@ -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
|
-
|
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 =
|
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}
|
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
|
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
|
135
|
-
raise RuntimeError
|
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
|
121
|
+
Task.yield{@result}
|
142
122
|
end
|
143
123
|
end
|
144
124
|
|
145
|
-
|
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
|
data/lib/async/version.rb
CHANGED
data/lib/async/wrapper.rb
CHANGED
@@ -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
|
-
|
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
|
data/spec/async/logger_spec.rb
CHANGED
@@ -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 '
|
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
|
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
|
28
|
+
Async do |task|
|
29
29
|
input = pipe.first
|
30
30
|
monitor = task.reactor.register(input, :r)
|
31
31
|
output = pipe.last
|