async 1.17.1 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +0 -3
- data/README.md +3 -1
- data/examples/capture/README.md +59 -0
- data/examples/capture/capture.rb +104 -0
- data/lib/async/condition.rb +1 -1
- data/lib/async/node.rb +10 -5
- data/lib/async/queue.rb +10 -0
- data/lib/async/reactor.rb +33 -20
- data/lib/async/task.rb +1 -1
- data/lib/async/version.rb +1 -1
- data/spec/async/condition_spec.rb +0 -1
- data/spec/async/performance_spec.rb +5 -1
- data/spec/async/reactor_spec.rb +12 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 36f2b61b71527d107240403ca7ec078814ed7638bd18cf6ef14b02003d6bfc5d
|
4
|
+
data.tar.gz: 0de32e45303f0ce6514c685169422e6cc22cc0b49a497739e183964414430f7d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f09914dff9e1eccff8b2b6849cd111fa43a6c2db9ff1bc43db02aeb1fa82059bccef2728ec93eecc549486b00012f97268d46686aed23ad375704ec08ca54e71
|
7
|
+
data.tar.gz: 259262665baaee81c4ff1d056649e14e77bc891b63936eef3dbfab1124408ee8cbd9442ebbb27903430a645bbeb2b4856c7bad333375d57a51cb2608b919092a
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -10,7 +10,7 @@ Async is a composable asynchronous I/O framework for Ruby based on [nio4r] and [
|
|
10
10
|
[](https://coveralls.io/r/socketry/async)
|
11
11
|
[](https://gitter.im/socketry/async)
|
12
12
|
|
13
|
-
> "Lately I've been looking into `async`, as one of my projects – [tus-ruby-server](https://github.com/janko/tus-ruby-server) – would really benefit from
|
13
|
+
> "Lately I've been looking into `async`, as one of my projects – [tus-ruby-server](https://github.com/janko/tus-ruby-server) – would really benefit from non-blocking I/O. It's really beautifully designed." *– [janko](https://github.com/janko)*
|
14
14
|
|
15
15
|
## Motivation
|
16
16
|
|
@@ -47,6 +47,8 @@ Or install it yourself as:
|
|
47
47
|
|
48
48
|
## Usage
|
49
49
|
|
50
|
+
Please [try the interactive online tutorial](https://katacoda.com/ioquatix/scenarios/async-introduction).
|
51
|
+
|
50
52
|
### Tasks
|
51
53
|
|
52
54
|
An `Async::Task` runs using a `Fiber` and blocking operations e.g. `sleep`, `read`, `write` yield control until the operation can complete. There are two main methods to create tasks.
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# Capture
|
2
|
+
|
3
|
+
## Falcon
|
4
|
+
|
5
|
+
```
|
6
|
+
% wrk -t 8 -c 32 http://localhost:9292/
|
7
|
+
Running 10s test @ http://localhost:9292/
|
8
|
+
8 threads and 32 connections
|
9
|
+
Thread Stats Avg Stdev Max +/- Stdev
|
10
|
+
Latency 106.31ms 10.20ms 211.79ms 98.00%
|
11
|
+
Req/Sec 37.94 5.43 40.00 84.24%
|
12
|
+
3003 requests in 10.01s, 170.16KB read
|
13
|
+
Requests/sec: 299.98
|
14
|
+
Transfer/sec: 17.00KB
|
15
|
+
```
|
16
|
+
|
17
|
+
```
|
18
|
+
0.0s: Process 28065 start times:
|
19
|
+
| #<struct Process::Tms utime=2.38, stime=0.0, cutime=0.0, cstime=0.2>
|
20
|
+
^C15.11s: strace -p 28065
|
21
|
+
| ["sendto", {:"% time"=>57.34, :seconds=>0.595047, :"usecs/call"=>14, :calls=>39716, :errors=>32, :syscall=>"sendto"}]
|
22
|
+
| ["recvfrom", {:"% time"=>42.58, :seconds=>0.441867, :"usecs/call"=>12, :calls=>36718, :errors=>70, :syscall=>"recvfrom"}]
|
23
|
+
| ["read", {:"% time"=>0.07, :seconds=>0.000723, :"usecs/call"=>7, :calls=>98, :errors=>nil, :syscall=>"read"}]
|
24
|
+
| ["write", {:"% time"=>0.01, :seconds=>0.000112, :"usecs/call"=>56, :calls=>2, :errors=>nil, :syscall=>"write"}]
|
25
|
+
| [:total, {:"% time"=>100.0, :seconds=>1.037749, :"usecs/call"=>nil, :calls=>76534, :errors=>102, :syscall=>"total"}]
|
26
|
+
15.11s: Process 28065 end times:
|
27
|
+
| #<struct Process::Tms utime=3.93, stime=0.0, cutime=0.0, cstime=0.2>
|
28
|
+
15.11s: Process Waiting: 1.0377s out of 1.55s
|
29
|
+
| Wait percentage: 66.95%
|
30
|
+
```
|
31
|
+
|
32
|
+
## Puma
|
33
|
+
|
34
|
+
```
|
35
|
+
wrk -t 8 -c 32 http://localhost:9292/
|
36
|
+
Running 10s test @ http://localhost:9292/
|
37
|
+
8 threads and 32 connections
|
38
|
+
Thread Stats Avg Stdev Max +/- Stdev
|
39
|
+
Latency 108.83ms 3.50ms 146.38ms 86.58%
|
40
|
+
Req/Sec 34.43 6.70 40.00 92.68%
|
41
|
+
1371 requests in 10.01s, 81.67KB read
|
42
|
+
Requests/sec: 136.94
|
43
|
+
Transfer/sec: 8.16KB
|
44
|
+
```
|
45
|
+
|
46
|
+
```
|
47
|
+
0.0s: Process 28448 start times:
|
48
|
+
| #<struct Process::Tms utime=0.63, stime=0.0, cutime=0.0, cstime=0.2>
|
49
|
+
^C24.89s: strace -p 28448
|
50
|
+
| ["recvfrom", {:"% time"=>64.65, :seconds=>0.595275, :"usecs/call"=>13, :calls=>44476, :errors=>27769, :syscall=>"recvfrom"}]
|
51
|
+
| ["sendto", {:"% time"=>30.68, :seconds=>0.282467, :"usecs/call"=>18, :calls=>15288, :errors=>nil, :syscall=>"sendto"}]
|
52
|
+
| ["write", {:"% time"=>4.66, :seconds=>0.042921, :"usecs/call"=>15, :calls=>2772, :errors=>nil, :syscall=>"write"}]
|
53
|
+
| ["read", {:"% time"=>0.02, :seconds=>0.000157, :"usecs/call"=>8, :calls=>19, :errors=>1, :syscall=>"read"}]
|
54
|
+
| [:total, {:"% time"=>100.0, :seconds=>0.92082, :"usecs/call"=>nil, :calls=>62555, :errors=>27770, :syscall=>"total"}]
|
55
|
+
24.89s: Process 28448 end times:
|
56
|
+
| #<struct Process::Tms utime=3.19, stime=0.0, cutime=0.0, cstime=0.2>
|
57
|
+
24.89s: Process Waiting: 0.9208s out of 2.56s
|
58
|
+
| Wait percentage: 35.97%
|
59
|
+
```
|
@@ -0,0 +1,104 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'irb'
|
4
|
+
require 'console'
|
5
|
+
|
6
|
+
pids = ARGV.collect(&:to_i)
|
7
|
+
|
8
|
+
TICKS = Process.clock_getres(:TIMES_BASED_CLOCK_PROCESS_CPUTIME_ID, :hertz).to_f
|
9
|
+
|
10
|
+
def getrusage(pid)
|
11
|
+
fields = File.read("/proc/#{pid}/stat").split(/\s+/)
|
12
|
+
|
13
|
+
return Process::Tms.new(
|
14
|
+
fields[14].to_f / TICKS,
|
15
|
+
fields[15].to_f / TICKS,
|
16
|
+
fields[16].to_f / TICKS,
|
17
|
+
fields[17].to_f / TICKS,
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse(value)
|
22
|
+
case value
|
23
|
+
when /^\s*\d+\.\d+/
|
24
|
+
Float(value)
|
25
|
+
when /^\s*\d+/
|
26
|
+
Integer(value)
|
27
|
+
else
|
28
|
+
value = value.strip
|
29
|
+
if value.empty?
|
30
|
+
nil
|
31
|
+
else
|
32
|
+
value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def strace(pid, duration = 10)
|
38
|
+
input, output = IO.pipe
|
39
|
+
|
40
|
+
pid = Process.spawn("strace", "-p", pid.to_s, "-cqf", "-w", "-e", "trace=read,write,sendto,recvfrom", err: output)
|
41
|
+
|
42
|
+
output.close
|
43
|
+
|
44
|
+
Signal.trap(:INT) do
|
45
|
+
Process.kill(:INT, pid)
|
46
|
+
Signal.trap(:INT, :DEFAULT)
|
47
|
+
end
|
48
|
+
|
49
|
+
summary = {}
|
50
|
+
|
51
|
+
if line = input.gets
|
52
|
+
rule = input.gets # horizontal separator
|
53
|
+
pattern = Regexp.new(
|
54
|
+
rule.split(/\s/).map{|s| "(.{1,#{s.size}})"}.join(' ')
|
55
|
+
)
|
56
|
+
|
57
|
+
header = pattern.match(line).captures.map{|key| key.strip.to_sym}
|
58
|
+
|
59
|
+
while line = input.gets
|
60
|
+
break if line == rule
|
61
|
+
row = pattern.match(line).captures.map{|value| parse(value)}
|
62
|
+
fields = header.zip(row).to_h
|
63
|
+
|
64
|
+
summary[fields[:syscall]] = fields
|
65
|
+
end
|
66
|
+
|
67
|
+
if line = input.gets
|
68
|
+
row = pattern.match(line).captures.map{|value| parse(value)}
|
69
|
+
fields = header.zip(row).to_h
|
70
|
+
summary[:total] = fields
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
Process.waitpid(pid)
|
75
|
+
|
76
|
+
return summary
|
77
|
+
end
|
78
|
+
|
79
|
+
pids.each do |pid|
|
80
|
+
start_times = getrusage(pid)
|
81
|
+
Console.logger.info("Process #{pid} start times:", start_times)
|
82
|
+
|
83
|
+
summary = strace(pid)
|
84
|
+
|
85
|
+
Console.logger.info("strace -p #{pid}") do |buffer|
|
86
|
+
summary.each do |fields|
|
87
|
+
buffer.puts fields.inspect
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end_times = getrusage(pid)
|
92
|
+
Console.logger.info("Process #{pid} end times:", end_times)
|
93
|
+
|
94
|
+
if total = summary[:total]
|
95
|
+
process_duration = end_times.utime - start_times.utime
|
96
|
+
wait_duration = summary[:total][:seconds]
|
97
|
+
|
98
|
+
Console.logger.info("Process Waiting: #{wait_duration.round(4)}s out of #{process_duration.round(4)}s") do |buffer|
|
99
|
+
buffer.puts "Wait percentage: #{(wait_duration / process_duration * 100.0).round(2)}%"
|
100
|
+
end
|
101
|
+
else
|
102
|
+
Console.logger.warn("No system calls detected.")
|
103
|
+
end
|
104
|
+
end
|
data/lib/async/condition.rb
CHANGED
@@ -33,8 +33,8 @@ module Async
|
|
33
33
|
# @return [Object]
|
34
34
|
def wait
|
35
35
|
fiber = Fiber.current
|
36
|
-
|
37
36
|
@waiting << fiber
|
37
|
+
|
38
38
|
Task.yield
|
39
39
|
|
40
40
|
# It would be nice if there was a better construct for this. We only need to invoke #delete if the task was not resumed normally. This can only occur with `raise` and `throw`. But there is no easy way to detect this.
|
data/lib/async/node.rb
CHANGED
@@ -26,7 +26,7 @@ module Async
|
|
26
26
|
# Create a new node in the tree.
|
27
27
|
# @param parent [Node, nil] This node will attach to the given parent.
|
28
28
|
def initialize(parent = nil)
|
29
|
-
@children =
|
29
|
+
@children = nil
|
30
30
|
@parent = nil
|
31
31
|
|
32
32
|
@annotation = nil
|
@@ -40,7 +40,7 @@ module Async
|
|
40
40
|
# @attr parent [Node, nil]
|
41
41
|
attr :parent
|
42
42
|
|
43
|
-
# @attr children [Set<Node>]
|
43
|
+
# @attr children [Set<Node>] Optional list of children.
|
44
44
|
attr :children
|
45
45
|
|
46
46
|
# A useful identifier for the current node.
|
@@ -84,17 +84,22 @@ module Async
|
|
84
84
|
|
85
85
|
if parent
|
86
86
|
@parent = parent
|
87
|
-
@parent.
|
87
|
+
@parent.add_child(self)
|
88
88
|
end
|
89
89
|
|
90
90
|
return self
|
91
91
|
end
|
92
92
|
|
93
|
+
protected def add_child child
|
94
|
+
@children ||= Set.new
|
95
|
+
@children << child
|
96
|
+
end
|
97
|
+
|
93
98
|
# Whether the node can be consumed safely. By default, checks if the
|
94
99
|
# children set is empty.
|
95
100
|
# @return [Boolean]
|
96
101
|
def finished?
|
97
|
-
@children.empty?
|
102
|
+
@children.nil? or @children.empty?
|
98
103
|
end
|
99
104
|
|
100
105
|
# If the node has a parent, and is {finished?}, then remove this node from
|
@@ -118,7 +123,7 @@ module Async
|
|
118
123
|
def traverse(level = 0, &block)
|
119
124
|
yield self, level
|
120
125
|
|
121
|
-
@children
|
126
|
+
@children&.each do |child|
|
122
127
|
child.traverse(level + 1, &block)
|
123
128
|
end
|
124
129
|
end
|
data/lib/async/queue.rb
CHANGED
@@ -37,6 +37,8 @@ module Async
|
|
37
37
|
self.signal unless self.empty?
|
38
38
|
end
|
39
39
|
|
40
|
+
alias << enqueue
|
41
|
+
|
40
42
|
def dequeue
|
41
43
|
while @items.empty?
|
42
44
|
self.wait
|
@@ -44,6 +46,14 @@ module Async
|
|
44
46
|
|
45
47
|
@items.shift
|
46
48
|
end
|
49
|
+
|
50
|
+
def async(&block)
|
51
|
+
parent = Task.current
|
52
|
+
|
53
|
+
while item = self.dequeue
|
54
|
+
parent.async(item, &block)
|
55
|
+
end
|
56
|
+
end
|
47
57
|
end
|
48
58
|
|
49
59
|
class LimitedQueue < Queue
|
data/lib/async/reactor.rb
CHANGED
@@ -58,7 +58,19 @@ module Async
|
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
-
def
|
61
|
+
def self.selector
|
62
|
+
if backend = ENV['ASYNC_BACKEND']&.to_sym
|
63
|
+
if NIO::Selector.backends.include?(backend)
|
64
|
+
return NIO::Selector.new(backend)
|
65
|
+
else
|
66
|
+
warn "Could not find ASYNC_BACKEND=#{backend}!"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
return NIO::Selector.new
|
71
|
+
end
|
72
|
+
|
73
|
+
def initialize(parent = nil, selector: self.class.selector, logger: nil)
|
62
74
|
super(parent)
|
63
75
|
|
64
76
|
@selector = selector
|
@@ -86,6 +98,7 @@ module Async
|
|
86
98
|
@stopped
|
87
99
|
end
|
88
100
|
|
101
|
+
# TODO Remove these in next major release. They are too confusing to use correctly.
|
89
102
|
def_delegators :@timers, :every, :after
|
90
103
|
|
91
104
|
# Start an asynchronous task within the specified reactor. The task will be
|
@@ -142,6 +155,7 @@ module Async
|
|
142
155
|
end
|
143
156
|
|
144
157
|
def finished?
|
158
|
+
# I'm not sure if checking `@running.empty?` is really required.
|
145
159
|
super && @ready.empty? && @running.empty?
|
146
160
|
end
|
147
161
|
|
@@ -155,14 +169,18 @@ module Async
|
|
155
169
|
initial_task = self.async(*args, &block) if block_given?
|
156
170
|
|
157
171
|
@timers.wait do |interval|
|
158
|
-
#
|
159
|
-
|
160
|
-
if @
|
172
|
+
# logger.debug(self) {"@ready = #{@ready} @running = #{@running}"}
|
173
|
+
|
174
|
+
if @ready.any?
|
175
|
+
# running used to correctly answer on `finished?`, and to reuse Array object.
|
176
|
+
@running, @ready = @ready, @running
|
177
|
+
|
161
178
|
@running.each do |fiber|
|
162
179
|
fiber.resume if fiber.alive?
|
163
180
|
end
|
181
|
+
|
164
182
|
@running.clear
|
165
|
-
|
183
|
+
|
166
184
|
# if there are tasks ready to execute, don't sleep.
|
167
185
|
if @ready.any?
|
168
186
|
interval = 0
|
@@ -172,23 +190,18 @@ module Async
|
|
172
190
|
end
|
173
191
|
end
|
174
192
|
|
175
|
-
# - nil: no timers
|
176
|
-
# - -ve: timers expired already
|
177
|
-
# - 0: timers ready to fire
|
178
|
-
# - +ve: timers waiting to fire
|
179
|
-
if interval && interval < 0
|
180
|
-
interval = 0
|
181
|
-
end
|
182
|
-
|
183
|
-
# logger.debug(self) {"Updating #{@children.count} children..."}
|
184
193
|
# As timeouts may have been updated, and caused fibers to complete, we should check this.
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
194
|
+
if interval.nil?
|
195
|
+
if self.finished?
|
196
|
+
# If there is nothing to do, then finish:
|
197
|
+
return initial_task
|
198
|
+
end
|
199
|
+
elsif interval < 0
|
200
|
+
# We have timers ready to fire, don't sleep in the selctor:
|
201
|
+
interval = 0
|
189
202
|
end
|
190
203
|
|
191
|
-
# logger.debug(self) {"Selecting with #{@children
|
204
|
+
# logger.debug(self) {"Selecting with #{@children&.count} children with interval = #{interval.inspect}..."}
|
192
205
|
if monitors = @selector.select(interval)
|
193
206
|
monitors.each do |monitor|
|
194
207
|
monitor.value.resume
|
@@ -207,7 +220,7 @@ module Async
|
|
207
220
|
#
|
208
221
|
# @return [void]
|
209
222
|
def close
|
210
|
-
@children
|
223
|
+
@children&.each(&:stop)
|
211
224
|
|
212
225
|
# TODO Should we also clear all timers?
|
213
226
|
@selector.close
|
data/lib/async/task.rb
CHANGED
data/lib/async/version.rb
CHANGED
@@ -4,9 +4,13 @@ require 'benchmark/ips'
|
|
4
4
|
RSpec.describe Async::Wrapper do
|
5
5
|
let(:pipe) {IO.pipe}
|
6
6
|
|
7
|
+
after do
|
8
|
+
pipe.each(&:close)
|
9
|
+
end
|
10
|
+
|
7
11
|
let(:input) {described_class.new(pipe.first)}
|
8
12
|
let(:output) {described_class.new(pipe.last)}
|
9
|
-
|
13
|
+
|
10
14
|
it "should be fast to wait until readable" do
|
11
15
|
Benchmark.ips do |x|
|
12
16
|
x.report('Wrapper#wait_readable') do |repeats|
|
data/spec/async/reactor_spec.rb
CHANGED
@@ -37,6 +37,14 @@ RSpec.describe Async::Reactor do
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
+
describe '#close' do
|
41
|
+
it "can close empty reactor" do
|
42
|
+
subject.close
|
43
|
+
|
44
|
+
expect(subject).to be_closed
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
40
48
|
describe '#stop' do
|
41
49
|
it "can be stop reactor" do
|
42
50
|
state = nil
|
@@ -55,6 +63,8 @@ RSpec.describe Async::Reactor do
|
|
55
63
|
subject.run
|
56
64
|
|
57
65
|
expect(state).to be == :started
|
66
|
+
|
67
|
+
subject.close
|
58
68
|
end
|
59
69
|
|
60
70
|
it "can stop reactor from different thread" do
|
@@ -73,9 +83,11 @@ RSpec.describe Async::Reactor do
|
|
73
83
|
subject.run
|
74
84
|
|
75
85
|
thread.join
|
86
|
+
|
76
87
|
expect(subject).to be_stopped
|
77
88
|
end
|
78
89
|
end
|
90
|
+
|
79
91
|
it "can't return" do
|
80
92
|
expect do
|
81
93
|
Async do |task|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.18.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-06-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nio4r
|
@@ -144,6 +144,8 @@ files:
|
|
144
144
|
- benchmark/async_vs_lightio.rb
|
145
145
|
- examples/async_method.rb
|
146
146
|
- examples/callback/loop.rb
|
147
|
+
- examples/capture/README.md
|
148
|
+
- examples/capture/capture.rb
|
147
149
|
- examples/fibers.rb
|
148
150
|
- examples/sleep_sort.rb
|
149
151
|
- gems/event.gemfile
|
@@ -199,7 +201,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
199
201
|
- !ruby/object:Gem::Version
|
200
202
|
version: '0'
|
201
203
|
requirements: []
|
202
|
-
rubygems_version: 3.0.
|
204
|
+
rubygems_version: 3.0.3
|
203
205
|
signing_key:
|
204
206
|
specification_version: 4
|
205
207
|
summary: Async is an asynchronous I/O framework based on nio4r.
|