async 1.2.2 → 1.3.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
- SHA1:
3
- metadata.gz: eaeb8a35292eab6b85c49779994816e8d6355427
4
- data.tar.gz: a97badcaa4eb6c8cbfa37ca4609d9f45b93b6e71
2
+ SHA256:
3
+ metadata.gz: c6eb0c5b8b5f8a6b2a05ce7c6a563d7deacb8a3ce0da0f0a7797ecc8080790b4
4
+ data.tar.gz: b0028ef539d8d11026bad3cc476b4fc2af5122e1ac5485888c456b9894be2084
5
5
  SHA512:
6
- metadata.gz: d6017907e393996093d196a36434d469acd824958d90ca560cff85ef6c1b8bfbb4e10e96c712d7542f106dcef9beb6e5aeb10d70cd9c4af9b058383b42434949
7
- data.tar.gz: 7330ce6f3aa50afcba05c5ec2b90cf8c1653176104c0fdbb4c006315f9ab8409aa8a8daae00dbef0cadb6b11d5867bab7b7df250de7d966cf3b6c1e018a75cf9
6
+ metadata.gz: bb55a40ee7d6de6c8ed5d32af4ce29c2e8e8395d9cdec05c6a4d8328cb5fe6794e30d743132967a7b5c985b312a8e0ae5daee54ff6766912f2ebfc218f1119ce
7
+ data.tar.gz: 9b767f9dff4254c65377ffcef840082a3e31dce20d8abdfe85c0aed0e7e3ddce7819fd30eaec0623e7428e51528694dc9208597fc416b79f3b9146357d9a72f9
@@ -2,14 +2,19 @@ language: ruby
2
2
  sudo: false
3
3
  dist: trusty
4
4
  cache: bundler
5
+ addons:
6
+ apt:
7
+ packages:
8
+ - bind9
9
+
10
+ before_script: sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'
5
11
 
6
12
  matrix:
7
13
  include:
8
- - rvm: 2.0
9
- - rvm: 2.1
10
14
  - rvm: 2.2
11
15
  - rvm: 2.3
12
16
  - rvm: 2.4
17
+ - rvm: 2.5
13
18
  - rvm: jruby-head
14
19
  env: JRUBY_OPTS="--debug -X+O"
15
20
  - rvm: ruby-head
data/Rakefile CHANGED
@@ -3,7 +3,27 @@ require "rspec/core/rake_task"
3
3
 
4
4
  RSpec::Core::RakeTask.new(:test)
5
5
 
6
- task :default => :test
6
+ task :default => [:test, :external]
7
+
8
+ def clone_and_test(name)
9
+ sh("git clone https://git@github.com/socketry/#{name}")
10
+
11
+ # I tried using `bundle config --local local.async ../` but it simply doesn't work.
12
+ File.open("#{name}/Gemfile", "a") do |file|
13
+ file.puts('gem "async", path: "../"')
14
+ end
15
+
16
+ sh("cd #{name} && bundle install && bundle exec rake test")
17
+ end
18
+
19
+ task :external do
20
+ Bundler.with_clean_env do
21
+ clone_and_test("async-io")
22
+ clone_and_test("async-websocket")
23
+ clone_and_test("async-dns")
24
+ clone_and_test("falcon")
25
+ end
26
+ end
7
27
 
8
28
  task :coverage do
9
29
  ENV['COVERAGE'] = 'y'
@@ -1,4 +1,4 @@
1
- # -*- encoding: utf-8 -*-
1
+
2
2
  require_relative 'lib/async/version'
3
3
 
4
4
  Gem::Specification.new do |spec|
@@ -21,9 +21,9 @@ Gem::Specification.new do |spec|
21
21
  spec.require_paths = ["lib"]
22
22
  spec.has_rdoc = "yard"
23
23
 
24
- spec.required_ruby_version = "~> 2.0"
24
+ spec.required_ruby_version = ">= 2.2.7"
25
25
 
26
- spec.add_runtime_dependency "nio4r"
26
+ spec.add_runtime_dependency "nio4r", "~> 2.0"
27
27
  spec.add_runtime_dependency "timers", "~> 4.1"
28
28
 
29
29
  spec.add_development_dependency "async-rspec", "~> 1.1"
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'async'
4
+ require 'lightio'
5
+
6
+ require 'benchmark/ips'
7
+
8
+ def run_async(count = 10000)
9
+ Async::Reactor.run do |task|
10
+ tasks = count.times.map do
11
+ # LightIO::Beam is a thread-like executor, use it instead Thread
12
+ task.async do |subtask|
13
+ # do some io operations in beam
14
+ subtask.sleep(0.0001)
15
+ end
16
+ end
17
+
18
+ tasks.each(&:wait)
19
+ end
20
+ end
21
+
22
+ def run_lightio(count = 10000)
23
+ beams = count.times.map do
24
+ # LightIO::Beam is a thread-like executor, use it instead Thread
25
+ LightIO::Beam.new do
26
+ # do some io operations in beam
27
+ LightIO.sleep(0.0001)
28
+ end
29
+ end
30
+
31
+ beams.each(&:join)
32
+ end
33
+
34
+ Benchmark.ips do |benchmark|
35
+ benchmark.report("lightio") do |count|
36
+ run_lightio(count)
37
+ end
38
+
39
+ benchmark.report("async") do |count|
40
+ run_async(count)
41
+ end
42
+
43
+ benchmark.compare!
44
+ end
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/async'
4
+
5
+ module Async::Methods
6
+ def sleep(*args)
7
+ Async::Task.current.sleep(*args)
8
+ end
9
+
10
+ def async(name)
11
+ original_method = self.method(name)
12
+
13
+ define_method(name) do |*args|
14
+ Async::Reactor.run do |task|
15
+ original_method.call(*args)
16
+ end
17
+ end
18
+ end
19
+
20
+ def await(&block)
21
+ block.call.wait
22
+ end
23
+
24
+ def barrier!
25
+ Async::Task.current.children.each(&:wait)
26
+ end
27
+ end
28
+
29
+ include Async::Methods
30
+
31
+ async def count_chickens(area_name)
32
+ 3.times do |i|
33
+ sleep rand
34
+
35
+ puts "Found a chicken in the #{area_name}!"
36
+ end
37
+ end
38
+
39
+ async def find_chicken(areas)
40
+ puts "Searching for chicken..."
41
+
42
+ sleep rand * 5
43
+
44
+ return areas.sample
45
+ end
46
+
47
+ async def count_all_chckens
48
+ # These methods all run at the same time.
49
+ count_chickens("garden")
50
+ count_chickens("house")
51
+ count_chickens("tree")
52
+
53
+ # Wait for all previous async work to complete...
54
+ barrier!
55
+
56
+ puts "There was a chicken in the #{find_chicken(["garden", "house", "tree"]).wait}"
57
+ end
58
+
59
+ count_all_chckens
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/async'
4
+
5
+ def sleep_sort(items)
6
+ Async::Reactor.run do |task|
7
+ # Where to save the sorted items:
8
+ sorted_items = []
9
+
10
+ items.each do |item|
11
+ # Spawn an async task...
12
+ task.async do |nested_task|
13
+ # Which goes to sleep for the specified duration:
14
+ nested_task.sleep(item)
15
+
16
+ # And then appends the item to the sorted array:
17
+ sorted_items << item
18
+ end
19
+ end
20
+
21
+ # Wait for all children to complete.
22
+ task.children.each(&:wait)
23
+
24
+ # Return the result:
25
+ sorted_items
26
+ end.wait # Wait for the entire process to complete.
27
+ end
28
+
29
+ # Calling at the top level blocks the thread:
30
+ puts sleep_sort(5.times.collect{rand}).inspect
31
+
32
+ # Calling in your own reactor allows you to control the asynchronus behaviour:
33
+ Async::Reactor.run do |task|
34
+ 3.times do
35
+ task.async do
36
+ puts sleep_sort(5.times.collect{rand}).inspect
37
+ end
38
+ end
39
+ end
@@ -46,17 +46,15 @@ module Async
46
46
  if current = Task.current?
47
47
  reactor = current.reactor
48
48
 
49
- reactor.async(*args, &block)
49
+ return reactor.async(*args, &block)
50
50
  else
51
51
  reactor = self.new
52
52
 
53
53
  begin
54
- reactor.run(*args, &block)
54
+ return reactor.run(*args, &block)
55
55
  ensure
56
56
  reactor.close
57
57
  end
58
-
59
- return reactor
60
58
  end
61
59
  end
62
60
 
@@ -103,7 +101,11 @@ module Async
103
101
  end
104
102
 
105
103
  def register(*args)
106
- @selector.register(*args)
104
+ monitor = @selector.register(*args)
105
+
106
+ monitor.value = Fiber.current
107
+
108
+ return monitor
107
109
  end
108
110
 
109
111
  # Stop the reactor at the earliest convenience. Can be called from a different thread safely.
@@ -123,7 +125,7 @@ module Async
123
125
  @stopped = false
124
126
 
125
127
  # Allow the user to kick of the initial async tasks.
126
- async(*args, &block) if block_given?
128
+ initial_task = async(*args, &block) if block_given?
127
129
 
128
130
  @timers.wait do |interval|
129
131
  # - nil: no timers
@@ -137,7 +139,7 @@ module Async
137
139
 
138
140
  # If there is nothing to do, then finish:
139
141
  # Async.logger.debug{"[#{self}] @children.empty? = #{@children.empty?} && interval #{interval.inspect}"}
140
- return if @children.empty? && interval.nil?
142
+ return initial_task if @children.empty? && interval.nil?
141
143
 
142
144
  # Async.logger.debug{"Selecting with #{@children.count} fibers interval = #{interval.inspect}..."}
143
145
  if monitors = @selector.select(interval)
@@ -149,7 +151,7 @@ module Async
149
151
  end
150
152
  end until @stopped
151
153
 
152
- return self
154
+ return initial_task
153
155
  ensure
154
156
  Async.logger.debug{"[#{self} Ensure] Exiting run-loop (stopped: #{@stopped} exception: #{$!.inspect})..."}
155
157
  @stopped = true
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Async
22
- VERSION = "1.2.2"
22
+ VERSION = "1.3.0"
23
23
  end
@@ -29,16 +29,25 @@ module Async
29
29
  def initialize(io, reactor = nil)
30
30
  @io = io
31
31
 
32
- @reactor = reactor || Task.current.reactor
32
+ @reactor = reactor
33
33
  @monitor = nil
34
34
  end
35
35
 
36
36
  # The underlying native `io`.
37
37
  attr :io
38
38
 
39
- # The reactor this wrapper is associated with.
39
+ # The reactor this wrapper is associated with, if any.
40
40
  attr :reactor
41
41
 
42
+ # Bind this wrapper to a different reactor. Assign nil to convert to an unbound wrapper (can be used from any reactor/task but with slightly increased overhead.)
43
+ # Binding to a reactor is purely a performance consideration. Generally, I don't like APIs that exist only due to optimisations. This is borderline, so consider this functionality semi-private.
44
+ def reactor= reactor
45
+ @monitor.close if @monitor
46
+
47
+ @reactor = reactor
48
+ @monitor = nil
49
+ end
50
+
42
51
  # Wait for the io to become readable.
43
52
  def wait_readable(duration = nil)
44
53
  wait_any(:r, duration)
@@ -53,50 +62,46 @@ module Async
53
62
  # @param interests [:r | :w | :rw] what events to wait for.
54
63
  # @param duration [Float] timeout after the given duration if not `nil`.
55
64
  def wait_any(interests = :rw, duration = nil)
56
- monitor(interests, duration)
65
+ # There is value in caching this monitor - if you can reuse it, you will get about 2x the throughput, because you avoid calling Reactor#register and Monitor#close for every call. That being said, by caching it, you also introduce lifetime issues. I'm going to accept this overhead into the wrapper design because it's pretty convenient, but if you want faster IO, take a look at the performance spec which compares this method with a more direct alternative.
66
+ if @reactor
67
+ unless @monitor
68
+ @monitor = @reactor.register(@io, interests)
69
+ else
70
+ @monitor.interests = interests
71
+ @monitor.value = Fiber.current
72
+ end
73
+
74
+ begin
75
+ wait_for(@reactor, @monitor, duration)
76
+ ensure
77
+ @monitor.remove_interest(@monitor.interests)
78
+ @monitor.value = nil
79
+ end
80
+ else
81
+ reactor = Task.current.reactor
82
+ monitor = reactor.register(@io, interests)
83
+
84
+ begin
85
+ wait_for(reactor, monitor, duration)
86
+ ensure
87
+ monitor.close
88
+ end
89
+ end
57
90
  end
58
91
 
59
- # Close the monitor.
92
+ # Close the io and monitor.
60
93
  def close
61
- close_monitor
94
+ @monitor.close if @monitor
62
95
 
63
- @io.close if @io
96
+ @io.close
64
97
  end
65
98
 
66
99
  private
67
100
 
68
- def close_monitor
69
- if @monitor
70
- @monitor.close
71
- @monitor = nil
72
- end
73
- end
74
-
75
- if ::NIO::VERSION >= "2.0"
76
- def clear_monitor
77
- if @monitor
78
- # Alas, @monitor.interests = nil does not yet work.
79
- @monitor.value = nil
80
- @monitor.remove_interest(@monitor.interests)
81
- end
82
- end
83
- else
84
- alias clear_monitor close_monitor
85
- end
86
-
87
- # Monitor the io for the given events
88
- def monitor(interests, duration = nil)
89
- unless @monitor
90
- @monitor = @reactor.register(@io, interests)
91
- else
92
- @monitor.interests = interests
93
- end
94
-
95
- @monitor.value = Fiber.current
96
-
101
+ def wait_for(reactor, monitor, duration)
97
102
  # If the user requested an explicit timeout for this operation:
98
103
  if duration
99
- @reactor.timeout(duration) do
104
+ reactor.timeout(duration) do
100
105
  Task.yield
101
106
  end
102
107
  else
@@ -104,8 +109,6 @@ module Async
104
109
  end
105
110
 
106
111
  return true
107
- ensure
108
- clear_monitor
109
112
  end
110
113
  end
111
114
  end
Binary file
Binary file
@@ -0,0 +1,44 @@
1
+
2
+ require 'benchmark/ips'
3
+
4
+ RSpec.describe Async::Wrapper do
5
+ let(:pipe) {IO.pipe}
6
+
7
+ let(:input) {described_class.new(pipe.first)}
8
+ let(:output) {described_class.new(pipe.last)}
9
+
10
+ it "should be fast to parse large documents" do
11
+ Benchmark.ips do |x|
12
+ x.report('Wrapper#wait_readable') do |repeats|
13
+ Async::Reactor.run do |task|
14
+ input = Async::Wrapper.new(pipe.first, task.reactor)
15
+ output = pipe.last
16
+
17
+ repeats.times do
18
+ output.write(".")
19
+ input.wait_readable
20
+ input.io.read(1)
21
+ end
22
+ end
23
+ end
24
+
25
+ x.report('Reactor#register') do |repeats|
26
+ Async::Reactor.run do |task|
27
+ input = pipe.first
28
+ monitor = task.reactor.register(input, :r)
29
+ output = pipe.last
30
+
31
+ repeats.times do
32
+ output.write(".")
33
+ Async::Task.yield
34
+ input.read(1)
35
+ end
36
+
37
+ monitor.close
38
+ end
39
+ end
40
+
41
+ x.compare!
42
+ end
43
+ end
44
+ end
@@ -60,7 +60,10 @@ RSpec.describe Async::Reactor do
60
60
  end
61
61
 
62
62
  it "is closed after running" do
63
- reactor = Async::Reactor.run do
63
+ reactor = nil
64
+
65
+ Async::Reactor.run do |task|
66
+ reactor = task.reactor
64
67
  end
65
68
 
66
69
  expect(reactor).to be_closed
@@ -68,6 +71,13 @@ RSpec.describe Async::Reactor do
68
71
  expect{reactor.run}.to raise_error(RuntimeError, /closed/)
69
72
  end
70
73
 
74
+ it "should return a task" do
75
+ result = Async::Reactor.run do |task|
76
+ end
77
+
78
+ expect(result).to be_kind_of(Async::Task)
79
+ end
80
+
71
81
  describe '#async' do
72
82
  include_context Async::RSpec::Reactor
73
83
 
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.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: 2018-02-02 00:00:00.000000000 Z
11
+ date: 2018-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nio4r
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '2.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: timers
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -113,6 +113,9 @@ files:
113
113
  - README.md
114
114
  - Rakefile
115
115
  - async.gemspec
116
+ - benchmark/async_vs_lightio.rb
117
+ - examples/async_method.rb
118
+ - examples/sleep_sort.rb
116
119
  - lib/async.rb
117
120
  - lib/async/condition.rb
118
121
  - lib/async/logger.rb
@@ -124,8 +127,11 @@ files:
124
127
  - lib/async/wrapper.rb
125
128
  - logo.png
126
129
  - logo.svg
130
+ - papers/1982 Grossman.pdf
131
+ - papers/1987 ODell.pdf
127
132
  - spec/async/condition_spec.rb
128
133
  - spec/async/node_spec.rb
134
+ - spec/async/performance_spec.rb
129
135
  - spec/async/reactor/nested_spec.rb
130
136
  - spec/async/reactor_spec.rb
131
137
  - spec/async/task_spec.rb
@@ -141,9 +147,9 @@ require_paths:
141
147
  - lib
142
148
  required_ruby_version: !ruby/object:Gem::Requirement
143
149
  requirements:
144
- - - "~>"
150
+ - - ">="
145
151
  - !ruby/object:Gem::Version
146
- version: '2.0'
152
+ version: 2.2.7
147
153
  required_rubygems_version: !ruby/object:Gem::Requirement
148
154
  requirements:
149
155
  - - ">="
@@ -151,13 +157,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
151
157
  version: '0'
152
158
  requirements: []
153
159
  rubyforge_project:
154
- rubygems_version: 2.6.12
160
+ rubygems_version: 2.7.6
155
161
  signing_key:
156
162
  specification_version: 4
157
163
  summary: Async is an asynchronous I/O framework based on nio4r.
158
164
  test_files:
159
165
  - spec/async/condition_spec.rb
160
166
  - spec/async/node_spec.rb
167
+ - spec/async/performance_spec.rb
161
168
  - spec/async/reactor/nested_spec.rb
162
169
  - spec/async/reactor_spec.rb
163
170
  - spec/async/task_spec.rb