thread_safe 0.1.3 → 0.2.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 +4 -4
- data/.travis.yml +24 -0
- data/README.md +19 -5
- data/Rakefile +13 -6
- data/examples/bench_cache.rb +1 -1
- data/ext/org/jruby/ext/thread_safe/JRubyCacheBackendLibrary.java +54 -15
- data/ext/org/jruby/ext/thread_safe/jsr166e/ConcurrentHashMap.java +28 -0
- data/ext/org/jruby/ext/thread_safe/jsr166e/ConcurrentHashMapV8.java +19 -10
- data/ext/org/jruby/ext/thread_safe/jsr166e/LongAdder.java +1 -2
- data/ext/org/jruby/ext/thread_safe/jsr166e/Striped64.java +1 -1
- data/ext/org/jruby/ext/thread_safe/jsr166e/nounsafe/ConcurrentHashMapV8.java +3788 -0
- data/ext/org/jruby/ext/thread_safe/jsr166e/nounsafe/LongAdder.java +204 -0
- data/ext/org/jruby/ext/thread_safe/jsr166e/nounsafe/Striped64.java +291 -0
- data/ext/org/jruby/ext/thread_safe/jsr166y/ThreadLocalRandom.java +1 -1
- data/ext/thread_safe/JrubyCacheBackendService.java +2 -2
- data/lib/thread_safe.rb +1 -1
- data/lib/thread_safe/atomic_reference_cache_backend.rb +1 -1
- data/lib/thread_safe/cache.rb +6 -3
- data/lib/thread_safe/mri_cache_backend.rb +2 -2
- data/lib/thread_safe/non_concurrent_cache_backend.rb +1 -1
- data/lib/thread_safe/synchronized_cache_backend.rb +1 -1
- data/lib/thread_safe/synchronized_delegator.rb +36 -19
- data/lib/thread_safe/util.rb +1 -1
- data/lib/thread_safe/util/adder.rb +1 -1
- data/lib/thread_safe/util/atomic_reference.rb +1 -1
- data/lib/thread_safe/util/cheap_lockable.rb +1 -1
- data/lib/thread_safe/util/power_of_two_tuple.rb +1 -1
- data/lib/thread_safe/util/striped64.rb +1 -1
- data/lib/thread_safe/util/volatile.rb +1 -1
- data/lib/thread_safe/util/volatile_tuple.rb +1 -1
- data/lib/thread_safe/util/xor_shift_random.rb +1 -1
- data/lib/thread_safe/version.rb +1 -1
- data/test/src/thread_safe/SecurityManager.java +21 -0
- data/test/test_array.rb +1 -1
- data/test/test_cache.rb +27 -10
- data/test/test_cache_loops.rb +377 -376
- data/test/test_hash.rb +1 -2
- data/test/test_helper.rb +33 -3
- data/test/test_synchronized_delegator.rb +67 -17
- data/thread_safe.gemspec +6 -3
- metadata +36 -10
@@ -1,7 +1,7 @@
|
|
1
1
|
package thread_safe;
|
2
2
|
|
3
3
|
import java.io.IOException;
|
4
|
-
|
4
|
+
|
5
5
|
import org.jruby.Ruby;
|
6
6
|
import org.jruby.ext.thread_safe.JRubyCacheBackendLibrary;
|
7
7
|
import org.jruby.runtime.load.BasicLibraryService;
|
@@ -12,4 +12,4 @@ public class JrubyCacheBackendService implements BasicLibraryService {
|
|
12
12
|
new JRubyCacheBackendLibrary().load(runtime, false);
|
13
13
|
return true;
|
14
14
|
}
|
15
|
-
}
|
15
|
+
}
|
data/lib/thread_safe.rb
CHANGED
@@ -28,7 +28,7 @@ module ThreadSafe
|
|
28
28
|
class Hash < ::Hash
|
29
29
|
include JRuby::Synchronized
|
30
30
|
end
|
31
|
-
elsif defined?(RUBY_ENGINE)
|
31
|
+
elsif !defined?(RUBY_ENGINE) || RUBY_ENGINE == 'ruby'
|
32
32
|
# Because MRI never runs code in parallel, the existing
|
33
33
|
# non-thread-safe structures should usually work fine.
|
34
34
|
Array = ::Array
|
data/lib/thread_safe/cache.rb
CHANGED
@@ -7,8 +7,8 @@ module ThreadSafe
|
|
7
7
|
autoload :AtomicReferenceCacheBackend, 'thread_safe/atomic_reference_cache_backend'
|
8
8
|
autoload :SynchronizedCacheBackend, 'thread_safe/synchronized_cache_backend'
|
9
9
|
|
10
|
-
ConcurrentCacheBackend =
|
11
|
-
case
|
10
|
+
ConcurrentCacheBackend = if defined?(RUBY_ENGINE)
|
11
|
+
case RUBY_ENGINE
|
12
12
|
when 'jruby'; JRubyCacheBackend
|
13
13
|
when 'ruby'; MriCacheBackend
|
14
14
|
when 'rbx'; AtomicReferenceCacheBackend
|
@@ -16,6 +16,9 @@ module ThreadSafe
|
|
16
16
|
warn 'ThreadSafe: unsupported Ruby engine, using a fully synchronized ThreadSafe::Cache implementation' if $VERBOSE
|
17
17
|
SynchronizedCacheBackend
|
18
18
|
end
|
19
|
+
else
|
20
|
+
MriCacheBackend
|
21
|
+
end
|
19
22
|
|
20
23
|
class Cache < ConcurrentCacheBackend
|
21
24
|
KEY_ERROR = defined?(KeyError) ? KeyError : IndexError # there is no KeyError in 1.8 mode
|
@@ -137,4 +140,4 @@ module ThreadSafe
|
|
137
140
|
end
|
138
141
|
end
|
139
142
|
end
|
140
|
-
end
|
143
|
+
end
|
@@ -4,7 +4,7 @@ module ThreadSafe
|
|
4
4
|
#
|
5
5
|
# The previous implementation used `Thread.critical` on 1.8 MRI to implement the 4 composed atomic operations (`put_if_absent`, `replace_pair`,
|
6
6
|
# `replace_if_exists`, `delete_pair`) this however doesn't work for `compute_if_absent` because on 1.8 the Mutex class is itself implemented
|
7
|
-
# via `Thread.critical` and a call to `Mutex#lock` does not restore the previous `Thread.critical` value (thus any synchronisation clears the
|
7
|
+
# via `Thread.critical` and a call to `Mutex#lock` does not restore the previous `Thread.critical` value (thus any synchronisation clears the
|
8
8
|
# `Thread.critical` flag and we loose control). This poses a problem as the provided block might use synchronisation on its own.
|
9
9
|
#
|
10
10
|
# NOTE: a neat idea of writing a c-ext to manually perform atomic put_if_absent, while relying on Ruby not releasing a GVL while calling
|
@@ -59,4 +59,4 @@ module ThreadSafe
|
|
59
59
|
WRITE_LOCK.synchronize { super }
|
60
60
|
end
|
61
61
|
end
|
62
|
-
end
|
62
|
+
end
|
@@ -1,35 +1,52 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'monitor'
|
3
|
+
|
1
4
|
# This class provides a trivial way to synchronize all calls to a given object
|
2
|
-
# by wrapping it with a Delegator that performs
|
3
|
-
# the delegated
|
5
|
+
# by wrapping it with a `Delegator` that performs `Monitor#enter/exit` calls
|
6
|
+
# around the delegated `#send`. Example:
|
4
7
|
#
|
5
8
|
# array = [] # not thread-safe on many impls
|
6
|
-
# array =
|
9
|
+
# array = SynchronizedDelegator.new([]) # thread-safe
|
7
10
|
#
|
8
|
-
# A simple
|
11
|
+
# A simple `Monitor` provides a very coarse-grained way to synchronize a given
|
9
12
|
# object, in that it will cause synchronization for methods that have no
|
10
13
|
# need for it, but this is a trivial way to get thread-safety where none may
|
11
14
|
# exist currently on some implementations.
|
12
15
|
#
|
13
16
|
# This class is currently being considered for inclusion into stdlib, via
|
14
17
|
# https://bugs.ruby-lang.org/issues/8556
|
18
|
+
class SynchronizedDelegator < SimpleDelegator
|
15
19
|
|
16
|
-
|
20
|
+
def initialize(obj)
|
21
|
+
__setobj__(obj)
|
22
|
+
@monitor = Monitor.new
|
23
|
+
end
|
17
24
|
|
18
|
-
|
19
|
-
|
20
|
-
|
25
|
+
def method_missing(method, *args, &block)
|
26
|
+
monitor = @monitor
|
27
|
+
begin
|
28
|
+
monitor.enter
|
21
29
|
super
|
22
|
-
|
30
|
+
ensure
|
31
|
+
monitor.exit
|
23
32
|
end
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
33
|
+
end
|
34
|
+
|
35
|
+
# Work-around for 1.8 std-lib not passing block around to delegate.
|
36
|
+
# @private
|
37
|
+
def method_missing(method, *args, &block)
|
38
|
+
monitor = @monitor
|
39
|
+
begin
|
40
|
+
monitor.enter
|
41
|
+
target = self.__getobj__
|
42
|
+
if target.respond_to?(method)
|
43
|
+
target.__send__(method, *args, &block)
|
44
|
+
else
|
45
|
+
super(method, *args, &block)
|
32
46
|
end
|
47
|
+
ensure
|
48
|
+
monitor.exit
|
33
49
|
end
|
34
|
-
end
|
35
|
-
|
50
|
+
end if RUBY_VERSION[0, 3] == '1.8'
|
51
|
+
|
52
|
+
end unless defined?(SynchronizedDelegator)
|
data/lib/thread_safe/util.rb
CHANGED
data/lib/thread_safe/version.rb
CHANGED
@@ -0,0 +1,21 @@
|
|
1
|
+
package thread_safe;
|
2
|
+
|
3
|
+
import java.security.Permission;
|
4
|
+
import java.util.ArrayList;
|
5
|
+
import java.util.List;
|
6
|
+
|
7
|
+
public class SecurityManager extends java.lang.SecurityManager {
|
8
|
+
private final List<Permission> deniedPermissions =
|
9
|
+
new ArrayList<Permission>();
|
10
|
+
|
11
|
+
@Override
|
12
|
+
public void checkPermission(Permission p) {
|
13
|
+
if (deniedPermissions.contains(p)) {
|
14
|
+
throw new SecurityException("Denied!");
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
public void deny(Permission p) {
|
19
|
+
deniedPermissions.add(p);
|
20
|
+
}
|
21
|
+
}
|
data/test/test_array.rb
CHANGED
data/test/test_cache.rb
CHANGED
@@ -290,17 +290,21 @@ class TestCache < Test::Unit::TestCase
|
|
290
290
|
end
|
291
291
|
|
292
292
|
def test_collision_resistance
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
i += 10
|
293
|
+
assert_collision_resistance((0..1000).map {|i| ThreadSafe::Test::HashCollisionKey(i, 1)})
|
294
|
+
end
|
295
|
+
|
296
|
+
def test_collision_resistance_with_arrays
|
297
|
+
special_array_class = Class.new(Array) do
|
298
|
+
def key # assert_collision_resistance expects to be able to call .key to get the "real" key
|
299
|
+
first.key
|
301
300
|
end
|
302
301
|
end
|
303
|
-
|
302
|
+
# Test collision resistance with a keys that say they responds_to <=>, but then raise exceptions
|
303
|
+
# when actually called (ie: an Array filled with non-comparable keys).
|
304
|
+
# See https://github.com/headius/thread_safe/issues/19 for more info.
|
305
|
+
assert_collision_resistance((0..100).map do |i|
|
306
|
+
special_array_class.new([ThreadSafe::Test::HashCollisionKeyNonComparable.new(i, 1)])
|
307
|
+
end)
|
304
308
|
end
|
305
309
|
|
306
310
|
def test_replace_pair
|
@@ -791,4 +795,17 @@ class TestCache < Test::Unit::TestCase
|
|
791
795
|
end
|
792
796
|
assert_equal expected_result, result
|
793
797
|
end
|
794
|
-
|
798
|
+
|
799
|
+
def assert_collision_resistance(keys)
|
800
|
+
keys.each {|k| @cache[k] = k.key}
|
801
|
+
10.times do |i|
|
802
|
+
size = keys.size
|
803
|
+
while i < size
|
804
|
+
k = keys[i]
|
805
|
+
assert(k.key == @cache.delete(k) && !@cache.key?(k) && (@cache[k] = k.key; @cache[k] == k.key))
|
806
|
+
i += 10
|
807
|
+
end
|
808
|
+
end
|
809
|
+
assert(keys.all? {|k| @cache[k] == k.key})
|
810
|
+
end
|
811
|
+
end
|
data/test/test_cache_loops.rb
CHANGED
@@ -5,449 +5,450 @@ require File.join(File.dirname(__FILE__), "test_helper")
|
|
5
5
|
|
6
6
|
Thread.abort_on_exception = true
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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)
|
8
|
+
unless RUBY_VERSION =~ /1\.8/ || ENV['TRAVIS']
|
9
|
+
class TestCacheTorture < Test::Unit::TestCase
|
10
|
+
THREAD_COUNT = 40
|
11
|
+
KEY_COUNT = (((2**13) - 2) * 0.75).to_i # get close to the doubling cliff
|
12
|
+
LOW_KEY_COUNT = (((2**8 ) - 2) * 0.75).to_i # get close to the doubling cliff
|
13
|
+
|
14
|
+
INITIAL_VALUE_CACHE_SETUP = lambda do |options, keys|
|
15
|
+
cache = ThreadSafe::Cache.new
|
16
|
+
initial_value = options[:initial_value] || 0
|
17
|
+
keys.each {|key| cache[key] = initial_value}
|
18
|
+
cache
|
19
|
+
end
|
20
|
+
ZERO_VALUE_CACHE_SETUP = lambda do |options, keys|
|
21
|
+
INITIAL_VALUE_CACHE_SETUP.call(options.merge(:initial_value => 0), keys)
|
47
22
|
end
|
48
|
-
end
|
49
23
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
24
|
+
DEFAULTS = {
|
25
|
+
:key_count => KEY_COUNT,
|
26
|
+
:thread_count => THREAD_COUNT,
|
27
|
+
:loop_count => 1,
|
28
|
+
:prelude => '',
|
29
|
+
:cache_setup => lambda {|options, keys| ThreadSafe::Cache.new}
|
30
|
+
}
|
31
|
+
|
32
|
+
LOW_KEY_COUNT_OPTIONS = {:loop_count => 150, :key_count => LOW_KEY_COUNT}
|
33
|
+
SINGLE_KEY_COUNT_OPTIONS = {:loop_count => 100_000, :key_count => 1}
|
34
|
+
|
35
|
+
def test_concurrency
|
36
|
+
code = <<-RUBY_EVAL
|
37
|
+
cache[key]
|
38
|
+
cache[key] = key
|
39
|
+
cache[key]
|
40
|
+
cache.delete(key)
|
41
|
+
RUBY_EVAL
|
42
|
+
do_thread_loop(:concurrency, code)
|
54
43
|
end
|
55
|
-
end
|
56
44
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
cache.compute_if_absent(key) { acc += 1; key }
|
61
|
-
else
|
62
|
-
acc += 1 unless cache.put_if_absent(key, key)
|
45
|
+
def test_put_if_absent
|
46
|
+
do_thread_loop(:put_if_absent, 'acc += 1 unless cache.put_if_absent(key, key)', :key_count => 100_000) do |result, cache, options, keys|
|
47
|
+
assert_standard_accumulator_test_result(result, cache, options, keys)
|
63
48
|
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
49
|
end
|
68
|
-
end
|
69
50
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
51
|
+
def test_compute_if_absent
|
52
|
+
code = 'cache.compute_if_absent(key) { acc += 1; key }'
|
53
|
+
do_thread_loop(:compute_if_absent, code) do |result, cache, options, keys|
|
54
|
+
assert_standard_accumulator_test_result(result, cache, options, keys)
|
55
|
+
end
|
56
|
+
end
|
81
57
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
58
|
+
def test_compute_put_if_absent
|
59
|
+
code = <<-RUBY_EVAL
|
60
|
+
if key.even?
|
61
|
+
cache.compute_if_absent(key) { acc += 1; key }
|
62
|
+
else
|
63
|
+
acc += 1 unless cache.put_if_absent(key, key)
|
64
|
+
end
|
65
|
+
RUBY_EVAL
|
66
|
+
do_thread_loop(:compute_put_if_absent, code) do |result, cache, options, keys|
|
67
|
+
assert_standard_accumulator_test_result(result, cache, options, keys)
|
68
|
+
end
|
69
|
+
end
|
87
70
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
71
|
+
def test_compute_if_absent_and_present
|
72
|
+
compute_if_absent_and_present
|
73
|
+
compute_if_absent_and_present(LOW_KEY_COUNT_OPTIONS)
|
74
|
+
compute_if_absent_and_present(SINGLE_KEY_COUNT_OPTIONS)
|
75
|
+
end
|
93
76
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
77
|
+
def test_add_remove_to_zero
|
78
|
+
add_remove_to_zero
|
79
|
+
add_remove_to_zero(LOW_KEY_COUNT_OPTIONS)
|
80
|
+
add_remove_to_zero(SINGLE_KEY_COUNT_OPTIONS)
|
81
|
+
end
|
99
82
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
83
|
+
def test_add_remove_to_zero_via_merge_pair
|
84
|
+
add_remove_to_zero_via_merge_pair
|
85
|
+
add_remove_to_zero_via_merge_pair(LOW_KEY_COUNT_OPTIONS)
|
86
|
+
add_remove_to_zero_via_merge_pair(SINGLE_KEY_COUNT_OPTIONS)
|
87
|
+
end
|
105
88
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
89
|
+
def test_add_remove
|
90
|
+
add_remove
|
91
|
+
add_remove(LOW_KEY_COUNT_OPTIONS)
|
92
|
+
add_remove(SINGLE_KEY_COUNT_OPTIONS)
|
93
|
+
end
|
111
94
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
95
|
+
def test_add_remove_via_compute
|
96
|
+
add_remove_via_compute
|
97
|
+
add_remove_via_compute(LOW_KEY_COUNT_OPTIONS)
|
98
|
+
add_remove_via_compute(SINGLE_KEY_COUNT_OPTIONS)
|
99
|
+
end
|
117
100
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
101
|
+
def add_remove_via_compute_if_absent_present
|
102
|
+
add_remove_via_compute_if_absent_present
|
103
|
+
add_remove_via_compute_if_absent_present(LOW_KEY_COUNT_OPTIONS)
|
104
|
+
add_remove_via_compute_if_absent_present(SINGLE_KEY_COUNT_OPTIONS)
|
105
|
+
end
|
123
106
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
107
|
+
def test_add_remove_indiscriminate
|
108
|
+
add_remove_indiscriminate
|
109
|
+
add_remove_indiscriminate(LOW_KEY_COUNT_OPTIONS)
|
110
|
+
add_remove_indiscriminate(SINGLE_KEY_COUNT_OPTIONS)
|
111
|
+
end
|
129
112
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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)
|
113
|
+
def test_count_up
|
114
|
+
count_up
|
115
|
+
count_up(LOW_KEY_COUNT_OPTIONS)
|
116
|
+
count_up(SINGLE_KEY_COUNT_OPTIONS)
|
141
117
|
end
|
142
|
-
end
|
143
118
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
119
|
+
def test_count_up_via_compute
|
120
|
+
count_up_via_compute
|
121
|
+
count_up_via_compute(LOW_KEY_COUNT_OPTIONS)
|
122
|
+
count_up_via_compute(SINGLE_KEY_COUNT_OPTIONS)
|
148
123
|
end
|
149
|
-
end
|
150
124
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
125
|
+
def test_count_up_via_merge_pair
|
126
|
+
count_up_via_merge_pair
|
127
|
+
count_up_via_merge_pair(LOW_KEY_COUNT_OPTIONS)
|
128
|
+
count_up_via_merge_pair(SINGLE_KEY_COUNT_OPTIONS)
|
155
129
|
end
|
156
|
-
end
|
157
130
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
131
|
+
def test_count_race
|
132
|
+
prelude = 'change = (rand(2) == 1) ? 1 : -1'
|
133
|
+
code = <<-RUBY_EVAL
|
134
|
+
v = cache[key]
|
135
|
+
acc += change if cache.replace_pair(key, v, v + change)
|
136
|
+
RUBY_EVAL
|
137
|
+
do_thread_loop(:count_race, code, :loop_count => 5, :prelude => prelude, :cache_setup => ZERO_VALUE_CACHE_SETUP) do |result, cache, options, keys|
|
138
|
+
result_sum = sum(result)
|
139
|
+
assert_equal(sum(keys.map {|key| cache[key]}), result_sum)
|
140
|
+
assert_equal(sum(cache.values), result_sum)
|
141
|
+
assert_equal(options[:key_count], cache.size)
|
166
142
|
end
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
stored_sum += value
|
174
|
-
stored_key_count += 1
|
175
|
-
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def test_get_and_set_new
|
146
|
+
code = 'acc += 1 unless cache.get_and_set(key, key)'
|
147
|
+
do_thread_loop(:get_and_set_new, code) do |result, cache, options, keys|
|
148
|
+
assert_standard_accumulator_test_result(result, cache, options, keys)
|
176
149
|
end
|
177
|
-
assert_equal(stored_sum, sum(result))
|
178
|
-
assert_equal(stored_key_count, cache.size)
|
179
150
|
end
|
180
|
-
end
|
181
151
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
acc += 1 unless cache.put_if_absent(key, key)
|
187
|
-
else
|
188
|
-
acc -= 1 if cache.delete_pair(key, key)
|
152
|
+
def test_get_and_set_existing
|
153
|
+
code = 'acc += 1 if cache.get_and_set(key, key) == -1'
|
154
|
+
do_thread_loop(:get_and_set_existing, code, :cache_setup => INITIAL_VALUE_CACHE_SETUP, :initial_value => -1) do |result, cache, options, keys|
|
155
|
+
assert_standard_accumulator_test_result(result, cache, options, keys)
|
189
156
|
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
157
|
end
|
195
|
-
end
|
196
158
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
159
|
+
private
|
160
|
+
def compute_if_absent_and_present(opts = {})
|
161
|
+
prelude = 'on_present = rand(2) == 1'
|
162
|
+
code = <<-RUBY_EVAL
|
163
|
+
if on_present
|
164
|
+
cache.compute_if_present(key) {|old_value| acc += 1; old_value + 1}
|
165
|
+
else
|
166
|
+
cache.compute_if_absent(key) { acc += 1; 1 }
|
167
|
+
end
|
168
|
+
RUBY_EVAL
|
169
|
+
do_thread_loop(__method__, code, {:loop_count => 5, :prelude => prelude}.merge(opts)) do |result, cache, options, keys|
|
170
|
+
stored_sum = 0
|
171
|
+
stored_key_count = 0
|
172
|
+
keys.each do |k|
|
173
|
+
if value = cache[k]
|
174
|
+
stored_sum += value
|
175
|
+
stored_key_count += 1
|
176
|
+
end
|
177
|
+
end
|
178
|
+
assert_equal(stored_sum, sum(result))
|
179
|
+
assert_equal(stored_key_count, cache.size)
|
208
180
|
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
181
|
end
|
214
|
-
end
|
215
182
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
183
|
+
def add_remove(opts = {})
|
184
|
+
prelude = 'do_add = rand(2) == 1'
|
185
|
+
code = <<-RUBY_EVAL
|
186
|
+
if do_add
|
187
|
+
acc += 1 unless cache.put_if_absent(key, key)
|
188
|
+
else
|
189
|
+
acc -= 1 if cache.delete_pair(key, key)
|
190
|
+
end
|
191
|
+
RUBY_EVAL
|
192
|
+
do_thread_loop(__method__, code, {:loop_count => 5, :prelude => prelude}.merge(opts)) do |result, cache, options, keys|
|
193
|
+
assert_all_key_mappings_exist(cache, keys, false)
|
194
|
+
assert_equal(cache.size, sum(result))
|
223
195
|
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
196
|
end
|
229
|
-
end
|
230
197
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
198
|
+
def add_remove_via_compute(opts = {})
|
199
|
+
prelude = 'do_add = rand(2) == 1'
|
200
|
+
code = <<-RUBY_EVAL
|
201
|
+
cache.compute(key) do |old_value|
|
202
|
+
if do_add
|
203
|
+
acc += 1 unless old_value
|
204
|
+
key
|
205
|
+
else
|
206
|
+
acc -= 1 if old_value
|
207
|
+
nil
|
208
|
+
end
|
209
|
+
end
|
210
|
+
RUBY_EVAL
|
211
|
+
do_thread_loop(__method__, code, {:loop_count => 5, :prelude => prelude}.merge(opts)) do |result, cache, options, keys|
|
212
|
+
assert_all_key_mappings_exist(cache, keys, false)
|
213
|
+
assert_equal(cache.size, sum(result))
|
238
214
|
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
215
|
end
|
244
|
-
end
|
245
216
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
217
|
+
def add_remove_via_compute_if_absent_present(opts = {})
|
218
|
+
prelude = 'do_add = rand(2) == 1'
|
219
|
+
code = <<-RUBY_EVAL
|
220
|
+
if do_add
|
221
|
+
cache.compute_if_absent(key) { acc += 1; key }
|
222
|
+
else
|
223
|
+
cache.compute_if_present(key) { acc -= 1; nil }
|
224
|
+
end
|
225
|
+
RUBY_EVAL
|
226
|
+
do_thread_loop(__method__, code, {:loop_count => 5, :prelude => prelude}.merge(opts)) do |result, cache, options, keys|
|
227
|
+
assert_all_key_mappings_exist(cache, keys, false)
|
228
|
+
assert_equal(cache.size, sum(result))
|
229
|
+
end
|
253
230
|
end
|
254
|
-
end
|
255
231
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
232
|
+
def add_remove_indiscriminate(opts = {})
|
233
|
+
prelude = 'do_add = rand(2) == 1'
|
234
|
+
code = <<-RUBY_EVAL
|
235
|
+
if do_add
|
236
|
+
acc += 1 unless cache.put_if_absent(key, key)
|
237
|
+
else
|
238
|
+
acc -= 1 if cache.delete(key)
|
239
|
+
end
|
240
|
+
RUBY_EVAL
|
241
|
+
do_thread_loop(__method__, code, {:loop_count => 5, :prelude => prelude}.merge(opts)) do |result, cache, options, keys|
|
242
|
+
assert_all_key_mappings_exist(cache, keys, false)
|
243
|
+
assert_equal(cache.size, sum(result))
|
261
244
|
end
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
245
|
+
end
|
246
|
+
|
247
|
+
def count_up(opts = {})
|
248
|
+
code = <<-RUBY_EVAL
|
249
|
+
v = cache[key]
|
250
|
+
acc += 1 if cache.replace_pair(key, v, v + 1)
|
251
|
+
RUBY_EVAL
|
252
|
+
do_thread_loop(__method__, code, {:loop_count => 5, :cache_setup => ZERO_VALUE_CACHE_SETUP}.merge(opts)) do |result, cache, options, keys|
|
253
|
+
assert_count_up(result, cache, options, keys)
|
268
254
|
end
|
269
255
|
end
|
270
|
-
end
|
271
256
|
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
257
|
+
def count_up_via_compute(opts = {})
|
258
|
+
code = <<-RUBY_EVAL
|
259
|
+
cache.compute(key) do |old_value|
|
260
|
+
acc += 1
|
261
|
+
old_value ? old_value + 1 : 1
|
262
|
+
end
|
263
|
+
RUBY_EVAL
|
264
|
+
do_thread_loop(:count_up_via_compute, code, {:loop_count => 5}.merge(opts)) do |result, cache, options, keys|
|
265
|
+
assert_count_up(result, cache, options, keys)
|
266
|
+
result.inject(nil) do |previous_value, next_value| # since compute guarantees atomicity all count ups should be equal
|
267
|
+
assert_equal previous_value, next_value if previous_value
|
268
|
+
next_value
|
269
|
+
end
|
284
270
|
end
|
285
|
-
assert all_match
|
286
271
|
end
|
287
|
-
end
|
288
272
|
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
273
|
+
def count_up_via_merge_pair(opts = {})
|
274
|
+
code = <<-RUBY_EVAL
|
275
|
+
cache.merge_pair(key, 1) {|old_value| old_value + 1}
|
276
|
+
RUBY_EVAL
|
277
|
+
do_thread_loop(:count_up_via_merge_pair, code, {:loop_count => 5}.merge(opts)) do |result, cache, options, keys|
|
278
|
+
all_match = true
|
279
|
+
expected_value = options[:loop_count] * options[:thread_count]
|
280
|
+
keys.each do |key|
|
281
|
+
if expected_value != (value = cache[key])
|
282
|
+
all_match = false
|
283
|
+
break
|
284
|
+
end
|
285
|
+
end
|
286
|
+
assert all_match
|
287
|
+
end
|
297
288
|
end
|
298
|
-
end
|
299
289
|
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
290
|
+
def add_remove_to_zero(opts = {})
|
291
|
+
code = <<-RUBY_EVAL
|
292
|
+
acc += 1 unless cache.put_if_absent(key, key)
|
293
|
+
acc -= 1 if cache.delete_pair(key, key)
|
294
|
+
RUBY_EVAL
|
295
|
+
do_thread_loop(:add_remove_to_zero, code, {:loop_count => 5}.merge(opts)) do |result, cache, options, keys|
|
296
|
+
assert_all_key_mappings_exist(cache, keys, false)
|
297
|
+
assert_equal(cache.size, sum(result))
|
298
|
+
end
|
307
299
|
end
|
308
|
-
end
|
309
300
|
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
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)
|
301
|
+
def add_remove_to_zero_via_merge_pair(opts = {})
|
302
|
+
code = <<-RUBY_EVAL
|
303
|
+
acc += (cache.merge_pair(key, key) {}) ? -1 : 1
|
304
|
+
RUBY_EVAL
|
305
|
+
do_thread_loop(:add_remove_to_zero_via_merge_pair, code, {:loop_count => 5}.merge(opts)) do |result, cache, options, keys|
|
306
|
+
assert_all_key_mappings_exist(cache, keys, false)
|
307
|
+
assert_equal(cache.size, sum(result))
|
321
308
|
end
|
322
309
|
end
|
323
|
-
end
|
324
310
|
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
311
|
+
def do_thread_loop(name, code, options = {}, &block)
|
312
|
+
options = DEFAULTS.merge(options)
|
313
|
+
meth = define_loop name, code, options[:prelude]
|
314
|
+
assert_nothing_raised do
|
315
|
+
keys = to_keys_array(options[:key_count])
|
316
|
+
run_thread_loop(meth, keys, options, &block)
|
317
|
+
|
318
|
+
if options[:key_count] > 1
|
319
|
+
options[:key_count] = (options[:key_count] / 40).to_i
|
320
|
+
keys = to_hash_collision_keys_array(options[:key_count])
|
321
|
+
run_thread_loop(meth, keys, options.merge(:loop_count => (options[:loop_count] * 5)), &block)
|
322
|
+
end
|
332
323
|
end
|
333
|
-
end
|
334
|
-
yield result, cache, options, keys if block_given?
|
335
|
-
end
|
324
|
+
end
|
336
325
|
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
326
|
+
def run_thread_loop(meth, keys, options)
|
327
|
+
cache = options[:cache_setup].call(options, keys)
|
328
|
+
barrier = ThreadSafe::Test::Barrier.new(options[:thread_count])
|
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?
|
345
335
|
end
|
346
|
-
end
|
347
336
|
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
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
|
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
|
415
347
|
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
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
|
421
415
|
|
422
|
-
|
423
|
-
|
424
|
-
|
416
|
+
def to_keys_array(key_count)
|
417
|
+
arr = []
|
418
|
+
key_count.times {|i| arr << i}
|
419
|
+
arr
|
420
|
+
end
|
425
421
|
|
426
|
-
|
427
|
-
|
428
|
-
|
422
|
+
def to_hash_collision_keys_array(key_count)
|
423
|
+
to_keys_array(key_count).map {|key| ThreadSafe::Test::HashCollisionKey(key)}
|
424
|
+
end
|
429
425
|
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
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
435
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
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
|
440
441
|
end
|
441
442
|
end
|
442
|
-
end
|
443
443
|
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
444
|
+
def assert_count_up(result, cache, options, keys)
|
445
|
+
keys.each do |key|
|
446
|
+
unless value = cache[key]
|
447
|
+
assert value
|
448
|
+
end
|
448
449
|
end
|
450
|
+
assert_equal(sum(cache.values), sum(result))
|
451
|
+
assert_equal(options[:key_count], cache.size)
|
449
452
|
end
|
450
|
-
assert_equal(sum(cache.values), sum(result))
|
451
|
-
assert_equal(options[:key_count], cache.size)
|
452
453
|
end
|
453
454
|
end
|