thread_safe 0.1.1-java

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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +144 -0
  5. data/README.md +34 -0
  6. data/Rakefile +36 -0
  7. data/examples/bench_cache.rb +35 -0
  8. data/ext/org/jruby/ext/thread_safe/JRubyCacheBackendLibrary.java +200 -0
  9. data/ext/org/jruby/ext/thread_safe/jsr166e/ConcurrentHashMapV8.java +3842 -0
  10. data/ext/org/jruby/ext/thread_safe/jsr166e/LongAdder.java +204 -0
  11. data/ext/org/jruby/ext/thread_safe/jsr166e/Striped64.java +342 -0
  12. data/ext/org/jruby/ext/thread_safe/jsr166y/ThreadLocalRandom.java +199 -0
  13. data/ext/thread_safe/JrubyCacheBackendService.java +15 -0
  14. data/lib/thread_safe.rb +65 -0
  15. data/lib/thread_safe/atomic_reference_cache_backend.rb +922 -0
  16. data/lib/thread_safe/cache.rb +137 -0
  17. data/lib/thread_safe/mri_cache_backend.rb +62 -0
  18. data/lib/thread_safe/non_concurrent_cache_backend.rb +133 -0
  19. data/lib/thread_safe/synchronized_cache_backend.rb +76 -0
  20. data/lib/thread_safe/synchronized_delegator.rb +35 -0
  21. data/lib/thread_safe/util.rb +16 -0
  22. data/lib/thread_safe/util/adder.rb +59 -0
  23. data/lib/thread_safe/util/atomic_reference.rb +12 -0
  24. data/lib/thread_safe/util/cheap_lockable.rb +105 -0
  25. data/lib/thread_safe/util/power_of_two_tuple.rb +26 -0
  26. data/lib/thread_safe/util/striped64.rb +226 -0
  27. data/lib/thread_safe/util/volatile.rb +62 -0
  28. data/lib/thread_safe/util/volatile_tuple.rb +46 -0
  29. data/lib/thread_safe/util/xor_shift_random.rb +39 -0
  30. data/lib/thread_safe/version.rb +3 -0
  31. data/test/test_array.rb +20 -0
  32. data/test/test_cache.rb +792 -0
  33. data/test/test_cache_loops.rb +453 -0
  34. data/test/test_hash.rb +20 -0
  35. data/test/test_helper.rb +73 -0
  36. data/test/test_synchronized_delegator.rb +42 -0
  37. data/thread_safe.gemspec +21 -0
  38. metadata +100 -0
@@ -0,0 +1,453 @@
1
+ require 'thread'
2
+ require 'test/unit'
3
+ require 'thread_safe'
4
+ require File.join(File.dirname(__FILE__), "test_helper")
5
+
6
+ Thread.abort_on_exception = true
7
+
8
+ class TestCacheTorture < Test::Unit::TestCase
9
+ THREAD_COUNT = 40
10
+ KEY_COUNT = (((2**13) - 2) * 0.75).to_i # get close to the doubling cliff
11
+ LOW_KEY_COUNT = (((2**8 ) - 2) * 0.75).to_i # get close to the doubling cliff
12
+
13
+ INITIAL_VALUE_CACHE_SETUP = lambda do |options, keys|
14
+ cache = ThreadSafe::Cache.new
15
+ initial_value = options[:initial_value] || 0
16
+ keys.each {|key| cache[key] = initial_value}
17
+ cache
18
+ end
19
+ ZERO_VALUE_CACHE_SETUP = lambda do |options, keys|
20
+ INITIAL_VALUE_CACHE_SETUP.call(options.merge(:initial_value => 0), keys)
21
+ end
22
+
23
+ DEFAULTS = {
24
+ :key_count => KEY_COUNT,
25
+ :thread_count => THREAD_COUNT,
26
+ :loop_count => 1,
27
+ :prelude => '',
28
+ :cache_setup => lambda {|options, keys| ThreadSafe::Cache.new}
29
+ }
30
+
31
+ LOW_KEY_COUNT_OPTIONS = {:loop_count => 150, :key_count => LOW_KEY_COUNT}
32
+ SINGLE_KEY_COUNT_OPTIONS = {:loop_count => 100_000, :key_count => 1}
33
+
34
+ def test_concurrency
35
+ code = <<-RUBY_EVAL
36
+ cache[key]
37
+ cache[key] = key
38
+ cache[key]
39
+ cache.delete(key)
40
+ RUBY_EVAL
41
+ do_thread_loop(:concurrency, code)
42
+ end
43
+
44
+ def test_put_if_absent
45
+ do_thread_loop(:put_if_absent, 'acc += 1 unless cache.put_if_absent(key, key)', :key_count => 100_000) do |result, cache, options, keys|
46
+ assert_standard_accumulator_test_result(result, cache, options, keys)
47
+ end
48
+ end
49
+
50
+ def test_compute_if_absent
51
+ code = 'cache.compute_if_absent(key) { acc += 1; key }'
52
+ do_thread_loop(:compute_if_absent, code) do |result, cache, options, keys|
53
+ assert_standard_accumulator_test_result(result, cache, options, keys)
54
+ end
55
+ end
56
+
57
+ def test_compute_put_if_absent
58
+ code = <<-RUBY_EVAL
59
+ if key.even?
60
+ cache.compute_if_absent(key) { acc += 1; key }
61
+ else
62
+ acc += 1 unless cache.put_if_absent(key, key)
63
+ end
64
+ RUBY_EVAL
65
+ do_thread_loop(:compute_put_if_absent, code) do |result, cache, options, keys|
66
+ assert_standard_accumulator_test_result(result, cache, options, keys)
67
+ end
68
+ end
69
+
70
+ def test_compute_if_absent_and_present
71
+ compute_if_absent_and_present
72
+ compute_if_absent_and_present(LOW_KEY_COUNT_OPTIONS)
73
+ compute_if_absent_and_present(SINGLE_KEY_COUNT_OPTIONS)
74
+ end
75
+
76
+ def test_add_remove_to_zero
77
+ add_remove_to_zero
78
+ add_remove_to_zero(LOW_KEY_COUNT_OPTIONS)
79
+ add_remove_to_zero(SINGLE_KEY_COUNT_OPTIONS)
80
+ end
81
+
82
+ def test_add_remove_to_zero_via_merge_pair
83
+ add_remove_to_zero_via_merge_pair
84
+ add_remove_to_zero_via_merge_pair(LOW_KEY_COUNT_OPTIONS)
85
+ add_remove_to_zero_via_merge_pair(SINGLE_KEY_COUNT_OPTIONS)
86
+ end
87
+
88
+ def test_add_remove
89
+ add_remove
90
+ add_remove(LOW_KEY_COUNT_OPTIONS)
91
+ add_remove(SINGLE_KEY_COUNT_OPTIONS)
92
+ end
93
+
94
+ def test_add_remove_via_compute
95
+ add_remove_via_compute
96
+ add_remove_via_compute(LOW_KEY_COUNT_OPTIONS)
97
+ add_remove_via_compute(SINGLE_KEY_COUNT_OPTIONS)
98
+ end
99
+
100
+ def add_remove_via_compute_if_absent_present
101
+ add_remove_via_compute_if_absent_present
102
+ add_remove_via_compute_if_absent_present(LOW_KEY_COUNT_OPTIONS)
103
+ add_remove_via_compute_if_absent_present(SINGLE_KEY_COUNT_OPTIONS)
104
+ end
105
+
106
+ def test_add_remove_indiscriminate
107
+ add_remove_indiscriminate
108
+ add_remove_indiscriminate(LOW_KEY_COUNT_OPTIONS)
109
+ add_remove_indiscriminate(SINGLE_KEY_COUNT_OPTIONS)
110
+ end
111
+
112
+ def test_count_up
113
+ count_up
114
+ count_up(LOW_KEY_COUNT_OPTIONS)
115
+ count_up(SINGLE_KEY_COUNT_OPTIONS)
116
+ end
117
+
118
+ def test_count_up_via_compute
119
+ count_up_via_compute
120
+ count_up_via_compute(LOW_KEY_COUNT_OPTIONS)
121
+ count_up_via_compute(SINGLE_KEY_COUNT_OPTIONS)
122
+ end
123
+
124
+ def test_count_up_via_merge_pair
125
+ count_up_via_merge_pair
126
+ count_up_via_merge_pair(LOW_KEY_COUNT_OPTIONS)
127
+ count_up_via_merge_pair(SINGLE_KEY_COUNT_OPTIONS)
128
+ end
129
+
130
+ def test_count_race
131
+ prelude = 'change = (rand(2) == 1) ? 1 : -1'
132
+ code = <<-RUBY_EVAL
133
+ v = cache[key]
134
+ acc += change if cache.replace_pair(key, v, v + change)
135
+ RUBY_EVAL
136
+ do_thread_loop(:count_race, code, :loop_count => 5, :prelude => prelude, :cache_setup => ZERO_VALUE_CACHE_SETUP) do |result, cache, options, keys|
137
+ result_sum = sum(result)
138
+ assert_equal(sum(keys.map {|key| cache[key]}), result_sum)
139
+ assert_equal(sum(cache.values), result_sum)
140
+ assert_equal(options[:key_count], cache.size)
141
+ end
142
+ end
143
+
144
+ def test_get_and_set_new
145
+ code = 'acc += 1 unless cache.get_and_set(key, key)'
146
+ do_thread_loop(:get_and_set_new, code) do |result, cache, options, keys|
147
+ assert_standard_accumulator_test_result(result, cache, options, keys)
148
+ end
149
+ end
150
+
151
+ def test_get_and_set_existing
152
+ code = 'acc += 1 if cache.get_and_set(key, key) == -1'
153
+ do_thread_loop(:get_and_set_existing, code, :cache_setup => INITIAL_VALUE_CACHE_SETUP, :initial_value => -1) do |result, cache, options, keys|
154
+ assert_standard_accumulator_test_result(result, cache, options, keys)
155
+ end
156
+ end
157
+
158
+ private
159
+ def compute_if_absent_and_present(opts = {})
160
+ prelude = 'on_present = rand(2) == 1'
161
+ code = <<-RUBY_EVAL
162
+ if on_present
163
+ cache.compute_if_present(key) {|old_value| acc += 1; old_value + 1}
164
+ else
165
+ cache.compute_if_absent(key) { acc += 1; 1 }
166
+ end
167
+ RUBY_EVAL
168
+ do_thread_loop(:compute_if_absent_and_present, code, {:loop_count => 5, :prelude => prelude}.merge(opts)) do |result, cache, options, keys|
169
+ stored_sum = 0
170
+ stored_key_count = 0
171
+ keys.each do |k|
172
+ if value = cache[k]
173
+ stored_sum += value
174
+ stored_key_count += 1
175
+ end
176
+ end
177
+ assert_equal(stored_sum, sum(result))
178
+ assert_equal(stored_key_count, cache.size)
179
+ end
180
+ end
181
+
182
+ def add_remove(opts = {})
183
+ prelude = 'do_add = rand(2) == 1'
184
+ code = <<-RUBY_EVAL
185
+ if do_add
186
+ acc += 1 unless cache.put_if_absent(key, key)
187
+ else
188
+ acc -= 1 if cache.delete_pair(key, key)
189
+ end
190
+ RUBY_EVAL
191
+ do_thread_loop(:add_remove, code, {:loop_count => 5, :prelude => prelude}.merge(opts)) do |result, cache, options, keys|
192
+ assert_all_key_mappings_exist(cache, keys, false)
193
+ assert_equal(cache.size, sum(result))
194
+ end
195
+ end
196
+
197
+ def add_remove_via_compute(opts = {})
198
+ prelude = 'do_add = rand(2) == 1'
199
+ code = <<-RUBY_EVAL
200
+ cache.compute(key) do |old_value|
201
+ if do_add
202
+ acc += 1 unless old_value
203
+ key
204
+ else
205
+ acc -= 1 if old_value
206
+ nil
207
+ end
208
+ end
209
+ RUBY_EVAL
210
+ do_thread_loop(:add_remove_via_compute, code, {:loop_count => 5, :prelude => prelude}.merge(opts)) do |result, cache, options, keys|
211
+ assert_all_key_mappings_exist(cache, keys, false)
212
+ assert_equal(cache.size, sum(result))
213
+ end
214
+ end
215
+
216
+ def add_remove_via_compute_if_absent_present(opts = {})
217
+ prelude = 'do_add = rand(2) == 1'
218
+ code = <<-RUBY_EVAL
219
+ if do_add
220
+ cache.compute_if_absent(key) { acc += 1; key }
221
+ else
222
+ cache.compute_if_present(key) { acc -= 1; nil }
223
+ end
224
+ RUBY_EVAL
225
+ do_thread_loop(:add_remove_via_compute_if_absent_present, code, {:loop_count => 5, :prelude => prelude}.merge(opts)) do |result, cache, options, keys|
226
+ assert_all_key_mappings_exist(cache, keys, false)
227
+ assert_equal(cache.size, sum(result))
228
+ end
229
+ end
230
+
231
+ def add_remove_indiscriminate(opts = {})
232
+ prelude = 'do_add = rand(2) == 1'
233
+ code = <<-RUBY_EVAL
234
+ if do_add
235
+ acc += 1 unless cache.put_if_absent(key, key)
236
+ else
237
+ acc -= 1 if cache.delete(key)
238
+ end
239
+ RUBY_EVAL
240
+ do_thread_loop(:add_remove, code, {:loop_count => 5, :prelude => prelude}.merge(opts)) do |result, cache, options, keys|
241
+ assert_all_key_mappings_exist(cache, keys, false)
242
+ assert_equal(cache.size, sum(result))
243
+ end
244
+ end
245
+
246
+ def count_up(opts = {})
247
+ code = <<-RUBY_EVAL
248
+ v = cache[key]
249
+ acc += 1 if cache.replace_pair(key, v, v + 1)
250
+ RUBY_EVAL
251
+ do_thread_loop(:count_up, code, {:loop_count => 5, :cache_setup => ZERO_VALUE_CACHE_SETUP}.merge(opts)) do |result, cache, options, keys|
252
+ assert_count_up(result, cache, options, keys)
253
+ end
254
+ end
255
+
256
+ def count_up_via_compute(opts = {})
257
+ code = <<-RUBY_EVAL
258
+ cache.compute(key) do |old_value|
259
+ acc += 1
260
+ old_value ? old_value + 1 : 1
261
+ end
262
+ RUBY_EVAL
263
+ do_thread_loop(:count_up_via_compute, code, {:loop_count => 5}.merge(opts)) do |result, cache, options, keys|
264
+ assert_count_up(result, cache, options, keys)
265
+ result.inject(nil) do |previous_value, next_value| # since compute guarantees atomicity all count ups should be equal
266
+ assert_equal previous_value, next_value if previous_value
267
+ next_value
268
+ end
269
+ end
270
+ end
271
+
272
+ def count_up_via_merge_pair(opts = {})
273
+ code = <<-RUBY_EVAL
274
+ cache.merge_pair(key, 1) {|old_value| old_value + 1}
275
+ RUBY_EVAL
276
+ do_thread_loop(:count_up_via_merge_pair, code, {:loop_count => 5}.merge(opts)) do |result, cache, options, keys|
277
+ all_match = true
278
+ expected_value = options[:loop_count] * options[:thread_count]
279
+ keys.each do |key|
280
+ if expected_value != (value = cache[key])
281
+ all_match = false
282
+ break
283
+ end
284
+ end
285
+ assert all_match
286
+ end
287
+ end
288
+
289
+ def add_remove_to_zero(opts = {})
290
+ code = <<-RUBY_EVAL
291
+ acc += 1 unless cache.put_if_absent(key, key)
292
+ acc -= 1 if cache.delete_pair(key, key)
293
+ RUBY_EVAL
294
+ do_thread_loop(:add_remove_to_zero, code, {:loop_count => 5}.merge(opts)) do |result, cache, options, keys|
295
+ assert_all_key_mappings_exist(cache, keys, false)
296
+ assert_equal(cache.size, sum(result))
297
+ end
298
+ end
299
+
300
+ def add_remove_to_zero_via_merge_pair(opts = {})
301
+ code = <<-RUBY_EVAL
302
+ acc += (cache.merge_pair(key, key) {}) ? -1 : 1
303
+ RUBY_EVAL
304
+ do_thread_loop(:add_remove_to_zero_via_merge_pair, code, {:loop_count => 5}.merge(opts)) do |result, cache, options, keys|
305
+ assert_all_key_mappings_exist(cache, keys, false)
306
+ assert_equal(cache.size, sum(result))
307
+ end
308
+ end
309
+
310
+ def do_thread_loop(name, code, options = {}, &block)
311
+ options = DEFAULTS.merge(options)
312
+ meth = define_loop name, code, options[:prelude]
313
+ assert_nothing_raised do
314
+ keys = to_keys_array(options[:key_count])
315
+ run_thread_loop(meth, keys, options, &block)
316
+
317
+ if options[:key_count] > 1
318
+ options[:key_count] = (options[:key_count] / 40).to_i
319
+ keys = to_hash_collision_keys_array(options[:key_count])
320
+ run_thread_loop(meth, keys, options.merge(:loop_count => (options[:loop_count] * 5)), &block)
321
+ end
322
+ end
323
+ end
324
+
325
+ def run_thread_loop(meth, keys, options)
326
+ cache = options[:cache_setup].call(options, keys)
327
+ barrier = ThreadSafe::Test::Barrier.new(options[:thread_count])
328
+ t = Time.now
329
+ result = (1..options[:thread_count]).map do
330
+ Thread.new do
331
+ setup_sync_and_start_loop(meth, cache, keys, barrier, options[:loop_count])
332
+ end
333
+ end.map(&:value)
334
+ yield result, cache, options, keys if block_given?
335
+ end
336
+
337
+ def setup_sync_and_start_loop(meth, cache, keys, barrier, loop_count)
338
+ my_keys = keys.shuffle
339
+ barrier.await
340
+ if my_keys.size == 1
341
+ key = my_keys.first
342
+ send("#{meth}_single_key", cache, key, loop_count)
343
+ else
344
+ send("#{meth}_multiple_keys", cache, my_keys, loop_count)
345
+ end
346
+ end
347
+
348
+ def define_loop(name, body, prelude)
349
+ inner_meth_name = :"_#{name}_loop_inner"
350
+ outer_meth_name = :"_#{name}_loop_outer"
351
+ # looping is splitted into the "loop methods" to trigger the JIT
352
+ self.class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
353
+ def #{inner_meth_name}_multiple_keys(cache, keys, i, length, acc)
354
+ #{prelude}
355
+ target = i + length
356
+ while i < target
357
+ key = keys[i]
358
+ #{body}
359
+ i += 1
360
+ end
361
+ acc
362
+ end unless method_defined?(:#{inner_meth_name}_multiple_keys)
363
+ RUBY_EVAL
364
+
365
+ self.class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
366
+ def #{inner_meth_name}_single_key(cache, key, i, length, acc)
367
+ #{prelude}
368
+ target = i + length
369
+ while i < target
370
+ #{body}
371
+ i += 1
372
+ end
373
+ acc
374
+ end unless method_defined?(:#{inner_meth_name}_single_key)
375
+ RUBY_EVAL
376
+
377
+ self.class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
378
+ def #{outer_meth_name}_multiple_keys(cache, keys, loop_count)
379
+ total_length = keys.size
380
+ acc = 0
381
+ inc = 100
382
+ loop_count.times do
383
+ i = 0
384
+ pre_loop_inc = total_length % inc
385
+ acc = #{inner_meth_name}_multiple_keys(cache, keys, i, pre_loop_inc, acc)
386
+ i += pre_loop_inc
387
+ while i < total_length
388
+ acc = #{inner_meth_name}_multiple_keys(cache, keys, i, inc, acc)
389
+ i += inc
390
+ end
391
+ end
392
+ acc
393
+ end unless method_defined?(:#{outer_meth_name}_multiple_keys)
394
+ RUBY_EVAL
395
+
396
+ self.class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
397
+ def #{outer_meth_name}_single_key(cache, key, loop_count)
398
+ acc = 0
399
+ i = 0
400
+ inc = 100
401
+
402
+ pre_loop_inc = loop_count % inc
403
+ acc = #{inner_meth_name}_single_key(cache, key, i, pre_loop_inc, acc)
404
+ i += pre_loop_inc
405
+
406
+ while i < loop_count
407
+ acc = #{inner_meth_name}_single_key(cache, key, i, inc, acc)
408
+ i += inc
409
+ end
410
+ acc
411
+ end unless method_defined?(:#{outer_meth_name}_single_key)
412
+ RUBY_EVAL
413
+ outer_meth_name
414
+ end
415
+
416
+ def to_keys_array(key_count)
417
+ arr = []
418
+ key_count.times {|i| arr << i}
419
+ arr
420
+ end
421
+
422
+ def to_hash_collision_keys_array(key_count)
423
+ to_keys_array(key_count).map {|key| ThreadSafe::Test::HashCollisionKey(key)}
424
+ end
425
+
426
+ def sum(result)
427
+ result.inject(0) {|acc, i| acc + i}
428
+ end
429
+
430
+ def assert_standard_accumulator_test_result(result, cache, options, keys)
431
+ assert_all_key_mappings_exist(cache, keys)
432
+ assert_equal(options[:key_count], sum(result))
433
+ assert_equal(options[:key_count], cache.size)
434
+ end
435
+
436
+ def assert_all_key_mappings_exist(cache, keys, all_must_exist = true)
437
+ keys.each do |key|
438
+ if (value = cache[key]) || all_must_exist
439
+ assert_equal key, value unless key == value # don't do a bazzilion assertions unless necessary
440
+ end
441
+ end
442
+ end
443
+
444
+ def assert_count_up(result, cache, options, keys)
445
+ keys.each do |key|
446
+ unless value = cache[key]
447
+ assert value
448
+ end
449
+ end
450
+ assert_equal(sum(cache.values), sum(result))
451
+ assert_equal(options[:key_count], cache.size)
452
+ end
453
+ end