async 1.17.1 → 1.18.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 +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
|
[![Coverage Status](https://coveralls.io/repos/socketry/async/badge.svg)](https://coveralls.io/r/socketry/async)
|
11
11
|
[![Gitter](https://badges.gitter.im/join.svg)](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.
|