innertube 1.0.1 → 1.0.2

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