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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9513c847ed714794e7792f205c2aa3d6ece0712f39b426246bb147f60ad21b20
4
- data.tar.gz: a1382d2118fff244e3bcaa06b1bab8a2284d395304b0e85ef7a8a32c171c9915
3
+ metadata.gz: 36f2b61b71527d107240403ca7ec078814ed7638bd18cf6ef14b02003d6bfc5d
4
+ data.tar.gz: 0de32e45303f0ce6514c685169422e6cc22cc0b49a497739e183964414430f7d
5
5
  SHA512:
6
- metadata.gz: 853b55599bc4c0923fdb49534f02e474ee7fa048bc29ee66b3d1fa57d7dc1d902a53b859bf1337b71b2f94fe85fa5b3e6462633f5af210178210221d3444a14d
7
- data.tar.gz: '07092c2764ebdbe0fb106cefffd1bbc6e33710d17d899f64626df60601d488ca696ea6522b62740ffbd5fdd46d8f72bbca98303d041ea2ce1076be74e477de03'
6
+ metadata.gz: f09914dff9e1eccff8b2b6849cd111fa43a6c2db9ff1bc43db02aeb1fa82059bccef2728ec93eecc549486b00012f97268d46686aed23ad375704ec08ca54e71
7
+ data.tar.gz: 259262665baaee81c4ff1d056649e14e77bc891b63936eef3dbfab1124408ee8cbd9442ebbb27903430a645bbeb2b4856c7bad333375d57a51cb2608b919092a
data/.travis.yml CHANGED
@@ -28,9 +28,6 @@ matrix:
28
28
  env: JRUBY_OPTS="--debug -X+O"
29
29
  - rvm: truffleruby
30
30
  - rvm: ruby-head
31
- - rvm: rbx-3
32
31
  allow_failures:
33
32
  - rvm: ruby-head
34
33
  - rvm: jruby-head
35
- - rvm: rbx-3
36
- - rvm: truffleruby
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 `async` I/O. It's really beautifully designed." *– [janko](https://github.com/janko)*
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
@@ -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 = Set.new
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.children << self
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.each do |child|
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 initialize(parent = nil, selector: NIO::Selector.new, logger: nil)
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
- # running used to correctly answer on `finished?`, and to reuse Array object.
159
- @running, @ready = @ready, @running
160
- if @running.any?
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
- # If there is nothing to do, then finish:
187
- if !interval && self.finished?
188
- return initial_task
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.count} fibers interval = #{interval.inspect}..."}
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.each(&:stop)
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
@@ -134,7 +134,7 @@ module Async
134
134
  # Stop the task and all of its children.
135
135
  # @return [void]
136
136
  def stop
137
- @children.each(&:stop)
137
+ @children&.each(&:stop)
138
138
 
139
139
  if @fiber.alive?
140
140
  @fiber.resume(Stop.new)
data/lib/async/version.rb CHANGED
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Async
22
- VERSION = "1.17.1"
22
+ VERSION = "1.18.0"
23
23
  end
@@ -29,7 +29,6 @@ RSpec.describe Async::Condition do
29
29
  it 'should continue after condition is signalled' do
30
30
  task = reactor.async do
31
31
  subject.wait
32
- #puts "Got #{value}"
33
32
  end
34
33
 
35
34
  expect(task.status).to be :running
@@ -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|
@@ -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.17.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-03-29 00:00:00.000000000 Z
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.2
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.