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 +4 -4
- data/.rspec +2 -0
- data/.travis.yml +23 -20
- data/Gemfile +5 -2
- data/README.md +5 -1
- data/Rakefile +10 -9
- data/lib/thread_safe/cache.rb +3 -5
- data/lib/thread_safe/jruby_cache_backend.jar +0 -0
- data/lib/thread_safe/mri_cache_backend.rb +0 -9
- data/lib/thread_safe/synchronized_delegator.rb +0 -17
- data/lib/thread_safe/util/atomic_reference.rb +0 -1
- data/lib/thread_safe/version.rb +1 -1
- data/spec/.gitignore +0 -0
- data/spec/spec_helper.rb +31 -0
- data/{test → spec}/src/thread_safe/SecurityManager.java +0 -0
- data/spec/support/.gitignore +0 -0
- data/spec/support/threads.rb +1 -0
- data/{test/test_helper.rb → spec/support/threadsafe_test.rb} +0 -59
- data/spec/thread_safe/.gitignore +0 -0
- data/spec/thread_safe/array_spec.rb +18 -0
- data/spec/thread_safe/cache_loops_spec.rb +507 -0
- data/spec/thread_safe/cache_spec.rb +943 -0
- data/spec/thread_safe/hash_spec.rb +17 -0
- data/spec/thread_safe/no_unsafe_spec.rb +27 -0
- data/spec/thread_safe/synchronized_delegator_spec.rb +85 -0
- data/thread_safe.gemspec +4 -4
- metadata +54 -41
- data/test/test_array.rb +0 -18
- data/test/test_cache.rb +0 -901
- data/test/test_cache_loops.rb +0 -449
- data/test/test_hash.rb +0 -17
- data/test/test_synchronized_delegator.rb +0 -84
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a56e33ebbf2000bf9b8ca0f20923d17823dc8fa7
|
4
|
+
data.tar.gz: f8c418f2b9b457bed5cd8afd13f02d139b54e175
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 548447d823cce2404e6b93f2b61388cfeb1faf6093c61527064d4f70653c7978ecf9d9610c81d8ba03da5c821a375b0b3b53dc88acc17b3b0a0034e4167d57ae
|
7
|
+
data.tar.gz: 9cc185a2ff3c32e3cba5c41fee624b6db6140da77d802fdca1fc72aeb380e9298dc7dc4c0a5f4a934fd0058c43923b02c1fc70dda193c55ac81a2eb082930245
|
data/.rspec
ADDED
data/.travis.yml
CHANGED
@@ -1,43 +1,46 @@
|
|
1
|
+
dist: trusty
|
2
|
+
sudo: required
|
1
3
|
language: ruby
|
2
4
|
rvm:
|
3
|
-
- 2.
|
4
|
-
- 2.
|
5
|
-
- 2.
|
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.
|
12
|
+
- jruby-9.1.5.0
|
13
|
+
- jruby-1.7.26
|
10
14
|
- jruby-head
|
11
|
-
-
|
12
|
-
|
15
|
+
- rubinius-3
|
16
|
+
- rubinius-2
|
17
|
+
jdk:
|
13
18
|
- openjdk7
|
14
19
|
- oraclejdk8
|
15
20
|
matrix:
|
16
21
|
exclude:
|
17
|
-
- rvm: 2.
|
18
|
-
jdk: openjdk7
|
22
|
+
- rvm: 2.4.0
|
19
23
|
jdk: oraclejdk8
|
20
|
-
- rvm: 2.
|
21
|
-
jdk: openjdk7
|
24
|
+
- rvm: 2.3.3
|
22
25
|
jdk: oraclejdk8
|
23
|
-
- rvm: 2.
|
24
|
-
jdk:
|
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:
|
36
|
-
jdk:
|
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:
|
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 '
|
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
|
[](http://badge.fury.io/rb/thread_safe) [](https://travis-ci.org/ruby-concurrency/thread_safe) [](https://coveralls.io/r/ruby-concurrency/thread_safe) [](https://codeclimate.com/github/ruby-concurrency/thread_safe) [](https://gemnasium.com/ruby-concurrency/thread_safe) [](http://opensource.org/licenses/MIT) [](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
|
2
|
-
require
|
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
|
22
|
+
require 'ant'
|
21
23
|
|
22
|
-
directory
|
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 => '
|
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 => '
|
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
|
-
|
59
|
-
t.
|
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
|
data/lib/thread_safe/cache.rb
CHANGED
@@ -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
|
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?(
|
156
|
-
raise ArgumentError, ":initial_capacity must be a positive
|
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"
|
Binary file
|
@@ -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
|
data/lib/thread_safe/version.rb
CHANGED
data/spec/.gitignore
ADDED
File without changes
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
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
|