thread_safe 0.3.5-java → 0.3.6-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fbd202dbf911be29e025cf656ec85fa07024805f
4
- data.tar.gz: ece8fcb51b53bea677a7de3bd3729b5a2345e394
3
+ metadata.gz: a56e33ebbf2000bf9b8ca0f20923d17823dc8fa7
4
+ data.tar.gz: f8c418f2b9b457bed5cd8afd13f02d139b54e175
5
5
  SHA512:
6
- metadata.gz: 8262d11575eda01b362dcf1661cfb023e1a6216fdffdfb0e4a47d4f48d0ea87a6d3950af9978d23dd9d85d524bbeef72f8b13b5c12db0bd930886bff2806c0d6
7
- data.tar.gz: b1ac9f0689d82854d827dc58009e519518cb57cc25bb3908465322739b9e3f92c6d51e43e5f54fb89dc30ac6efa4136e5884fa6aecabf0b3eb8801fffb283aed
6
+ metadata.gz: 548447d823cce2404e6b93f2b61388cfeb1faf6093c61527064d4f70653c7978ecf9d9610c81d8ba03da5c821a375b0b3b53dc88acc17b3b0a0034e4167d57ae
7
+ data.tar.gz: 9cc185a2ff3c32e3cba5c41fee624b6db6140da77d802fdca1fc72aeb380e9298dc7dc4c0a5f4a934fd0058c43923b02c1fc70dda193c55ac81a2eb082930245
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,10 +45,10 @@ 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
54
  task :package => [ :clean, :compile, :jar, 'test-jar' ]
@@ -55,7 +57,6 @@ else
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