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 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.