thread_safe 0.3.1-java → 0.3.2-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.
- checksums.yaml +4 -4
- data/lib/thread_safe/synchronized_delegator.rb +8 -0
- data/lib/thread_safe/util/atomic_reference.rb +36 -3
- data/lib/thread_safe/version.rb +1 -1
- data/test/test_cache_loops.rb +376 -378
- data/test/test_synchronized_delegator.rb +0 -8
- data/thread_safe.gemspec +1 -1
- metadata +14 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 011bc3a02316bba0bb69f2aeaaf95d72d14c25a5
|
4
|
+
data.tar.gz: 27e578528177719a9eb1ac72727c3f84c053c484
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e821f7904a0bac53fc7d0a5854ee442daffc75aebed26e216f4f58f3c21a87e6b71bac97fbea8ecf074725ef8c3b0dc3eda741999580539e570afa3857a0c30
|
7
|
+
data.tar.gz: 4b49dc4c12fd055deefe80e21ee38567dbdfb74dd36b51e85bdf80c252465cadbab8aec826dcea71b23c1bf7f120ba45d6bf8243015e573272893be450fd93ab
|
@@ -16,6 +16,14 @@ require 'monitor'
|
|
16
16
|
# This class is currently being considered for inclusion into stdlib, via
|
17
17
|
# https://bugs.ruby-lang.org/issues/8556
|
18
18
|
class SynchronizedDelegator < SimpleDelegator
|
19
|
+
def setup
|
20
|
+
@old_abort = Thread.abort_on_exception
|
21
|
+
Thread.abort_on_exception = true
|
22
|
+
end
|
23
|
+
|
24
|
+
def teardown
|
25
|
+
Thread.abort_on_exception = @old_abort
|
26
|
+
end
|
19
27
|
|
20
28
|
def initialize(obj)
|
21
29
|
__setobj__(obj)
|
@@ -1,12 +1,45 @@
|
|
1
1
|
module ThreadSafe
|
2
2
|
module Util
|
3
|
-
# An overhead-less atomic reference.
|
4
3
|
AtomicReference =
|
5
4
|
if defined?(Rubinius::AtomicReference)
|
5
|
+
# An overhead-less atomic reference.
|
6
6
|
Rubinius::AtomicReference
|
7
7
|
else
|
8
|
-
|
9
|
-
|
8
|
+
begin
|
9
|
+
require 'atomic'
|
10
|
+
defined?(Atomic::InternalReference) ? Atomic::InternalReference : Atomic
|
11
|
+
rescue NameError
|
12
|
+
require 'thread' # get Mutex on 1.8
|
13
|
+
class FullLockingAtomicReference
|
14
|
+
def initialize(value = nil)
|
15
|
+
@___mutex = Mutex.new
|
16
|
+
@___value = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def get
|
20
|
+
@___mutex.synchronize { @___value }
|
21
|
+
end
|
22
|
+
alias_method :value, :get
|
23
|
+
|
24
|
+
def set(new_value)
|
25
|
+
@___mutex.synchronize { @___value = new_value }
|
26
|
+
end
|
27
|
+
alias_method :value=, :set
|
28
|
+
|
29
|
+
def compare_and_set(old_value, new_value)
|
30
|
+
return false unless @___mutex.try_lock
|
31
|
+
begin
|
32
|
+
return false unless @___value.equal? old_value
|
33
|
+
@___value = new_value
|
34
|
+
ensure
|
35
|
+
@___mutex.unlock
|
36
|
+
end
|
37
|
+
true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
FullLockingAtomicReference
|
42
|
+
end
|
10
43
|
end
|
11
44
|
end
|
12
45
|
end
|
data/lib/thread_safe/version.rb
CHANGED
data/test/test_cache_loops.rb
CHANGED
@@ -5,450 +5,448 @@ require File.join(File.dirname(__FILE__), "test_helper")
|
|
5
5
|
|
6
6
|
Thread.abort_on_exception = true
|
7
7
|
|
8
|
-
unless RUBY_VERSION =~ /1\.8/ || ENV['TRAVIS']
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
8
|
+
class TestCacheTorture < Test::Unit::TestCase # this is not run unless RUBY_VERSION =~ /1\.8/ || ENV['TRAVIS'] (see the end of the file)
|
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
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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(__method__, code)
|
42
|
+
end
|
44
43
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
end
|
44
|
+
def test_put_if_absent
|
45
|
+
do_thread_loop(__method__, '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)
|
49
47
|
end
|
48
|
+
end
|
50
49
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
end
|
50
|
+
def test_compute_if_absent
|
51
|
+
code = 'cache.compute_if_absent(key) { acc += 1; key }'
|
52
|
+
do_thread_loop(__method__, code) do |result, cache, options, keys|
|
53
|
+
assert_standard_accumulator_test_result(result, cache, options, keys)
|
56
54
|
end
|
55
|
+
end
|
57
56
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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)
|
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)
|
68
63
|
end
|
64
|
+
RUBY_EVAL
|
65
|
+
do_thread_loop(__method__, code) do |result, cache, options, keys|
|
66
|
+
assert_standard_accumulator_test_result(result, cache, options, keys)
|
69
67
|
end
|
68
|
+
end
|
70
69
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
76
75
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
82
81
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
88
87
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
88
|
+
def test_add_remove
|
89
|
+
add_remove
|
90
|
+
add_remove(LOW_KEY_COUNT_OPTIONS)
|
91
|
+
add_remove(SINGLE_KEY_COUNT_OPTIONS)
|
92
|
+
end
|
94
93
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
100
99
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
106
105
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
112
111
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
112
|
+
def test_count_up
|
113
|
+
count_up
|
114
|
+
count_up(LOW_KEY_COUNT_OPTIONS)
|
115
|
+
count_up(SINGLE_KEY_COUNT_OPTIONS)
|
116
|
+
end
|
118
117
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
124
123
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
130
129
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
end
|
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(__method__, 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)
|
143
141
|
end
|
142
|
+
end
|
144
143
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
end
|
144
|
+
def test_get_and_set_new
|
145
|
+
code = 'acc += 1 unless cache.get_and_set(key, key)'
|
146
|
+
do_thread_loop(__method__, code) do |result, cache, options, keys|
|
147
|
+
assert_standard_accumulator_test_result(result, cache, options, keys)
|
150
148
|
end
|
149
|
+
end
|
151
150
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
end
|
151
|
+
def test_get_and_set_existing
|
152
|
+
code = 'acc += 1 if cache.get_and_set(key, key) == -1'
|
153
|
+
do_thread_loop(__method__, 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)
|
157
155
|
end
|
156
|
+
end
|
158
157
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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)
|
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 }
|
180
166
|
end
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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))
|
167
|
+
RUBY_EVAL
|
168
|
+
do_thread_loop(__method__, 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
|
195
176
|
end
|
177
|
+
assert_equal(stored_sum, sum(result))
|
178
|
+
assert_equal(stored_key_count, cache.size)
|
196
179
|
end
|
180
|
+
end
|
197
181
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
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))
|
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)
|
214
189
|
end
|
190
|
+
RUBY_EVAL
|
191
|
+
do_thread_loop(__method__, 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))
|
215
194
|
end
|
195
|
+
end
|
216
196
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
assert_equal(cache.size, sum(result))
|
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
|
229
208
|
end
|
209
|
+
RUBY_EVAL
|
210
|
+
do_thread_loop(__method__, 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))
|
230
213
|
end
|
214
|
+
end
|
231
215
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
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))
|
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 }
|
244
223
|
end
|
224
|
+
RUBY_EVAL
|
225
|
+
do_thread_loop(__method__, 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))
|
245
228
|
end
|
229
|
+
end
|
246
230
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
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)
|
254
238
|
end
|
239
|
+
RUBY_EVAL
|
240
|
+
do_thread_loop(__method__, 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))
|
255
243
|
end
|
244
|
+
end
|
256
245
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
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
|
270
|
-
end
|
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(__method__, 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)
|
271
253
|
end
|
254
|
+
end
|
272
255
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
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
|
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
|
287
261
|
end
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
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))
|
262
|
+
RUBY_EVAL
|
263
|
+
do_thread_loop(__method__, 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
|
298
268
|
end
|
299
269
|
end
|
270
|
+
end
|
300
271
|
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
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(__method__, 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
|
308
284
|
end
|
285
|
+
assert all_match
|
309
286
|
end
|
287
|
+
end
|
310
288
|
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
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
|
323
|
-
end
|
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(__method__, 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))
|
324
297
|
end
|
298
|
+
end
|
325
299
|
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
end.map(&:value)
|
334
|
-
yield result, cache, options, keys if block_given?
|
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(__method__, 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))
|
335
307
|
end
|
308
|
+
end
|
336
309
|
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
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)
|
345
321
|
end
|
346
322
|
end
|
323
|
+
end
|
347
324
|
|
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
|
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
|
+
result = (1..options[:thread_count]).map do
|
329
|
+
Thread.new do
|
330
|
+
setup_sync_and_start_loop(meth, cache, keys, barrier, options[:loop_count])
|
331
|
+
end
|
332
|
+
end.map(&:value)
|
333
|
+
yield result, cache, options, keys if block_given?
|
334
|
+
end
|
415
335
|
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
336
|
+
def setup_sync_and_start_loop(meth, cache, keys, barrier, loop_count)
|
337
|
+
my_keys = keys.shuffle
|
338
|
+
barrier.await
|
339
|
+
if my_keys.size == 1
|
340
|
+
key = my_keys.first
|
341
|
+
send("#{meth}_single_key", cache, key, loop_count)
|
342
|
+
else
|
343
|
+
send("#{meth}_multiple_keys", cache, my_keys, loop_count)
|
420
344
|
end
|
345
|
+
end
|
421
346
|
|
422
|
-
|
423
|
-
|
424
|
-
|
347
|
+
def define_loop(name, body, prelude)
|
348
|
+
inner_meth_name = :"_#{name}_loop_inner"
|
349
|
+
outer_meth_name = :"_#{name}_loop_outer"
|
350
|
+
# looping is splitted into the "loop methods" to trigger the JIT
|
351
|
+
self.class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
352
|
+
def #{inner_meth_name}_multiple_keys(cache, keys, i, length, acc)
|
353
|
+
#{prelude}
|
354
|
+
target = i + length
|
355
|
+
while i < target
|
356
|
+
key = keys[i]
|
357
|
+
#{body}
|
358
|
+
i += 1
|
359
|
+
end
|
360
|
+
acc
|
361
|
+
end unless method_defined?(:#{inner_meth_name}_multiple_keys)
|
362
|
+
RUBY_EVAL
|
363
|
+
|
364
|
+
self.class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
365
|
+
def #{inner_meth_name}_single_key(cache, key, i, length, acc)
|
366
|
+
#{prelude}
|
367
|
+
target = i + length
|
368
|
+
while i < target
|
369
|
+
#{body}
|
370
|
+
i += 1
|
371
|
+
end
|
372
|
+
acc
|
373
|
+
end unless method_defined?(:#{inner_meth_name}_single_key)
|
374
|
+
RUBY_EVAL
|
375
|
+
|
376
|
+
self.class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
377
|
+
def #{outer_meth_name}_multiple_keys(cache, keys, loop_count)
|
378
|
+
total_length = keys.size
|
379
|
+
acc = 0
|
380
|
+
inc = 100
|
381
|
+
loop_count.times do
|
382
|
+
i = 0
|
383
|
+
pre_loop_inc = total_length % inc
|
384
|
+
acc = #{inner_meth_name}_multiple_keys(cache, keys, i, pre_loop_inc, acc)
|
385
|
+
i += pre_loop_inc
|
386
|
+
while i < total_length
|
387
|
+
acc = #{inner_meth_name}_multiple_keys(cache, keys, i, inc, acc)
|
388
|
+
i += inc
|
389
|
+
end
|
390
|
+
end
|
391
|
+
acc
|
392
|
+
end unless method_defined?(:#{outer_meth_name}_multiple_keys)
|
393
|
+
RUBY_EVAL
|
394
|
+
|
395
|
+
self.class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
396
|
+
def #{outer_meth_name}_single_key(cache, key, loop_count)
|
397
|
+
acc = 0
|
398
|
+
i = 0
|
399
|
+
inc = 100
|
400
|
+
|
401
|
+
pre_loop_inc = loop_count % inc
|
402
|
+
acc = #{inner_meth_name}_single_key(cache, key, i, pre_loop_inc, acc)
|
403
|
+
i += pre_loop_inc
|
404
|
+
|
405
|
+
while i < loop_count
|
406
|
+
acc = #{inner_meth_name}_single_key(cache, key, i, inc, acc)
|
407
|
+
i += inc
|
408
|
+
end
|
409
|
+
acc
|
410
|
+
end unless method_defined?(:#{outer_meth_name}_single_key)
|
411
|
+
RUBY_EVAL
|
412
|
+
outer_meth_name
|
413
|
+
end
|
425
414
|
|
426
|
-
|
427
|
-
|
428
|
-
|
415
|
+
def to_keys_array(key_count)
|
416
|
+
arr = []
|
417
|
+
key_count.times {|i| arr << i}
|
418
|
+
arr
|
419
|
+
end
|
429
420
|
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
assert_equal(options[:key_count], cache.size)
|
434
|
-
end
|
421
|
+
def to_hash_collision_keys_array(key_count)
|
422
|
+
to_keys_array(key_count).map {|key| ThreadSafe::Test::HashCollisionKey(key)}
|
423
|
+
end
|
435
424
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
425
|
+
def sum(result)
|
426
|
+
result.inject(0) {|acc, i| acc + i}
|
427
|
+
end
|
428
|
+
|
429
|
+
def assert_standard_accumulator_test_result(result, cache, options, keys)
|
430
|
+
assert_all_key_mappings_exist(cache, keys)
|
431
|
+
assert_equal(options[:key_count], sum(result))
|
432
|
+
assert_equal(options[:key_count], cache.size)
|
433
|
+
end
|
434
|
+
|
435
|
+
def assert_all_key_mappings_exist(cache, keys, all_must_exist = true)
|
436
|
+
keys.each do |key|
|
437
|
+
if (value = cache[key]) || all_must_exist
|
438
|
+
assert_equal key, value unless key == value # don't do a bazzilion assertions unless necessary
|
441
439
|
end
|
442
440
|
end
|
441
|
+
end
|
443
442
|
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
end
|
443
|
+
def assert_count_up(result, cache, options, keys)
|
444
|
+
keys.each do |key|
|
445
|
+
unless value = cache[key]
|
446
|
+
assert value
|
449
447
|
end
|
450
|
-
assert_equal(sum(cache.values), sum(result))
|
451
|
-
assert_equal(options[:key_count], cache.size)
|
452
448
|
end
|
449
|
+
assert_equal(sum(cache.values), sum(result))
|
450
|
+
assert_equal(options[:key_count], cache.size)
|
453
451
|
end
|
454
|
-
end
|
452
|
+
end unless RUBY_VERSION =~ /1\.8/ || ENV['TRAVIS']
|
@@ -2,14 +2,6 @@ require 'test/unit'
|
|
2
2
|
require 'thread_safe/synchronized_delegator.rb'
|
3
3
|
|
4
4
|
class TestSynchronizedDelegator < Test::Unit::TestCase
|
5
|
-
def setup
|
6
|
-
@old_abort = Thread.abort_on_exception
|
7
|
-
Thread.abort_on_exception = true
|
8
|
-
end
|
9
|
-
|
10
|
-
def teardown
|
11
|
-
Thread.abort_on_exception = @old_abort
|
12
|
-
end
|
13
5
|
|
14
6
|
def test_wraps_array
|
15
7
|
sync_array = SynchronizedDelegator.new(array = [])
|
data/thread_safe.gemspec
CHANGED
@@ -19,6 +19,6 @@ Gem::Specification.new do |gem|
|
|
19
19
|
gem.version = ThreadSafe::VERSION
|
20
20
|
gem.license = "Apache-2.0"
|
21
21
|
|
22
|
-
gem.
|
22
|
+
gem.add_development_dependency 'atomic', ['>= 1.1.7', '< 2']
|
23
23
|
gem.add_development_dependency 'rake'
|
24
24
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: thread_safe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.2
|
5
5
|
platform: java
|
6
6
|
authors:
|
7
7
|
- Charles Oliver Nutter
|
@@ -9,10 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-
|
12
|
+
date: 2014-04-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
|
15
|
+
name: atomic
|
16
|
+
version_requirements: !ruby/object:Gem::Requirement
|
16
17
|
requirements:
|
17
18
|
- - '>='
|
18
19
|
- !ruby/object:Gem::Version
|
@@ -20,10 +21,7 @@ dependencies:
|
|
20
21
|
- - <
|
21
22
|
- !ruby/object:Gem::Version
|
22
23
|
version: '2'
|
23
|
-
|
24
|
-
prerelease: false
|
25
|
-
type: :runtime
|
26
|
-
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirement: !ruby/object:Gem::Requirement
|
27
25
|
requirements:
|
28
26
|
- - '>='
|
29
27
|
- !ruby/object:Gem::Version
|
@@ -31,20 +29,22 @@ dependencies:
|
|
31
29
|
- - <
|
32
30
|
- !ruby/object:Gem::Version
|
33
31
|
version: '2'
|
32
|
+
prerelease: false
|
33
|
+
type: :development
|
34
34
|
- !ruby/object:Gem::Dependency
|
35
|
-
|
35
|
+
name: rake
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
37
|
requirements:
|
37
38
|
- - '>='
|
38
39
|
- !ruby/object:Gem::Version
|
39
40
|
version: '0'
|
40
|
-
|
41
|
-
prerelease: false
|
42
|
-
type: :development
|
43
|
-
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirement: !ruby/object:Gem::Requirement
|
44
42
|
requirements:
|
45
43
|
- - '>='
|
46
44
|
- !ruby/object:Gem::Version
|
47
45
|
version: '0'
|
46
|
+
prerelease: false
|
47
|
+
type: :development
|
48
48
|
description: Thread-safe collections and utilities for Ruby
|
49
49
|
email:
|
50
50
|
- headius@headius.com
|
@@ -73,6 +73,7 @@ files:
|
|
73
73
|
- lib/thread_safe.rb
|
74
74
|
- lib/thread_safe/atomic_reference_cache_backend.rb
|
75
75
|
- lib/thread_safe/cache.rb
|
76
|
+
- lib/thread_safe/jruby_cache_backend.jar
|
76
77
|
- lib/thread_safe/mri_cache_backend.rb
|
77
78
|
- lib/thread_safe/non_concurrent_cache_backend.rb
|
78
79
|
- lib/thread_safe/synchronized_cache_backend.rb
|
@@ -95,7 +96,6 @@ files:
|
|
95
96
|
- test/test_helper.rb
|
96
97
|
- test/test_synchronized_delegator.rb
|
97
98
|
- thread_safe.gemspec
|
98
|
-
- lib/thread_safe/jruby_cache_backend.jar
|
99
99
|
homepage: https://github.com/headius/thread_safe
|
100
100
|
licenses:
|
101
101
|
- Apache-2.0
|
@@ -116,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
116
|
version: '0'
|
117
117
|
requirements: []
|
118
118
|
rubyforge_project:
|
119
|
-
rubygems_version: 2.
|
119
|
+
rubygems_version: 2.2.2
|
120
120
|
signing_key:
|
121
121
|
specification_version: 4
|
122
122
|
summary: A collection of data structures and utilities to make thread-safe programming in Ruby easier
|