async 1.21.0 → 1.22.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: b9c9a081ed81fc6b4b0b34ebb5282063574994045189a9544b968c7729543cf1
4
- data.tar.gz: df9d9266a02ff86895cfc9934fbafbeeb7159cb1b964f10705323d9f4798ae34
3
+ metadata.gz: 4fa7e7ae29e8bff86757e97d1f63bb5fd297caa79f97b5e4ccdc1f4568d65609
4
+ data.tar.gz: 17239cfec1b3b5062fdf6745b7708ebbdd9f39c63199dc33cc6495362da016ca
5
5
  SHA512:
6
- metadata.gz: 1f5014904c26006fd50462c9161f3ae24f867ed995569bc1faecebe67d3a1b8799eb4a91896121a07885512ae21361be18f9f9ea3614825140cb3596239cc13b
7
- data.tar.gz: 26c13f37945ceb4d2542a2ef0736b6fe04cc9515d90053d4b2a00888a5501c008ce614849462b006216a94a1eece3fc781cf378f25f369ae65bceeede12dbfe6
6
+ metadata.gz: 18566e7d62eca30c72491cc06f633baa1f2b027fbb474f7bb226d0c5ea09cc095e06b3f2943470f378d98a2174ff8e0b4d7875643f3abba751fe69c76e3f6657
7
+ data.tar.gz: 12aed7ad107c756e90292a2179336441205d66a4f457dcf1d55e5655d9bc361753a9b4deb48c644104ca5d349610a1eb33773e995c91a7e2b92a71c97df3a0cd
@@ -0,0 +1,51 @@
1
+ # (All) Rubies Benchmark
2
+
3
+ This is a simple benchmark, which reads and writes data over a pipe.
4
+
5
+ It is designed to work as far back as Ruby 1.9.3 at the expense of code clarity. It also works on JRuby and TruffleRuby.
6
+
7
+ ## Usage
8
+
9
+ The simplest way is to use RVM.
10
+
11
+ rvm all do ./benchmark.rb
12
+
13
+ ## Results
14
+
15
+ General improvements.
16
+
17
+ ruby 1.9.3p551 (2014-11-13 revision 48407) [x86_64-linux]
18
+ #<struct Struct::Tms utime=63.41, stime=7.15, cutime=0.0, cstime=0.0>
19
+
20
+ ruby 2.0.0p648 (2015-12-16 revision 53162) [x86_64-linux]
21
+ #<struct Struct::Tms utime=59.5, stime=6.57, cutime=0.0, cstime=0.0>
22
+
23
+ ruby 2.1.10p492 (2016-04-01 revision 54464) [x86_64-linux]
24
+ #<struct Process::Tms utime=40.53, stime=6.87, cutime=0.0, cstime=0.0>
25
+
26
+ ruby 2.2.10p489 (2018-03-28 revision 63023) [x86_64-linux]
27
+ #<struct Process::Tms utime=41.26, stime=6.62, cutime=0.0, cstime=0.0>
28
+
29
+ ruby 2.3.8p459 (2018-10-18 revision 65136) [x86_64-linux]
30
+ #<struct Process::Tms utime=31.85, stime=6.55, cutime=0.0, cstime=0.0>
31
+
32
+ ruby 2.4.6p354 (2019-04-01 revision 67394) [x86_64-linux]
33
+ #<struct Process::Tms utime=41.89, stime=6.72, cutime=0.0, cstime=0.0>
34
+
35
+ ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-linux]
36
+ #<struct Process::Tms utime=26.446285, stime=6.549777, cutime=0.0, cstime=0.0>
37
+
38
+ Native fiber implementation & reduced syscalls (https://bugs.ruby-lang.org/issues/14739).
39
+
40
+ ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux]
41
+ #<struct Process::Tms utime=20.045192, stime=5.5941600000000005, cutime=0.0, cstime=0.0>
42
+
43
+ Performance regression (https://bugs.ruby-lang.org/issues/16009).
44
+
45
+ ruby 2.7.0preview1 (2019-05-31 trunk c55db6aa271df4a689dc8eb0039c929bf6ed43ff) [x86_64-linux]
46
+ #<struct Process::Tms utime=25.193268, stime=5.808202, cutime=0.0, cstime=0.0>
47
+
48
+ Improve fiber performance using pool alloation strategy (https://bugs.ruby-lang.org/issues/15997).
49
+
50
+ ruby 2.7.0dev (2019-10-02T08:19:14Z trunk 9759e3c9f0) [x86_64-linux]
51
+ #<struct Process::Tms utime=19.110835, stime=5.738776, cutime=0.0, cstime=0.0>
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'socket'
4
+ require 'fiber'
5
+
6
+ puts
7
+ puts RUBY_DESCRIPTION
8
+
9
+ if RUBY_VERSION < "2.0"
10
+ class String
11
+ def b
12
+ self
13
+ end
14
+ end
15
+ end
16
+
17
+ # TODO: make these much larger, see if we're effectively batching
18
+ # even if we don't mean to...
19
+ QUERY_TEXT = "STATUS".freeze
20
+ RESPONSE_TEXT = "OK".freeze
21
+
22
+ NUM_WORKERS = (ARGV[0] || 10_000).to_i
23
+ NUM_REQUESTS = (ARGV[1] || 100).to_i
24
+
25
+ # Fiber reactor code taken from
26
+ # https://www.codeotaku.com/journal/2018-11/fibers-are-the-right-solution/index
27
+ class Reactor
28
+ def initialize
29
+ @readable = {}
30
+ @writable = {}
31
+ end
32
+
33
+ def run
34
+ while @readable.any? or @writable.any?
35
+ readable, writable = IO.select(@readable.keys, @writable.keys, [])
36
+
37
+ readable.each do |io|
38
+ @readable[io].resume
39
+ end
40
+
41
+ writable.each do |io|
42
+ @writable[io].resume
43
+ end
44
+ end
45
+ end
46
+
47
+ def wait_readable(io)
48
+ @readable[io] = Fiber.current
49
+ Fiber.yield
50
+ @readable.delete(io)
51
+ end
52
+
53
+ def wait_writable(io)
54
+ @writable[io] = Fiber.current
55
+ Fiber.yield
56
+ @writable.delete(io)
57
+ end
58
+ end
59
+
60
+ class Wrapper
61
+ def initialize(io, reactor)
62
+ @io = io
63
+ @reactor = reactor
64
+ end
65
+
66
+ if RUBY_VERSION >= "2.3"
67
+ def read_nonblock(length, buffer)
68
+ while true
69
+ case result = @io.read_nonblock(length, buffer, exception: false)
70
+ when :wait_readable
71
+ @reactor.wait_readable(@io)
72
+ when :wait_writable
73
+ @reactor.wait_writable(@io)
74
+ else
75
+ return result
76
+ end
77
+ end
78
+
79
+ end
80
+
81
+ def write_nonblock(buffer)
82
+ while true
83
+ case result = @io.write_nonblock(buffer, exception: false)
84
+ when :wait_readable
85
+ @reactor.wait_readable(@io)
86
+ when :wait_writable
87
+ @reactor.wait_writable(@io)
88
+ else
89
+ return result
90
+ end
91
+ end
92
+ end
93
+ else
94
+ def read_nonblock(length, buffer)
95
+ while true
96
+ begin
97
+ return @io.read_nonblock(length, buffer)
98
+ rescue IO::WaitReadable
99
+ @reactor.wait_readable(@io)
100
+ rescue IO::WaitWritable
101
+ @reactor.wait_writable(@io)
102
+ end
103
+ end
104
+ end
105
+
106
+ def write_nonblock(buffer)
107
+ while true
108
+ begin
109
+ return @io.write_nonblock(buffer)
110
+ rescue IO::WaitReadable
111
+ @reactor.wait_readable(@io)
112
+ rescue IO::WaitWritable
113
+ @reactor.wait_writable(@io)
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ def read(length, buffer = nil)
120
+ if buffer
121
+ buffer.clear
122
+ else
123
+ buffer = String.new.b
124
+ end
125
+
126
+ result = self.read_nonblock(length - buffer.bytesize, buffer)
127
+
128
+ if result == length
129
+ return result
130
+ end
131
+
132
+ chunk = String.new.b
133
+ while chunk = self.read_nonblock(length - buffer.bytesize, chunk)
134
+ buffer << chunk
135
+
136
+ break if buffer.bytesize == length
137
+ end
138
+
139
+ return buffer
140
+ end
141
+
142
+ def write(buffer)
143
+ remaining = buffer.dup
144
+
145
+ while true
146
+ result = self.write_nonblock(remaining)
147
+
148
+ if result == remaining.bytesize
149
+ return buffer.bytesize
150
+ else
151
+ remaining = remaining.byteslice(result, remaining.bytesize - result)
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ reactor = Reactor.new
158
+
159
+ worker_read = []
160
+ worker_write = []
161
+
162
+ master_read = []
163
+ master_write = []
164
+
165
+ workers = []
166
+
167
+ # puts "Setting up pipes..."
168
+ NUM_WORKERS.times do |i|
169
+ r, w = IO.pipe
170
+ worker_read.push Wrapper.new(r, reactor)
171
+ master_write.push Wrapper.new(w, reactor)
172
+
173
+ r, w = IO.pipe
174
+ worker_write.push Wrapper.new(w, reactor)
175
+ master_read.push Wrapper.new(r, reactor)
176
+ end
177
+
178
+ # puts "Setting up fibers..."
179
+ NUM_WORKERS.times do |i|
180
+ f = Fiber.new do
181
+ # Worker code
182
+ NUM_REQUESTS.times do |req_num|
183
+ q = worker_read[i].read(QUERY_TEXT.size)
184
+ if q != QUERY_TEXT
185
+ raise "Fail! Expected #{QUERY_TEXT.inspect} but got #{q.inspect} on request #{req_num.inspect}!"
186
+ end
187
+ worker_write[i].write(RESPONSE_TEXT)
188
+ end
189
+ end
190
+ workers.push f
191
+ end
192
+
193
+ workers.each { |f| f.resume }
194
+
195
+ master_fiber = Fiber.new do
196
+ NUM_WORKERS.times do |worker_num|
197
+ f = Fiber.new do
198
+ NUM_REQUESTS.times do |req_num|
199
+ master_write[worker_num].write(QUERY_TEXT)
200
+ buffer = master_read[worker_num].read(RESPONSE_TEXT.size)
201
+ if buffer != RESPONSE_TEXT
202
+ raise "Error! Fiber no. #{worker_num} on req #{req_num} expected #{RESPONSE_TEXT.inspect} but got #{buf.inspect}!"
203
+ end
204
+ end
205
+ end
206
+ f.resume
207
+ end
208
+ end
209
+
210
+ master_fiber.resume
211
+
212
+ # puts "Starting reactor..."
213
+ reactor.run
214
+
215
+ # puts "Done, finished all reactor Fibers!"
216
+
217
+ puts Process.times
218
+
219
+ # Exit
@@ -0,0 +1,51 @@
1
+ # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'task'
22
+
23
+ module Async
24
+ # A semaphore is used to control access to a common resource in a concurrent system. A useful way to think of a semaphore as used in the real-world systems is as a record of how many units of a particular resource are available, coupled with operations to adjust that record safely (i.e. to avoid race conditions) as units are required or become free, and, if necessary, wait until a unit of the resource becomes available.
25
+ class Barrier
26
+ def initialize
27
+ @tasks = []
28
+ end
29
+
30
+ # All tasks which have been invoked into the barrier.
31
+ attr :tasks
32
+
33
+ def async(*args, parent: Task.current, **options, &block)
34
+ task = parent.async(*args, **options, &block)
35
+
36
+ @tasks << task
37
+
38
+ return task
39
+ end
40
+
41
+ def empty?
42
+ @tasks.empty?
43
+ end
44
+
45
+ def wait
46
+ while task = @tasks.pop
47
+ task.wait
48
+ end
49
+ end
50
+ end
51
+ end
@@ -116,9 +116,9 @@ module Async
116
116
  # When calling an async block, we deterministically execute it until the
117
117
  # first blocking operation. We don't *have* to do this - we could schedule
118
118
  # it for later execution, but it's useful to:
119
- # - Fail at the point of call where possible.
119
+ # - Fail at the point of the method call where possible.
120
120
  # - Execute determinstically where possible.
121
- # - Avoid overhead if no blocking operation is performed.
121
+ # - Avoid scheduler overhead if no blocking operation is performed.
122
122
  task.run(*args)
123
123
 
124
124
  # logger.debug "Initial execution of task #{fiber} complete (#{result} -> #{fiber.alive?})..."
@@ -168,7 +168,7 @@ module Async
168
168
 
169
169
  initial_task = self.async(*args, &block) if block_given?
170
170
 
171
- @timers.wait do |interval|
171
+ until @stopped
172
172
  # logger.debug(self) {"@ready = #{@ready} @running = #{@running}"}
173
173
 
174
174
  if @ready.any?
@@ -180,17 +180,16 @@ module Async
180
180
  end
181
181
 
182
182
  @running.clear
183
-
184
- # if there are tasks ready to execute, don't sleep.
185
- if @ready.any?
186
- interval = 0
187
- else
188
- # The above tasks may schedule, cancel or affect timers in some way. We need to compute a new wait interval for the blocking selector call below:
189
- interval = @timers.wait_interval
190
- end
191
183
  end
192
184
 
193
- # As timeouts may have been updated, and caused fibers to complete, we should check this.
185
+ if @ready.empty?
186
+ interval = @timers.wait_interval
187
+ else
188
+ # if there are tasks ready to execute, don't sleep:
189
+ interval = 0
190
+ end
191
+
192
+ # If there is no interval to wait (thus no timers), and no tasks, we could be done:
194
193
  if interval.nil?
195
194
  if self.finished?
196
195
  # If there is nothing to do, then finish:
@@ -207,7 +206,9 @@ module Async
207
206
  monitor.value.resume
208
207
  end
209
208
  end
210
- end until @stopped
209
+
210
+ @timers.fire
211
+ end
211
212
 
212
213
  return initial_task
213
214
  ensure
@@ -47,12 +47,10 @@ module Async
47
47
  end
48
48
 
49
49
  # Run an async task. Will wait until the semaphore is ready until spawning and running the task.
50
- def async(*args)
51
- parent = Task.current
52
-
50
+ def async(*args, parent: Task.current, **options)
53
51
  wait
54
52
 
55
- parent.async do |task|
53
+ parent.async(**options) do |task|
56
54
  @count += 1
57
55
 
58
56
  begin
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Async
22
- VERSION = "1.21.0"
22
+ VERSION = "1.22.0"
23
23
  end
@@ -0,0 +1,77 @@
1
+ # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'async/barrier'
22
+ require 'async/clock'
23
+ require 'async/rspec'
24
+
25
+ RSpec.describe Async::Barrier do
26
+ include_context Async::RSpec::Reactor
27
+
28
+ context '#async' do
29
+ let(:repeats) {40}
30
+ let(:delay) {0.1}
31
+
32
+ it 'should wait for all jobs to complete' do
33
+ finished = 0
34
+
35
+ repeats.times.map do |i|
36
+ subject.async do |task|
37
+ task.sleep(delay)
38
+ finished += 1
39
+
40
+ # This task is a child task but not part of the barrier.
41
+ task.async do
42
+ task.sleep(delay*3)
43
+ end
44
+ end
45
+ end
46
+
47
+ expect(subject).to_not be_empty
48
+ expect(finished).to be < repeats
49
+
50
+ duration = Async::Clock.measure{subject.wait}
51
+
52
+ expect(duration).to be < (delay * 2)
53
+ expect(finished).to be == repeats
54
+ expect(subject).to be_empty
55
+ end
56
+ end
57
+
58
+ context '#wait' do
59
+ it 'should wait for tasks even after exceptions' do
60
+ task1 = subject.async do
61
+ raise "Boom"
62
+ end
63
+
64
+ task2 = subject.async do
65
+ end
66
+
67
+ expect(task1).to be_failed
68
+ expect(task2).to be_finished
69
+
70
+ expect{subject.wait}.to raise_exception(/Boom/)
71
+
72
+ subject.wait until subject.empty?
73
+
74
+ expect(subject).to be_empty
75
+ end
76
+ end
77
+ end
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.21.0
4
+ version: 1.22.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-09-17 00:00:00.000000000 Z
11
+ date: 2019-10-08 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
  - async.gemspec
145
145
  - benchmark/async_vs_lightio.rb
146
146
  - benchmark/fiber_count.rb
147
+ - benchmark/rubies/README.md
148
+ - benchmark/rubies/benchmark.rb
147
149
  - benchmark/thread_count.rb
148
150
  - benchmark/thread_vs_fiber.rb
149
151
  - examples/async_method.rb
@@ -157,6 +159,7 @@ files:
157
159
  - examples/stop/sleep.rb
158
160
  - gems/event.gemfile
159
161
  - lib/async.rb
162
+ - lib/async/barrier.rb
160
163
  - lib/async/clock.rb
161
164
  - lib/async/condition.rb
162
165
  - lib/async/debug/monitor.rb
@@ -176,6 +179,7 @@ files:
176
179
  - logo.svg
177
180
  - papers/1982 Grossman.pdf
178
181
  - papers/1987 ODell.pdf
182
+ - spec/async/barrier_spec.rb
179
183
  - spec/async/clock_spec.rb
180
184
  - spec/async/condition_examples.rb
181
185
  - spec/async/condition_spec.rb
@@ -218,6 +222,7 @@ signing_key:
218
222
  specification_version: 4
219
223
  summary: Async is an asynchronous I/O framework based on nio4r.
220
224
  test_files:
225
+ - spec/async/barrier_spec.rb
221
226
  - spec/async/clock_spec.rb
222
227
  - spec/async/condition_examples.rb
223
228
  - spec/async/condition_spec.rb