thread_safe 0.3.5 → 0.3.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c96499b75d23ede7f61f92d312c2bf7ae74faca6
4
- data.tar.gz: ea25ab899f080309bc6e377abf862659cf7182fd
3
+ metadata.gz: fcad57c51382a0b336b584d99616a827a21f7935
4
+ data.tar.gz: ff9a9028bfd4f486ae56f39ce73ccd77282f55bb
5
5
  SHA512:
6
- metadata.gz: 6013a1a1ef823ce1e2a41f846d72ca6be5783e038f7a236e29b424dbc03d03249b9d63cee18b78598bc1442ee7fda2ba239ea5bc97816fa043283d12e97a01f4
7
- data.tar.gz: 9b55f254f97e693dc1b18ff9cb5b3a2fdc1f7e16525fb68af1616308cf5dc2bc1623a4af71905493f04f633a7861e117c8987ffa85da2034865f6d91de476604
6
+ metadata.gz: 8fe56d332d35f83aff17a19ea35ffd09537ae2823c030c44f4026084cfb0cc185c681c9a9800cc40c5834157cd5b60f5f6bb3d6238ab75f41f98c1900e2d50d2
7
+ data.tar.gz: 74a72a9d381c6eb47c3d4b650d355e2a89ceb7b3d24f35441e921938484aa44e83d5894e0fe89e7e8f9b1004a1e73a69dea66d1d30ffcbc35fd8ab1d9e9bbd11
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --format progress
@@ -1,43 +1,46 @@
1
+ dist: trusty
2
+ sudo: required
1
3
  language: ruby
2
4
  rvm:
3
- - 2.2.0
4
- - 2.1.5
5
- - 2.1.4
5
+ - 2.4.0
6
+ - 2.3.3
7
+ - 2.2.6
8
+ - 2.1.9
6
9
  - 2.0.0
7
10
  - 1.9.3
8
11
  - ruby-head
9
- - jruby-1.7.18
12
+ - jruby-9.1.5.0
13
+ - jruby-1.7.26
10
14
  - jruby-head
11
- - rbx-2
12
- jdk: # for JRuby only
15
+ - rubinius-3
16
+ - rubinius-2
17
+ jdk:
13
18
  - openjdk7
14
19
  - oraclejdk8
15
20
  matrix:
16
21
  exclude:
17
- - rvm: 2.2.0
18
- jdk: openjdk7
22
+ - rvm: 2.4.0
19
23
  jdk: oraclejdk8
20
- - rvm: 2.1.5
21
- jdk: openjdk7
24
+ - rvm: 2.3.3
22
25
  jdk: oraclejdk8
23
- - rvm: 2.1.4
24
- jdk: openjdk7
26
+ - rvm: 2.2.6
27
+ jdk: oraclejdk8
28
+ - rvm: 2.1.9
25
29
  jdk: oraclejdk8
26
30
  - rvm: 2.0.0
27
- jdk: openjdk7
28
31
  jdk: oraclejdk8
29
32
  - rvm: 1.9.3
30
- jdk: openjdk7
31
33
  jdk: oraclejdk8
32
34
  - rvm: ruby-head
33
- jdk: openjdk7
34
35
  jdk: oraclejdk8
35
- - rvm: rbx-2
36
- jdk: openjdk7
36
+ - rvm: rubinius-3
37
+ jdk: oraclejdk8
38
+ - rvm: rubinius-2
37
39
  jdk: oraclejdk8
38
40
  allow_failures:
41
+ - rvm: 1.9.3
39
42
  - rvm: ruby-head
43
+ - rvm: jruby-1.7.26
40
44
  - rvm: jruby-head
41
- - rvm: 1.9.3
42
-
43
- script: "rake TESTOPTS='--seed=1'"
45
+ - rvm: rubinius-2
46
+ script: "bundle exec rake test"
data/Gemfile CHANGED
@@ -3,9 +3,12 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  group :development, :test do
6
- gem 'minitest', '~> 5.5.1'
7
- gem 'minitest-reporters', '~> 1.0.11'
6
+ gem 'rspec', '~> 3.2.0'
8
7
  gem 'simplecov', '~> 0.9.2', :require => false
8
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0')
9
+ gem 'term-ansicolor', '~> 1.3.2', :require => false
10
+ gem 'tins', '~> 1.6.0', :require => false
11
+ end
9
12
  gem 'coveralls', '~> 0.7.11', :require => false
10
13
  end
11
14
 
data/README.md CHANGED
@@ -1,9 +1,13 @@
1
- # Threadsafe
1
+ # Threadsafe (Inactive, code moved to concurrent-ruby gem and repo.)
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/thread_safe.svg)](http://badge.fury.io/rb/thread_safe) [![Build Status](https://travis-ci.org/ruby-concurrency/thread_safe.svg?branch=master)](https://travis-ci.org/ruby-concurrency/thread_safe) [![Coverage Status](https://img.shields.io/coveralls/ruby-concurrency/thread_safe/master.svg)](https://coveralls.io/r/ruby-concurrency/thread_safe) [![Code Climate](https://codeclimate.com/github/ruby-concurrency/thread_safe.svg)](https://codeclimate.com/github/ruby-concurrency/thread_safe) [![Dependency Status](https://gemnasium.com/ruby-concurrency/thread_safe.svg)](https://gemnasium.com/ruby-concurrency/thread_safe) [![License](https://img.shields.io/badge/license-apache-green.svg)](http://opensource.org/licenses/MIT) [![Gitter chat](http://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/ruby-concurrency/concurrent-ruby)
4
4
 
5
5
  A collection of thread-safe versions of common core Ruby classes.
6
6
 
7
+ __This code base is now part of the concurrent-ruby gem
8
+ at https://github.com/ruby-concurrency/concurrent-ruby.
9
+ The code in this repository is no longer maintained.__
10
+
7
11
  ## Installation
8
12
 
9
13
  Add this line to your application's Gemfile:
data/Rakefile CHANGED
@@ -1,5 +1,6 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec'
3
+ require 'rspec/core/rake_task'
3
4
 
4
5
  ## safely load all the rake tasks in the `tasks` directory
5
6
  def safe_load(file)
@@ -10,6 +11,7 @@ def safe_load(file)
10
11
  puts ex.message
11
12
  end
12
13
  end
14
+
13
15
  Dir.glob('tasks/**/*.rake').each do |rakefile|
14
16
  safe_load rakefile
15
17
  end
@@ -17,9 +19,9 @@ end
17
19
  task :default => :test
18
20
 
19
21
  if defined?(JRUBY_VERSION)
20
- require "ant"
22
+ require 'ant'
21
23
 
22
- directory "pkg/classes"
24
+ directory 'pkg/classes'
23
25
  directory 'pkg/tests'
24
26
 
25
27
  desc "Clean up build artifacts"
@@ -43,19 +45,18 @@ if defined?(JRUBY_VERSION)
43
45
 
44
46
  desc "Build test jar"
45
47
  task 'test-jar' => 'pkg/tests' do |t|
46
- ant.javac :srcdir => 'test/src', :destdir => t.prerequisites.first,
48
+ ant.javac :srcdir => 'spec/src', :destdir => t.prerequisites.first,
47
49
  :source => "1.5", :target => "1.5", :debug => true
48
50
 
49
- ant.jar :basedir => 'pkg/tests', :destfile => 'test/package.jar', :includes => '**/*.class'
51
+ ant.jar :basedir => 'pkg/tests', :destfile => 'spec/package.jar', :includes => '**/*.class'
50
52
  end
51
53
 
52
- task :package => [ :jar, 'test-jar' ]
54
+ task :package => [ :clean, :compile, :jar, 'test-jar' ]
53
55
  else
54
56
  # No need to package anything for non-jruby rubies
55
57
  task :package
56
58
  end
57
59
 
58
- Rake::TestTask.new :test => :package do |t|
59
- t.libs << "lib"
60
- t.test_files = FileList["test/**/*.rb"]
60
+ RSpec::Core::RakeTask.new :test => :package do |t|
61
+ t.rspec_opts = '--color --backtrace --tag ~unfinished --seed 1 --format documentation ./spec'
61
62
  end
@@ -21,8 +21,6 @@ module ThreadSafe
21
21
  end
22
22
 
23
23
  class Cache < ConcurrentCacheBackend
24
- KEY_ERROR = defined?(KeyError) ? KeyError : IndexError # there is no KeyError in 1.8 mode
25
-
26
24
  def initialize(options = nil, &block)
27
25
  if options.kind_of?(::Hash)
28
26
  validate_options_hash!(options)
@@ -138,7 +136,7 @@ module ThreadSafe
138
136
 
139
137
  private
140
138
  def raise_fetch_no_key
141
- raise KEY_ERROR, 'key not found'
139
+ raise KeyError, 'key not found'
142
140
  end
143
141
 
144
142
  def initialize_copy(other)
@@ -152,8 +150,8 @@ module ThreadSafe
152
150
  end
153
151
 
154
152
  def validate_options_hash!(options)
155
- if (initial_capacity = options[:initial_capacity]) && (!initial_capacity.kind_of?(Fixnum) || initial_capacity < 0)
156
- raise ArgumentError, ":initial_capacity must be a positive Fixnum"
153
+ if (initial_capacity = options[:initial_capacity]) && (!initial_capacity.kind_of?(0.class) || initial_capacity < 0)
154
+ raise ArgumentError, ":initial_capacity must be a positive #{0.class}"
157
155
  end
158
156
  if (load_factor = options[:load_factor]) && (!load_factor.kind_of?(Numeric) || load_factor <= 0 || load_factor > 1)
159
157
  raise ArgumentError, ":load_factor must be a number between 0 and 1"
@@ -3,15 +3,6 @@ module ThreadSafe
3
3
  # We can get away with a single global write lock (instead of a per-instance
4
4
  # one) because of the GVL/green threads.
5
5
  #
6
- # The previous implementation used `Thread.critical` on 1.8 MRI to implement
7
- # the 4 composed atomic operations (`put_if_absent`, `replace_pair`,
8
- # `replace_if_exists`, `delete_pair`) this however doesn't work for
9
- # `compute_if_absent` because on 1.8 the Mutex class is itself implemented
10
- # via `Thread.critical` and a call to `Mutex#lock` does not restore the
11
- # previous `Thread.critical` value (thus any synchronisation clears the
12
- # `Thread.critical` flag and we loose control). This poses a problem as the
13
- # provided block might use synchronisation on its own.
14
- #
15
6
  # NOTE: a neat idea of writing a c-ext to manually perform atomic
16
7
  # put_if_absent, while relying on Ruby not releasing a GVL while calling a
17
8
  # c-ext will not work because of the potentially Ruby implemented `#hash`
@@ -40,21 +40,4 @@ class SynchronizedDelegator < SimpleDelegator
40
40
  end
41
41
  end
42
42
 
43
- # Work-around for 1.8 std-lib not passing block around to delegate.
44
- # @private
45
- def method_missing(method, *args, &block)
46
- monitor = @monitor
47
- begin
48
- monitor.enter
49
- target = self.__getobj__
50
- if target.respond_to?(method)
51
- target.__send__(method, *args, &block)
52
- else
53
- super(method, *args, &block)
54
- end
55
- ensure
56
- monitor.exit
57
- end
58
- end if RUBY_VERSION[0, 3] == '1.8'
59
-
60
43
  end unless defined?(SynchronizedDelegator)
@@ -9,7 +9,6 @@ module ThreadSafe
9
9
  require 'atomic'
10
10
  defined?(Atomic::InternalReference) ? Atomic::InternalReference : Atomic
11
11
  rescue LoadError, NameError
12
- require 'thread' # get Mutex on 1.8
13
12
  class FullLockingAtomicReference
14
13
  def initialize(value = nil)
15
14
  @___mutex = Mutex.new
@@ -1,5 +1,5 @@
1
1
  module ThreadSafe
2
- VERSION = "0.3.5"
2
+ VERSION = "0.3.6"
3
3
  end
4
4
 
5
5
  # NOTE: <= 0.2.0 used Threadsafe::VERSION
File without changes
@@ -0,0 +1,31 @@
1
+ require 'simplecov'
2
+ require 'coveralls'
3
+ require 'logger'
4
+
5
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
6
+ SimpleCov::Formatter::HTMLFormatter,
7
+ Coveralls::SimpleCov::Formatter
8
+ ]
9
+
10
+ SimpleCov.start do
11
+ project_name 'thread_safe'
12
+ add_filter '/coverage/'
13
+ add_filter '/pkg/'
14
+ add_filter '/spec/'
15
+ add_filter '/tasks/'
16
+ add_filter '/yard-template/'
17
+ end
18
+
19
+ $VERBOSE = nil # suppress our deprecation warnings
20
+ require 'thread_safe'
21
+
22
+ logger = Logger.new($stderr)
23
+ logger.level = Logger::WARN
24
+
25
+ # import all the support files
26
+ Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require File.expand_path(f) }
27
+
28
+ RSpec.configure do |config|
29
+ #config.raise_errors_for_deprecations!
30
+ config.order = 'random'
31
+ end
File without changes
@@ -0,0 +1 @@
1
+ THREADS = (RUBY_ENGINE == 'ruby' ? 100 : 10)
@@ -1,62 +1,3 @@
1
- unless defined?(JRUBY_VERSION)
2
- require 'simplecov'
3
- require 'coveralls'
4
-
5
- SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
6
- SimpleCov::Formatter::HTMLFormatter,
7
- Coveralls::SimpleCov::Formatter
8
- ]
9
-
10
- SimpleCov.start do
11
- project_name 'thread_safe'
12
-
13
- add_filter '/examples/'
14
- add_filter '/pkg/'
15
- add_filter '/test/'
16
- add_filter '/tasks/'
17
- add_filter '/yard-template/'
18
- add_filter '/yardoc/'
19
-
20
- command_name 'Mintest'
21
- end
22
- end
23
-
24
- require 'minitest/autorun'
25
-
26
- require 'minitest/reporters'
27
- Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new(color: true)
28
-
29
- require 'thread'
30
- require 'thread_safe'
31
-
32
- THREADS = (RUBY_ENGINE == 'ruby' ? 100 : 10)
33
-
34
- if defined?(JRUBY_VERSION) && ENV['TEST_NO_UNSAFE']
35
- # to be used like this: rake test TEST_NO_UNSAFE=true
36
- load 'test/package.jar'
37
- java_import 'thread_safe.SecurityManager'
38
- manager = SecurityManager.new
39
-
40
- # Prevent accessing internal classes
41
- manager.deny java.lang.RuntimePermission.new("accessClassInPackage.sun.misc")
42
- java.lang.System.setSecurityManager manager
43
-
44
- class TestNoUnsafe < Minitest::Test
45
- def test_security_manager_is_used
46
- begin
47
- java_import 'sun.misc.Unsafe'
48
- flunk
49
- rescue SecurityError
50
- end
51
- end
52
-
53
- def test_no_unsafe_version_of_chmv8_is_used
54
- require 'thread_safe/jruby_cache_backend' # make sure the jar has been loaded
55
- assert !Java::OrgJrubyExtThread_safe::JRubyCacheBackendLibrary::JRubyCacheBackend::CAN_USE_UNSAFE_CHM
56
- end
57
- end
58
- end
59
-
60
1
  module ThreadSafe
61
2
  module Test
62
3
  class Latch
File without changes
@@ -0,0 +1,18 @@
1
+ module ThreadSafe
2
+ describe Array do
3
+ let!(:ary) { described_class.new }
4
+
5
+ it 'concurrency' do
6
+ (1..THREADS).map do |i|
7
+ Thread.new do
8
+ 1000.times do
9
+ ary << i
10
+ ary.each { |x| x * 2 }
11
+ ary.shift
12
+ ary.last
13
+ end
14
+ end
15
+ end.map(&:join)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,507 @@
1
+ Thread.abort_on_exception = true
2
+
3
+ module ThreadSafe
4
+ describe 'CacheTorture' do
5
+ THREAD_COUNT = 40
6
+ KEY_COUNT = (((2**13) - 2) * 0.75).to_i # get close to the doubling cliff
7
+ LOW_KEY_COUNT = (((2**8 ) - 2) * 0.75).to_i # get close to the doubling cliff
8
+
9
+ INITIAL_VALUE_CACHE_SETUP = lambda do |options, keys|
10
+ cache = ThreadSafe::Cache.new
11
+ initial_value = options[:initial_value] || 0
12
+ keys.each { |key| cache[key] = initial_value }
13
+ cache
14
+ end
15
+ ZERO_VALUE_CACHE_SETUP = lambda do |options, keys|
16
+ INITIAL_VALUE_CACHE_SETUP.call(options.merge(:initial_value => 0), keys)
17
+ end
18
+
19
+ DEFAULTS = {
20
+ key_count: KEY_COUNT,
21
+ thread_count: THREAD_COUNT,
22
+ loop_count: 1,
23
+ prelude: '',
24
+ cache_setup: lambda { |options, keys| ThreadSafe::Cache.new }
25
+ }
26
+
27
+ LOW_KEY_COUNT_OPTIONS = {loop_count: 150, key_count: LOW_KEY_COUNT}
28
+ SINGLE_KEY_COUNT_OPTIONS = {loop_count: 100_000, key_count: 1}
29
+
30
+ it 'concurrency' do
31
+ code = <<-RUBY_EVAL
32
+ cache[key]
33
+ cache[key] = key
34
+ cache[key]
35
+ cache.delete(key)
36
+ RUBY_EVAL
37
+ do_thread_loop(:concurrency, code)
38
+ end
39
+
40
+ it '#put_if_absent' do
41
+ do_thread_loop(
42
+ :put_if_absent,
43
+ 'acc += 1 unless cache.put_if_absent(key, key)',
44
+ key_count: 100_000
45
+ ) do |result, cache, options, keys|
46
+ expect_standard_accumulator_test_result(result, cache, options, keys)
47
+ end
48
+ end
49
+
50
+ it '#compute_put_if_absent' do
51
+ code = <<-RUBY_EVAL
52
+ if key.even?
53
+ cache.compute_if_absent(key) { acc += 1; key }
54
+ else
55
+ acc += 1 unless cache.put_if_absent(key, key)
56
+ end
57
+ RUBY_EVAL
58
+ do_thread_loop(:compute_if_absent, code) do |result, cache, options, keys|
59
+ expect_standard_accumulator_test_result(result, cache, options, keys)
60
+ end
61
+ end
62
+
63
+ it '#compute_if_absent_and_present' do
64
+ compute_if_absent_and_present
65
+ compute_if_absent_and_present(LOW_KEY_COUNT_OPTIONS)
66
+ compute_if_absent_and_present(SINGLE_KEY_COUNT_OPTIONS)
67
+ end
68
+
69
+ it 'add_remove_to_zero' do
70
+ add_remove_to_zero
71
+ add_remove_to_zero(LOW_KEY_COUNT_OPTIONS)
72
+ add_remove_to_zero(SINGLE_KEY_COUNT_OPTIONS)
73
+ end
74
+
75
+ it 'add_remove_to_zero_via_merge_pair' do
76
+ add_remove_to_zero_via_merge_pair
77
+ add_remove_to_zero_via_merge_pair(LOW_KEY_COUNT_OPTIONS)
78
+ add_remove_to_zero_via_merge_pair(SINGLE_KEY_COUNT_OPTIONS)
79
+ end
80
+
81
+ it 'add_remove' do
82
+ add_remove
83
+ add_remove(LOW_KEY_COUNT_OPTIONS)
84
+ add_remove(SINGLE_KEY_COUNT_OPTIONS)
85
+ end
86
+
87
+ it 'add_remove_via_compute' do
88
+ add_remove_via_compute
89
+ add_remove_via_compute(LOW_KEY_COUNT_OPTIONS)
90
+ add_remove_via_compute(SINGLE_KEY_COUNT_OPTIONS)
91
+ end
92
+
93
+ it 'emove_via_compute_if_absent_present' do
94
+ add_remove_via_compute_if_absent_present
95
+ add_remove_via_compute_if_absent_present(LOW_KEY_COUNT_OPTIONS)
96
+ add_remove_via_compute_if_absent_present(SINGLE_KEY_COUNT_OPTIONS)
97
+ end
98
+
99
+ it 'add_remove_indiscriminate' do
100
+ add_remove_indiscriminate
101
+ add_remove_indiscriminate(LOW_KEY_COUNT_OPTIONS)
102
+ add_remove_indiscriminate(SINGLE_KEY_COUNT_OPTIONS)
103
+ end
104
+
105
+ it 'count_up' do
106
+ count_up
107
+ count_up(LOW_KEY_COUNT_OPTIONS)
108
+ count_up(SINGLE_KEY_COUNT_OPTIONS)
109
+ end
110
+
111
+ it 'count_up_via_compute' do
112
+ count_up_via_compute
113
+ count_up_via_compute(LOW_KEY_COUNT_OPTIONS)
114
+ count_up_via_compute(SINGLE_KEY_COUNT_OPTIONS)
115
+ end
116
+
117
+ it 'count_up_via_merge_pair' do
118
+ count_up_via_merge_pair
119
+ count_up_via_merge_pair(LOW_KEY_COUNT_OPTIONS)
120
+ count_up_via_merge_pair(SINGLE_KEY_COUNT_OPTIONS)
121
+ end
122
+
123
+ it 'count_race' do
124
+ prelude = 'change = (rand(2) == 1) ? 1 : -1'
125
+ code = <<-RUBY_EVAL
126
+ v = cache[key]
127
+ acc += change if cache.replace_pair(key, v, v + change)
128
+ RUBY_EVAL
129
+ do_thread_loop(
130
+ :count_race,
131
+ code,
132
+ loop_count: 5,
133
+ prelude: prelude,
134
+ cache_setup: ZERO_VALUE_CACHE_SETUP
135
+ ) do |result, cache, options, keys|
136
+ result_sum = sum(result)
137
+ expect(sum(keys.map { |key| cache[key] })).to eq result_sum
138
+ expect(sum(cache.values)).to eq result_sum
139
+ expect(options[:key_count]).to eq cache.size
140
+ end
141
+ end
142
+
143
+ it 'get_and_set_new' do
144
+ code = 'acc += 1 unless cache.get_and_set(key, key)'
145
+ do_thread_loop(:get_and_set_new, code) do |result, cache, options, keys|
146
+ expect_standard_accumulator_test_result(result, cache, options, keys)
147
+ end
148
+ end
149
+
150
+ it 'get_and_set_existing' do
151
+ code = 'acc += 1 if cache.get_and_set(key, key) == -1'
152
+ do_thread_loop(
153
+ :get_and_set_existing,
154
+ code,
155
+ cache_setup: INITIAL_VALUE_CACHE_SETUP,
156
+ initial_value: -1
157
+ ) do |result, cache, options, keys|
158
+ expect_standard_accumulator_test_result(result, cache, options, keys)
159
+ end
160
+ end
161
+
162
+ private
163
+
164
+ def compute_if_absent_and_present(opts = {})
165
+ prelude = 'on_present = rand(2) == 1'
166
+ code = <<-RUBY_EVAL
167
+ if on_present
168
+ cache.compute_if_present(key) { |old_value| acc += 1; old_value + 1 }
169
+ else
170
+ cache.compute_if_absent(key) { acc += 1; 1 }
171
+ end
172
+ RUBY_EVAL
173
+ do_thread_loop(
174
+ __method__,
175
+ code,
176
+ {loop_count: 5, prelude: prelude}.merge(opts)
177
+ ) do |result, cache, options, keys|
178
+ stored_sum = 0
179
+ stored_key_count = 0
180
+ keys.each do |k|
181
+ if value = cache[k]
182
+ stored_sum += value
183
+ stored_key_count += 1
184
+ end
185
+ end
186
+ expect(stored_sum).to eq sum(result)
187
+ expect(stored_key_count).to eq cache.size
188
+ end
189
+ end
190
+
191
+ def add_remove(opts = {})
192
+ prelude = 'do_add = rand(2) == 1'
193
+ code = <<-RUBY_EVAL
194
+ if do_add
195
+ acc += 1 unless cache.put_if_absent(key, key)
196
+ else
197
+ acc -= 1 if cache.delete_pair(key, key)
198
+ end
199
+ RUBY_EVAL
200
+ do_thread_loop(
201
+ __method__,
202
+ code,
203
+ {loop_count: 5, prelude: prelude}.merge(opts)
204
+ ) do |result, cache, options, keys|
205
+ expect_all_key_mappings_exist(cache, keys, false)
206
+ expect(cache.size).to eq sum(result)
207
+ end
208
+ end
209
+
210
+ def add_remove_via_compute(opts = {})
211
+ prelude = 'do_add = rand(2) == 1'
212
+ code = <<-RUBY_EVAL
213
+ cache.compute(key) do |old_value|
214
+ if do_add
215
+ acc += 1 unless old_value
216
+ key
217
+ else
218
+ acc -= 1 if old_value
219
+ nil
220
+ end
221
+ end
222
+ RUBY_EVAL
223
+ do_thread_loop(
224
+ __method__,
225
+ code,
226
+ {loop_count: 5, prelude: prelude}.merge(opts)
227
+ ) do |result, cache, options, keys|
228
+ expect_all_key_mappings_exist(cache, keys, false)
229
+ expect(cache.size).to eq sum(result)
230
+ end
231
+ end
232
+
233
+ def add_remove_via_compute_if_absent_present(opts = {})
234
+ prelude = 'do_add = rand(2) == 1'
235
+ code = <<-RUBY_EVAL
236
+ if do_add
237
+ cache.compute_if_absent(key) { acc += 1; key }
238
+ else
239
+ cache.compute_if_present(key) { acc -= 1; nil }
240
+ end
241
+ RUBY_EVAL
242
+ do_thread_loop(
243
+ __method__,
244
+ code,
245
+ {loop_count: 5, prelude: prelude}.merge(opts)
246
+ ) do |result, cache, options, keys|
247
+ expect_all_key_mappings_exist(cache, keys, false)
248
+ expect(cache.size).to eq sum(result)
249
+ end
250
+ end
251
+
252
+ def add_remove_indiscriminate(opts = {})
253
+ prelude = 'do_add = rand(2) == 1'
254
+ code = <<-RUBY_EVAL
255
+ if do_add
256
+ acc += 1 unless cache.put_if_absent(key, key)
257
+ else
258
+ acc -= 1 if cache.delete(key)
259
+ end
260
+ RUBY_EVAL
261
+ do_thread_loop(
262
+ __method__,
263
+ code,
264
+ {loop_count: 5, prelude: prelude}.merge(opts)
265
+ ) do |result, cache, options, keys|
266
+ expect_all_key_mappings_exist(cache, keys, false)
267
+ expect(cache.size).to eq sum(result)
268
+ end
269
+ end
270
+
271
+ def count_up(opts = {})
272
+ code = <<-RUBY_EVAL
273
+ v = cache[key]
274
+ acc += 1 if cache.replace_pair(key, v, v + 1)
275
+ RUBY_EVAL
276
+ do_thread_loop(
277
+ __method__,
278
+ code,
279
+ {loop_count: 5, cache_setup: ZERO_VALUE_CACHE_SETUP}.merge(opts)
280
+ ) do |result, cache, options, keys|
281
+ expect_count_up(result, cache, options, keys)
282
+ end
283
+ end
284
+
285
+ def count_up_via_compute(opts = {})
286
+ code = <<-RUBY_EVAL
287
+ cache.compute(key) do |old_value|
288
+ acc += 1
289
+ old_value ? old_value + 1 : 1
290
+ end
291
+ RUBY_EVAL
292
+ do_thread_loop(
293
+ __method__,
294
+ code, {loop_count: 5}.merge(opts)
295
+ ) do |result, cache, options, keys|
296
+ expect_count_up(result, cache, options, keys)
297
+ result.inject(nil) do |previous_value, next_value| # since compute guarantees atomicity all count ups should be equal
298
+ expect(previous_value).to eq next_value if previous_value
299
+ next_value
300
+ end
301
+ end
302
+ end
303
+
304
+ def count_up_via_merge_pair(opts = {})
305
+ code = <<-RUBY_EVAL
306
+ cache.merge_pair(key, 1) { |old_value| old_value + 1 }
307
+ RUBY_EVAL
308
+ do_thread_loop(
309
+ __method__,
310
+ code,
311
+ {loop_count: 5}.merge(opts)
312
+ ) do |result, cache, options, keys|
313
+ all_match = true
314
+ expected_value = options[:loop_count] * options[:thread_count]
315
+ keys.each do |key|
316
+ value = cache[key]
317
+ if expected_value != value
318
+ all_match = false
319
+ break
320
+ end
321
+ end
322
+ expect(all_match).to be_truthy
323
+ end
324
+ end
325
+
326
+ def add_remove_to_zero(opts = {})
327
+ code = <<-RUBY_EVAL
328
+ acc += 1 unless cache.put_if_absent(key, key)
329
+ acc -= 1 if cache.delete_pair(key, key)
330
+ RUBY_EVAL
331
+ do_thread_loop(
332
+ __method__,
333
+ code,
334
+ {loop_count: 5}.merge(opts)
335
+ ) do |result, cache, options, keys|
336
+ expect_all_key_mappings_exist(cache, keys, false)
337
+ expect(cache.size).to eq sum(result)
338
+ end
339
+ end
340
+
341
+ def add_remove_to_zero_via_merge_pair(opts = {})
342
+ code = <<-RUBY_EVAL
343
+ acc += (cache.merge_pair(key, key) {}) ? 1 : -1
344
+ RUBY_EVAL
345
+ do_thread_loop(
346
+ __method__,
347
+ code,
348
+ {loop_count: 5}.merge(opts)
349
+ ) do |result, cache, options, keys|
350
+ expect_all_key_mappings_exist(cache, keys, false)
351
+ expect(cache.size).to eq sum(result)
352
+ end
353
+ end
354
+
355
+ def do_thread_loop(name, code, options = {}, &block)
356
+ options = DEFAULTS.merge(options)
357
+ meth = define_loop(name, code, options[:prelude])
358
+ keys = to_keys_array(options[:key_count])
359
+ run_thread_loop(meth, keys, options, &block)
360
+
361
+ if options[:key_count] > 1
362
+ options[:key_count] = (options[:key_count] / 40).to_i
363
+ keys = to_hash_collision_keys_array(options[:key_count])
364
+ run_thread_loop(
365
+ meth,
366
+ keys,
367
+ options.merge(loop_count: options[:loop_count] * 5),
368
+ &block
369
+ )
370
+ end
371
+ end
372
+
373
+ def run_thread_loop(meth, keys, options, &block)
374
+ cache = options[:cache_setup].call(options, keys)
375
+ barrier = ThreadSafe::Test::Barrier.new(options[:thread_count])
376
+ result = (1..options[:thread_count]).map do
377
+ Thread.new do
378
+ setup_sync_and_start_loop(
379
+ meth,
380
+ cache,
381
+ keys,
382
+ barrier,
383
+ options[:loop_count]
384
+ )
385
+ end
386
+ end.map(&:value)
387
+ block.call(result, cache, options, keys) if block_given?
388
+ end
389
+
390
+ def setup_sync_and_start_loop(meth, cache, keys, barrier, loop_count)
391
+ my_keys = keys.shuffle
392
+ barrier.await
393
+ if my_keys.size == 1
394
+ key = my_keys.first
395
+ send("#{meth}_single_key", cache, key, loop_count)
396
+ else
397
+ send("#{meth}_multiple_keys", cache, my_keys, loop_count)
398
+ end
399
+ end
400
+
401
+ def define_loop(name, body, prelude)
402
+ inner_meth_name = :"_#{name}_loop_inner"
403
+ outer_meth_name = :"_#{name}_loop_outer"
404
+ # looping is splitted into the "loop methods" to trigger the JIT
405
+ self.class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
406
+ def #{inner_meth_name}_multiple_keys(cache, keys, i, length, acc)
407
+ #{prelude}
408
+ target = i + length
409
+ while i < target
410
+ key = keys[i]
411
+ #{body}
412
+ i += 1
413
+ end
414
+ acc
415
+ end unless method_defined?(:#{inner_meth_name}_multiple_keys)
416
+ RUBY_EVAL
417
+
418
+ self.class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
419
+ def #{inner_meth_name}_single_key(cache, key, i, length, acc)
420
+ #{prelude}
421
+ target = i + length
422
+ while i < target
423
+ #{body}
424
+ i += 1
425
+ end
426
+ acc
427
+ end unless method_defined?(:#{inner_meth_name}_single_key)
428
+ RUBY_EVAL
429
+
430
+ self.class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
431
+ def #{outer_meth_name}_multiple_keys(cache, keys, loop_count)
432
+ total_length = keys.size
433
+ acc = 0
434
+ inc = 100
435
+ loop_count.times do
436
+ i = 0
437
+ pre_loop_inc = total_length % inc
438
+ acc = #{inner_meth_name}_multiple_keys(cache, keys, i, pre_loop_inc, acc)
439
+ i += pre_loop_inc
440
+ while i < total_length
441
+ acc = #{inner_meth_name}_multiple_keys(cache, keys, i, inc, acc)
442
+ i += inc
443
+ end
444
+ end
445
+ acc
446
+ end unless method_defined?(:#{outer_meth_name}_multiple_keys)
447
+ RUBY_EVAL
448
+
449
+ self.class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
450
+ def #{outer_meth_name}_single_key(cache, key, loop_count)
451
+ acc = 0
452
+ i = 0
453
+ inc = 100
454
+
455
+ pre_loop_inc = loop_count % inc
456
+ acc = #{inner_meth_name}_single_key(cache, key, i, pre_loop_inc, acc)
457
+ i += pre_loop_inc
458
+
459
+ while i < loop_count
460
+ acc = #{inner_meth_name}_single_key(cache, key, i, inc, acc)
461
+ i += inc
462
+ end
463
+ acc
464
+ end unless method_defined?(:#{outer_meth_name}_single_key)
465
+ RUBY_EVAL
466
+ outer_meth_name
467
+ end
468
+
469
+ def to_keys_array(key_count)
470
+ arr = []
471
+ key_count.times {|i| arr << i}
472
+ arr
473
+ end
474
+
475
+ def to_hash_collision_keys_array(key_count)
476
+ to_keys_array(key_count).map { |key| ThreadSafe::Test::HashCollisionKey(key) }
477
+ end
478
+
479
+ def sum(result)
480
+ result.inject(0) { |acc, i| acc + i }
481
+ end
482
+
483
+ def expect_standard_accumulator_test_result(result, cache, options, keys)
484
+ expect_all_key_mappings_exist(cache, keys)
485
+ expect(options[:key_count]).to eq sum(result)
486
+ expect(options[:key_count]).to eq cache.size
487
+ end
488
+
489
+ def expect_all_key_mappings_exist(cache, keys, all_must_exist = true)
490
+ keys.each do |key|
491
+ value = cache[key]
492
+ if value || all_must_exist
493
+ expect(key).to eq value unless key == value # don't do a bazzilion assertions unless necessary
494
+ end
495
+ end
496
+ end
497
+
498
+ def expect_count_up(result, cache, options, keys)
499
+ keys.each do |key|
500
+ value = cache[key]
501
+ expect(value).to be_truthy unless value
502
+ end
503
+ expect(sum(cache.values)).to eq sum(result)
504
+ expect(options[:key_count]).to eq cache.size
505
+ end
506
+ end unless RUBY_ENGINE == 'rbx' || ENV['TRAVIS']
507
+ end