innertube 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -153,6 +153,7 @@ module Innertube
153
153
  @iterator.synchronize do
154
154
  until targets.empty?
155
155
  @lock.synchronize do
156
+ @element_released.wait(@iterator) if targets.all? {|e| e.locked? }
156
157
  unlocked, targets = targets.partition {|e| e.unlocked? }
157
158
  unlocked.each {|e| e.lock }
158
159
  end
@@ -164,7 +165,6 @@ module Innertube
164
165
  e.unlock
165
166
  end
166
167
  end
167
- @element_released.wait(@iterator) unless targets.empty?
168
168
  end
169
169
  end
170
170
  end
@@ -1,3 +1,3 @@
1
1
  module Innertube
2
- VERSION = "1.0.1"
2
+ VERSION = "1.0.2"
3
3
  end
@@ -1,103 +1,102 @@
1
1
  require 'spec_helper'
2
2
  require 'thread'
3
+ require 'thwait'
3
4
 
4
5
  describe Innertube::Pool do
5
- let(:pool_members) { subject.instance_variable_get(:@pool) }
6
-
7
- describe 'basics' do
8
- subject do
9
- described_class.new(lambda { [0] }, lambda { |x| })
6
+ def wait_all(threads)
7
+ message "Waiting on #{threads.size} threads: "
8
+ ThreadsWait.all_waits(*threads) do |t|
9
+ message "<#{threads.index(t) + 1}> "
10
10
  end
11
+ message "\n"
12
+ end
11
13
 
12
- it 'yields a new object' do
13
- subject.take do |x|
14
- x.should == [0]
15
- end
14
+ let(:pool_members) { pool.instance_variable_get(:@pool) }
15
+
16
+ let(:pool) { described_class.new(lambda { [0] }, lambda { |x| }) }
17
+
18
+ it 'yields a new object when the pool is empty' do
19
+ pool.take do |x|
20
+ x.should == [0]
16
21
  end
22
+ end
17
23
 
18
- it 'retains a single object for serial access' do
19
- n = 100
20
- n.times do |i|
21
- subject.take do |x|
22
- x.should == [i]
23
- x[0] += 1
24
- end
24
+ it 'retains a single object for serial access' do
25
+ n = 100
26
+ n.times do |i|
27
+ pool.take do |x|
28
+ x.should == [i]
29
+ x[0] += 1
25
30
  end
26
- subject.size.should == 1
27
31
  end
32
+ pool.size.should == 1
33
+ end
28
34
 
29
- it 'should be re-entrant' do
30
- n = 10
31
- n.times do |i|
32
- subject.take do |x|
33
- x.replace [1]
34
- subject.take do |y|
35
- y.replace [2]
36
- subject.take do |z|
37
- z.replace [3]
38
- subject.take do |t|
39
- t.replace [4]
40
- end
35
+ it 'should be re-entrant' do
36
+ n = 10
37
+ n.times do |i|
38
+ pool.take do |x|
39
+ x.replace [1]
40
+ pool.take do |y|
41
+ y.replace [2]
42
+ pool.take do |z|
43
+ z.replace [3]
44
+ pool.take do |t|
45
+ t.replace [4]
41
46
  end
42
47
  end
43
48
  end
44
49
  end
45
- subject.instance_variable_get(:@pool).map { |e| e.object.first }.sort.should == [1,2,3,4]
46
50
  end
51
+ pool_members.map { |e| e.object.first }.sort.should == [1,2,3,4]
52
+ end
47
53
 
48
54
 
49
- it 'should unlock when exceptions are raised' do
50
- begin
51
- subject.take do |x|
52
- x << 1
53
- subject.take do |y|
54
- x << 2
55
- y << 3
56
- raise
57
- end
55
+ it 'should unlock when exceptions are raised' do
56
+ begin
57
+ pool.take do |x|
58
+ x << 1
59
+ pool.take do |y|
60
+ x << 2
61
+ y << 3
62
+ raise
58
63
  end
59
- rescue
60
64
  end
61
- pool_members.should be_all {|e| not e.owner }
62
- pool_members.map { |e| e.object }.should =~ [[0,1,2],[0,3]]
65
+ rescue
63
66
  end
67
+ pool_members.should be_all {|e| not e.owner }
68
+ pool_members.map { |e| e.object }.should =~ [[0,1,2],[0,3]]
69
+ end
64
70
 
65
- context 'when BadResource is raised' do
66
- subject do
67
- described_class.new(lambda do
68
- m = mock('resource')
69
- m.should_receive(:close)
70
- m
71
- end,
72
- lambda do |res|
73
- res.close
74
- end)
75
- end
71
+ context 'when BadResource is raised' do
72
+ let(:pool) do
73
+ described_class.new(lambda { mock('resource').tap {|m| m.should_receive(:close) } },
74
+ lambda { |res| res.close })
75
+ end
76
76
 
77
- it 'should delete' do
78
- lambda do
79
- subject.take do |x|
80
- raise Innertube::Pool::BadResource
81
- end
82
- end.should raise_error(Innertube::Pool::BadResource)
83
- subject.size.should == 0
84
- end
77
+ it 'should remove the member from the pool' do
78
+ lambda do
79
+ pool.take do |x|
80
+ raise Innertube::Pool::BadResource
81
+ end
82
+ end.should raise_error(Innertube::Pool::BadResource)
83
+ pool_members.size.should == 0
85
84
  end
86
85
  end
87
86
 
88
- describe 'threads' do
89
- subject {
90
- described_class.new(lambda { [] }, lambda { |x| })
91
- }
87
+
88
+ context 'threaded access' do
89
+ let!(:pool) { described_class.new(lambda { [] }, lambda { |x| }) }
92
90
 
93
91
  it 'should allocate n objects for n concurrent operations' do
94
92
  # n threads concurrently allocate and sign objects from the pool
95
93
  n = 10
96
94
  readyq = Queue.new
97
95
  finishq = Queue.new
96
+
98
97
  threads = (0...n).map do
99
98
  Thread.new do
100
- subject.take do |x|
99
+ pool.take do |x|
101
100
  readyq << 1
102
101
  x << Thread.current
103
102
  finishq.pop
@@ -105,16 +104,18 @@ describe Innertube::Pool do
105
104
  end
106
105
  end
107
106
 
107
+ # Give the go-ahead to all threads
108
108
  n.times { readyq.pop }
109
+
110
+ # Let all threads finish
109
111
  n.times { finishq << 1 }
110
112
 
111
113
  # Wait for completion
112
- threads.each do |t|
113
- t.join
114
- end
114
+ ThreadsWait.all_waits(*threads)
115
115
 
116
116
  # Should have taken exactly n objects to do this
117
- subject.size.should == n
117
+ pool_members.size.should == n
118
+
118
119
  # And each one should be signed exactly once
119
120
  pool_members.map do |e|
120
121
  e.object.size.should == 1
@@ -124,16 +125,17 @@ describe Innertube::Pool do
124
125
 
125
126
  it 'take with filter and default' do
126
127
  n = 10
127
- subject = described_class.new(lambda { [] }, lambda { |x| })
128
+ pool = described_class.new(lambda { [] }, lambda { |x| })
128
129
 
129
130
  # Allocate several elements of the pool
130
131
  q = Queue.new
132
+ finishq = Queue.new
131
133
  threads = (0...n).map do |i|
132
134
  Thread.new do
133
- subject.take do |a|
134
- a << i
135
+ pool.take do |a|
135
136
  q << 1
136
- sleep 0.02
137
+ a << i
138
+ finishq.pop
137
139
  end
138
140
  end
139
141
  end
@@ -141,29 +143,32 @@ describe Innertube::Pool do
141
143
  # Wait for all threads to have acquired an element
142
144
  n.times { q.pop }
143
145
 
144
- threads.each do |t|
145
- t.join
146
- end
146
+ # Let all threads finish
147
+ n.times { finishq << 1 }
148
+
149
+ # Wait for completion
150
+ # threads.each {|t| t.join }
151
+ ThreadsWait.all_waits(*threads)
147
152
 
148
153
  # Get and delete existing even elements
149
- got = Set.new
154
+ got = []
150
155
  (n / 2).times do
151
156
  begin
152
- subject.take(
153
- :filter => lambda { |x| x.first.even? },
154
- :default => [:default]
155
- ) do |x|
157
+ pool.take(
158
+ :filter => lambda { |x| x.first.even? },
159
+ :default => [:default]
160
+ ) do |x|
156
161
  got << x.first
157
162
  raise Innertube::Pool::BadResource
158
163
  end
159
164
  rescue Innertube::Pool::BadResource
160
165
  end
161
166
  end
162
- got.should == (0...n).select(&:even?).to_set
167
+ got.should =~ (0...n).select(&:even?)
163
168
 
164
169
  # This time, no even elements exist, so we should get the default.
165
- subject.take(:filter => lambda { |x| x.first.even? },
166
- :default => :default) do |x|
170
+ pool.take(:filter => lambda { |x| x.first.even? },
171
+ :default => :default) do |x|
167
172
  x.should == :default
168
173
  end
169
174
  end
@@ -174,7 +179,7 @@ describe Innertube::Pool do
174
179
  threads = (0..n).map do
175
180
  Thread.new do
176
181
  psleep = 0.75 * rand # up to 50ms sleep
177
- subject.take do |a|
182
+ pool.take do |a|
178
183
  started << 1
179
184
  a << rand
180
185
  sleep psleep
@@ -185,114 +190,209 @@ describe Innertube::Pool do
185
190
  n.times { started.pop }
186
191
  touched = []
187
192
 
188
- subject.each do |e|
189
- touched << e
190
- end
193
+ pool.each {|e| touched << e }
191
194
 
192
- threads.each do |t|
193
- t.join
194
- end
195
+ wait_all threads
195
196
 
196
- touched.should be_all {|item| pool_members.find {|e| e.object == item } }
197
+ touched.should be_all {|item| pool_members.any? {|e| e.object == item } }
197
198
  end
198
199
 
199
- it 'should clear' do
200
- n = 10
201
- subject = described_class.new(
202
- lambda { mock('connection').tap {|m| m.should_receive(:teardown) }},
203
- lambda { |b| b.teardown }
204
- )
200
+ context 'clearing the pool' do
201
+ let(:pool) do
202
+ described_class.new(lambda { mock('connection').tap {|m| m.should_receive(:teardown) }},
203
+ lambda { |b| b.teardown })
204
+ end
205
205
 
206
- # Allocate several elements of the pool
207
- q = Queue.new
208
- threads = (0...n).map do |i|
209
- Thread.new do
210
- subject.take do |a|
211
- q << 1
212
- sleep 0.1
206
+ it 'should remove all elements' do
207
+ n = 10
208
+ q, fq = Queue.new, Queue.new
209
+
210
+ # Allocate several elements of the pool
211
+ threads = (0...n).map do |i|
212
+ Thread.new do
213
+ pool.take do |a|
214
+ q << i
215
+ sleep(rand * 0.5)
216
+ message "W<#{i}> "
217
+ fq.pop
218
+ message "X<#{i}> "
219
+ end
213
220
  end
214
221
  end
215
- end
216
222
 
217
- # Wait for all threads to have acquired an element
218
- n.times { q.pop }
223
+ # Wait for all threads to have acquired an element
224
+ n.times { message "S<#{q.pop}> " }
225
+
226
+ # Start a thread to push stuff onto the finish queue, allowing
227
+ # the worker threads to exit
228
+ pusher = Thread.new do
229
+ n.times do |i|
230
+ message "R<#{i}> "
231
+ fq << 1
232
+ sleep(rand * 0.1)
233
+ end
234
+ end
219
235
 
220
- # Clear the pool while threads still have elements checked out
221
- subject.clear
222
- pool_members.should be_empty
236
+ # Clear the pool while threads still have elements checked out
237
+ message "S<C> "
238
+ pool.clear
239
+ message "X<C> "
223
240
 
224
- # Wait for threads to complete
225
- threads.each do |t|
226
- t.join
241
+ # Wait for threads to complete
242
+ wait_all(threads + [pusher])
243
+ pool_members.should be_empty
227
244
  end
228
245
  end
229
246
 
230
- it 'should delete_if' do
231
- n = 10
232
- subject = described_class.new(
233
- lambda { [] },
234
- lambda { |x| }
235
- )
247
+ context 'conditionally deleting members' do
248
+ let(:pool) { described_class.new( lambda { [] }, lambda { |x| } ) }
249
+ it 'should remove them from the pool' do
250
+ n = 10
251
+
252
+ # Allocate several elements of the pool
253
+ q = Queue.new
254
+ threads = (0...n).map do |i|
255
+ Thread.new do
256
+ pool.take do |a|
257
+ message "S<#{i}> "
258
+ a << i
259
+ q << i
260
+ Thread.pass
261
+ end
262
+ end
263
+ end
236
264
 
237
- # Allocate several elements of the pool
238
- q = Queue.new
265
+ # Wait for all threads to have acquired an element
266
+ n.times { message "X<#{q.pop}> " }
267
+
268
+ # Delete odd elements
269
+ pool.delete_if do |x|
270
+ x.first.odd?
271
+ end
272
+
273
+ # Verify odds are gone.
274
+ pool_members.all? do |x|
275
+ x.object.first.even?
276
+ end.should == true
277
+
278
+ # Wait for threads
279
+ wait_all threads
280
+ end
281
+ end
282
+
283
+ it 'iteration race-condition regression', :timeout => 60 do
284
+ # This simulates a race-condition where the condition variable
285
+ # waited on by the iterator until an element is released might
286
+ # be signaled before the iterator begins waiting, thus dropping
287
+ # the signal and sending the iterator into an infinite wait.
288
+
289
+ # First we pick a largish random thread count, and split it into
290
+ # threads that release before the iterator starts (split) and
291
+ # ones that release while the iterator is busy (rest).
292
+ n = rand(250)
293
+ split = rand(n)
294
+ rest = n - split
295
+
296
+ message "[#{n}:#{split}] "
297
+ # We use two queues to signal between the main thread and the
298
+ # workers, and a queue to communicate with the iterator thread
299
+ sq, fq, iq = Queue.new, Queue.new, Queue.new
300
+
301
+ # Start all the worker threads
239
302
  threads = (0...n).map do |i|
240
303
  Thread.new do
241
- subject.take do |a|
242
- a << i
243
- q << 1
244
- sleep 0.02
304
+ pool.take do |e|
305
+ # Signal to the main thread that we're inside the take
306
+ sq << i+1
307
+ # Block waiting on the main thread. When reactivated, log
308
+ # the exit of the thread
309
+ fq.pop
310
+ message "X<#{i+1}> "
311
+ sq << Thread.current
245
312
  end
246
313
  end
247
314
  end
248
315
 
249
- # Wait for all threads to have acquired an element
250
- n.times { q.pop }
316
+ # Wait for all workers to start up, log their startup to the console
317
+ n.times { message "S<#{sq.pop}> " }
251
318
 
252
- # Delete odd elements
253
- subject.delete_if do |x|
254
- x.first.odd?
255
- end
319
+ message "[all started] "
256
320
 
257
- # Verify odds are gone.
258
- pool_members.all? do |x|
259
- x.object.first.even?
260
- end.should == true
321
+ # Now signal for the first group to continue
322
+ finished = []
323
+ split.times { fq << 1; finished << sq.pop }
324
+ wait_all finished
261
325
 
262
- # Wait for threads
263
- threads.each do |t|
264
- t.join
326
+ message "[first group #{split}] "
327
+
328
+ # Start the iterator thread
329
+ iterator = Thread.new do
330
+ Thread.current[:wait] = true
331
+ pool.each do |e|
332
+ # Block in the first iteration so the other workers can exit
333
+ # while the iterator is not waiting on the condition variable
334
+ if Thread.current[:wait]
335
+ sq << 'i'
336
+ iq.pop
337
+ Thread.current[:wait] = false
338
+ end
339
+ # Make sure we've touched every element of the pool by
340
+ # modifying every entry.
341
+ e << 1
342
+ end
343
+ message "X<i> "
265
344
  end
345
+
346
+ # Wait on the iterator thread to start
347
+ message "S<#{sq.pop}> "
348
+
349
+ # Now signal the remaining workers to finish, and wait on all
350
+ # workers to exit (even ones that exited in the first pass)
351
+ finished.clear
352
+ rest.times { fq << 1; finished << sq.pop }
353
+ wait_all(finished)
354
+
355
+ message "[second group #{rest}] "
356
+
357
+ # Now signal the iterator to continue, and wait for it to exit
358
+ iq << 1
359
+ wait_all([ iterator ])
360
+
361
+ # Finally, verify that all elements of the pool were touched by
362
+ # the iterator
363
+ pool_members.each {|e| e.object.size.should == 1 }
266
364
  end
267
365
 
268
- it 'stress test', :slow => true do
269
- n = 100
270
- psleep = 0.8
271
- tsleep = 0.01
272
- rounds = 100
366
+ it 'stress test', :timeout => 60 do
367
+ n = rand(400)
368
+ passes = rand(20)
369
+ rounds = rand(200)
370
+ breaker = rand
371
+ message "[#{n}t:#{rounds}r:#{passes}p:#{'%0.5f' % breaker}b] "
273
372
 
274
373
  threads = (0...n).map do
275
374
  Thread.new do
276
375
  rounds.times do |i|
277
- subject.take do |a|
376
+ pool.take do |a|
278
377
  a.should == []
279
378
  a << Thread.current
280
379
  a.should == [Thread.current]
281
380
 
282
- # Sleep and check
283
- while rand < psleep
284
- sleep tsleep
381
+ # Pass and check
382
+ passes.times do
383
+ Thread.pass
384
+ # Nobody else should get ahold of this while I'm idle
285
385
  a.should == [Thread.current]
386
+ break if rand > breaker
286
387
  end
287
388
 
288
389
  a.delete Thread.current
390
+ message "."
289
391
  end
290
392
  end
291
393
  end
292
394
  end
293
- threads.each do |t|
294
- t.join
295
- end
395
+ wait_all threads
296
396
  end
297
397
  end
298
398
  end
@@ -4,13 +4,16 @@ require 'rubygems'
4
4
  require 'rspec'
5
5
  require 'innertube'
6
6
 
7
+ require 'support/verbose_formatter'
8
+ require 'support/timeout'
9
+
7
10
  RSpec.configure do |config|
8
11
  config.mock_with :rspec
9
12
  config.filter_run :focus => true
10
13
  config.run_all_when_everything_filtered = true
11
-
12
14
  if defined?(::Java)
13
- config.seed = Time.now.utc
15
+ seed = Time.now.utc
16
+ config.seed = seed
14
17
  else
15
18
  config.order = :random
16
19
  end
@@ -0,0 +1,15 @@
1
+ require 'timeout'
2
+
3
+ RSpec.configure do |config|
4
+ config.include Timeout
5
+ config.around(:each) do |example|
6
+ time = example.metadata[:timeout] || 30
7
+ begin
8
+ timeout(time, Timeout::Error) do
9
+ example.run
10
+ end
11
+ rescue Timeout::Error => e
12
+ example.send :set_exception, e
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,112 @@
1
+ require 'rspec/core/formatters/base_text_formatter'
2
+
3
+ class VerboseFormatter < RSpec::Core::Formatters::BaseTextFormatter
4
+ attr_reader :column, :current_indentation
5
+ def initialize(*args)
6
+ super
7
+ @mutex = Mutex.new
8
+ @column = @current_indentation = 0
9
+ end
10
+
11
+ def start(count)
12
+ super
13
+ output.puts
14
+ output.puts "Running suite with seed #{RSpec.configuration.seed}\n"
15
+ output.puts
16
+ end
17
+
18
+ def example_group_started(example_group)
19
+ super
20
+ # $stderr.puts example_group.metadata.inspect
21
+ output.puts "#{padding}#{example_group.metadata[:example_group][:description_args].first}"
22
+ indent!
23
+ end
24
+
25
+ def example_group_finished(example_group)
26
+ super
27
+ outdent!
28
+ end
29
+
30
+ def example_started(example)
31
+ output.puts "#{padding}#{example.description}:"
32
+ indent!
33
+ end
34
+
35
+ def message(m)
36
+ @mutex.synchronize do
37
+ messages = m.split(/\r?\n/).reject {|s| s.empty? }
38
+ messages.each do |message|
39
+ if column + message.length > max_columns
40
+ output.puts
41
+ @column = current_indentation
42
+ end
43
+ if at_left_margin?
44
+ output.print "#{padding}#{message}"
45
+ else
46
+ output.print message
47
+ end
48
+ @column += message.length
49
+ end
50
+ end
51
+ end
52
+
53
+ def example_passed(example)
54
+ super
55
+ print_example_result green("PASS")
56
+ end
57
+
58
+ def example_failed(example)
59
+ super
60
+ print_example_result red("FAIL")
61
+ end
62
+
63
+ def example_pending(example)
64
+ super
65
+ print_example_result yellow("PENDING: #{example.metadata[:execution_result][:pending_message]}")
66
+ end
67
+
68
+ private
69
+ def print_example_result(text)
70
+ output.puts unless at_left_margin?
71
+ output.puts "#{padding}#{text}"
72
+ output.puts
73
+ outdent!
74
+ end
75
+
76
+ def at_left_margin?
77
+ column == current_indentation
78
+ end
79
+
80
+ def max_columns
81
+ @max_columns ||= ENV.include?('COLUMNS') ? ENV['COLUMNS'].to_i : 72
82
+ end
83
+
84
+ def indent_width
85
+ 2
86
+ end
87
+
88
+ def padding
89
+ ' ' * current_indentation
90
+ end
91
+
92
+ def indent!
93
+ @current_indentation += indent_width
94
+ @column = @current_indentation
95
+ end
96
+
97
+ def outdent!
98
+ @current_indentation -= indent_width
99
+ @column = @current_indentation
100
+ end
101
+ end
102
+
103
+ module ExposeFormatter
104
+ def message(string)
105
+ RSpec.configuration.formatters.first.message(string)
106
+ end
107
+ end
108
+
109
+ RSpec.configure do |config|
110
+ config.include ExposeFormatter
111
+ config.add_formatter VerboseFormatter
112
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: innertube
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-06-01 00:00:00.000000000 Z
13
+ date: 2012-07-06 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rspec
@@ -44,6 +44,8 @@ files:
44
44
  - README.md
45
45
  - spec/innertube_spec.rb
46
46
  - spec/spec_helper.rb
47
+ - spec/support/timeout.rb
48
+ - spec/support/verbose_formatter.rb
47
49
  - .gitignore
48
50
  homepage: http://github.com/basho/innertube
49
51
  licenses: []
@@ -72,4 +74,6 @@ summary: A thread-safe resource pool, originally borne in riak-client (Ripple).
72
74
  test_files:
73
75
  - spec/innertube_spec.rb
74
76
  - spec/spec_helper.rb
77
+ - spec/support/timeout.rb
78
+ - spec/support/verbose_formatter.rb
75
79
  - .gitignore