with_advisory_lock 4.6.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci-mysql5.yml +61 -0
- data/.github/workflows/ci-mysql8.yml +62 -0
- data/.github/workflows/ci-postgresql.yml +64 -0
- data/.github/workflows/ci-sqlite3.yml +54 -0
- data/.github/workflows/release.yml +17 -0
- data/.gitignore +2 -0
- data/.release-please-manifest.json +1 -0
- data/.ruby-version +2 -0
- data/.tool-versions +1 -1
- data/Appraisals +34 -18
- data/CHANGELOG.md +50 -0
- data/Gemfile +0 -12
- data/LICENSE.txt +4 -4
- data/Makefile +14 -0
- data/README.md +17 -6
- data/docker-compose.yml +20 -0
- data/gemfiles/{activerecord_6.0.gemfile → activerecord_6.1.gemfile} +4 -2
- data/gemfiles/{activerecord_5.2.gemfile → activerecord_7.0.gemfile} +4 -2
- data/gemfiles/activerecord_7.1.gemfile +14 -0
- data/lib/with_advisory_lock/base.rb +18 -4
- data/lib/with_advisory_lock/concern.rb +17 -17
- data/lib/with_advisory_lock/database_adapter_support.rb +7 -47
- data/lib/with_advisory_lock/failed_to_acquire_lock.rb +9 -0
- data/lib/with_advisory_lock/flock.rb +4 -3
- data/lib/with_advisory_lock/mysql.rb +12 -7
- data/lib/with_advisory_lock/postgresql.rb +43 -18
- data/lib/with_advisory_lock/version.rb +3 -1
- data/lib/with_advisory_lock.rb +9 -10
- data/release-please-config.json +9 -0
- data/test/test_helper.rb +66 -0
- data/test/test_models.rb +16 -10
- data/test/with_advisory_lock/base_test.rb +9 -0
- data/test/with_advisory_lock/concern_test.rb +33 -0
- data/test/with_advisory_lock/lock_test.rb +111 -0
- data/test/with_advisory_lock/nesting_test.rb +28 -0
- data/test/with_advisory_lock/options_test.rb +66 -0
- data/test/with_advisory_lock/parallelism_test.rb +75 -0
- data/test/with_advisory_lock/shared_test.rb +134 -0
- data/test/with_advisory_lock/thread_test.rb +61 -0
- data/test/with_advisory_lock/transaction_test.rb +68 -0
- data/with_advisory_lock.gemspec +29 -23
- metadata +56 -47
- data/.travis.yml +0 -38
- data/gemfiles/activerecord_4.2.gemfile +0 -19
- data/gemfiles/activerecord_5.0.gemfile +0 -19
- data/gemfiles/activerecord_5.1.gemfile +0 -19
- data/lib/with_advisory_lock/mysql_no_nesting.rb +0 -20
- data/lib/with_advisory_lock/nested_advisory_lock_error.rb +0 -14
- data/test/concern_test.rb +0 -20
- data/test/database.yml +0 -17
- data/test/lock_test.rb +0 -47
- data/test/minitest_helper.rb +0 -40
- data/test/nesting_test.rb +0 -93
- data/test/options_test.rb +0 -64
- data/test/parallelism_test.rb +0 -77
- data/test/shared_test.rb +0 -131
- data/test/thread_test.rb +0 -60
- data/test/transaction_test.rb +0 -70
- data/tests.sh +0 -11
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# This file was generated by Appraisal
|
|
2
|
-
|
|
3
|
-
source "https://rubygems.org"
|
|
4
|
-
|
|
5
|
-
gem "activerecord", "~> 4.2.0"
|
|
6
|
-
|
|
7
|
-
platforms :ruby do
|
|
8
|
-
gem "mysql2", "< 0.5"
|
|
9
|
-
gem "pg", "~> 0.21"
|
|
10
|
-
gem "sqlite3", "~> 1.3.6"
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
platforms :jruby do
|
|
14
|
-
gem "activerecord-jdbcmysql-adapter"
|
|
15
|
-
gem "activerecord-jdbcpostgresql-adapter"
|
|
16
|
-
gem "activerecord-jdbcsqlite3-adapter"
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
gemspec path: "../"
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# This file was generated by Appraisal
|
|
2
|
-
|
|
3
|
-
source "https://rubygems.org"
|
|
4
|
-
|
|
5
|
-
gem "activerecord", "~> 5.0.0"
|
|
6
|
-
|
|
7
|
-
platforms :ruby do
|
|
8
|
-
gem "mysql2"
|
|
9
|
-
gem "pg"
|
|
10
|
-
gem "sqlite3", "~> 1.3.6"
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
platforms :jruby do
|
|
14
|
-
gem "activerecord-jdbcmysql-adapter"
|
|
15
|
-
gem "activerecord-jdbcpostgresql-adapter"
|
|
16
|
-
gem "activerecord-jdbcsqlite3-adapter"
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
gemspec path: "../"
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# This file was generated by Appraisal
|
|
2
|
-
|
|
3
|
-
source "https://rubygems.org"
|
|
4
|
-
|
|
5
|
-
gem "activerecord", "~> 5.1.0"
|
|
6
|
-
|
|
7
|
-
platforms :ruby do
|
|
8
|
-
gem "mysql2"
|
|
9
|
-
gem "pg"
|
|
10
|
-
gem "sqlite3", "~> 1.3.6"
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
platforms :jruby do
|
|
14
|
-
gem "activerecord-jdbcmysql-adapter"
|
|
15
|
-
gem "activerecord-jdbcpostgresql-adapter"
|
|
16
|
-
gem "activerecord-jdbcsqlite3-adapter"
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
gemspec path: "../"
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
module WithAdvisoryLock
|
|
2
|
-
# For MySQL < 5.7.5 that does not support nested locks
|
|
3
|
-
class MySQLNoNesting < MySQL
|
|
4
|
-
# See http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock
|
|
5
|
-
def try_lock
|
|
6
|
-
unless lock_stack.empty?
|
|
7
|
-
raise NestedAdvisoryLockError.new(
|
|
8
|
-
"MySQL < 5.7.5 doesn't support nested Advisory Locks",
|
|
9
|
-
lock_stack.dup
|
|
10
|
-
)
|
|
11
|
-
end
|
|
12
|
-
super
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# MySQL doesn't support nested locks:
|
|
16
|
-
def already_locked?
|
|
17
|
-
lock_stack.last == lock_stack_item
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module WithAdvisoryLock
|
|
2
|
-
class NestedAdvisoryLockError < StandardError
|
|
3
|
-
attr_accessor :lock_stack
|
|
4
|
-
|
|
5
|
-
def initialize(msg = nil, lock_stack = nil)
|
|
6
|
-
super(msg)
|
|
7
|
-
@lock_stack = lock_stack
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def to_s
|
|
11
|
-
super + (lock_stack ? ": lock stack = #{lock_stack}" : '')
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
data/test/concern_test.rb
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
require 'minitest_helper'
|
|
2
|
-
|
|
3
|
-
describe "with_advisory_lock.concern" do
|
|
4
|
-
it "adds with_advisory_lock to ActiveRecord classes" do
|
|
5
|
-
assert Tag.respond_to?(:with_advisory_lock)
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
it "adds with_advisory_lock to ActiveRecord instances" do
|
|
9
|
-
assert Label.new.respond_to?(:with_advisory_lock)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
it "adds advisory_lock_exists? to ActiveRecord classes" do
|
|
13
|
-
assert Tag.respond_to?(:advisory_lock_exists?)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
it "adds advisory_lock_exists? to ActiveRecord classes" do
|
|
17
|
-
assert Label.new.respond_to?(:advisory_lock_exists?)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
end
|
data/test/database.yml
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
sqlite:
|
|
2
|
-
adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
|
|
3
|
-
database: test/sqlite.db
|
|
4
|
-
timeout: 500
|
|
5
|
-
pool: 50
|
|
6
|
-
postgresql:
|
|
7
|
-
adapter: postgresql
|
|
8
|
-
username: postgres
|
|
9
|
-
database: with_advisory_lock_test
|
|
10
|
-
min_messages: ERROR
|
|
11
|
-
pool: 50
|
|
12
|
-
mysql:
|
|
13
|
-
adapter: mysql2
|
|
14
|
-
host: localhost
|
|
15
|
-
username: root
|
|
16
|
-
database: with_advisory_lock_test
|
|
17
|
-
pool: 50
|
data/test/lock_test.rb
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
require 'minitest_helper'
|
|
2
|
-
|
|
3
|
-
describe 'class methods' do
|
|
4
|
-
let(:lock_name) { 'test lock' }
|
|
5
|
-
|
|
6
|
-
describe '.current_advisory_lock' do
|
|
7
|
-
it 'returns nil outside an advisory lock request' do
|
|
8
|
-
Tag.current_advisory_lock.must_be_nil
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
it 'returns the name of the last lock acquired' do
|
|
12
|
-
Tag.with_advisory_lock(lock_name) do
|
|
13
|
-
# The lock name may have a prefix if WITH_ADVISORY_LOCK_PREFIX env is set
|
|
14
|
-
Tag.current_advisory_lock.must_match(/#{lock_name}/)
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
it 'can obtain a lock with a name that attempts to disrupt a SQL comment' do
|
|
19
|
-
dangerous_lock_name = 'test */ lock /*'
|
|
20
|
-
Tag.with_advisory_lock(dangerous_lock_name) do
|
|
21
|
-
Tag.current_advisory_lock.must_match(/#{Regexp.escape(dangerous_lock_name)}/)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
describe '.advisory_lock_exists?' do
|
|
28
|
-
it 'returns false for an unacquired lock' do
|
|
29
|
-
Tag.advisory_lock_exists?(lock_name).must_be_false
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
it 'returns the name of the last lock acquired' do
|
|
33
|
-
Tag.with_advisory_lock(lock_name) do
|
|
34
|
-
Tag.advisory_lock_exists?(lock_name).must_be_true
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
describe 'zero timeout_seconds' do
|
|
40
|
-
it 'attempts the lock exactly once with no timeout' do
|
|
41
|
-
expected = SecureRandom.base64
|
|
42
|
-
Tag.with_advisory_lock(lock_name, 0) do
|
|
43
|
-
expected
|
|
44
|
-
end.must_equal expected
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
data/test/minitest_helper.rb
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
require 'erb'
|
|
2
|
-
require 'active_record'
|
|
3
|
-
require 'with_advisory_lock'
|
|
4
|
-
require 'tmpdir'
|
|
5
|
-
require 'securerandom'
|
|
6
|
-
|
|
7
|
-
def env_db
|
|
8
|
-
(ENV['DB'] || :mysql).to_sym
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
db_config = File.expand_path('database.yml', File.dirname(__FILE__))
|
|
12
|
-
ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(db_config)).result)
|
|
13
|
-
|
|
14
|
-
ENV['WITH_ADVISORY_LOCK_PREFIX'] ||= SecureRandom.hex
|
|
15
|
-
|
|
16
|
-
ActiveRecord::Base.establish_connection(env_db)
|
|
17
|
-
ActiveRecord::Migration.verbose = false
|
|
18
|
-
|
|
19
|
-
require 'test_models'
|
|
20
|
-
begin
|
|
21
|
-
require 'minitest'
|
|
22
|
-
rescue LoadError
|
|
23
|
-
puts 'Failed to load the minitest gem; built-in version will be used.'
|
|
24
|
-
end
|
|
25
|
-
require 'minitest/autorun'
|
|
26
|
-
require 'minitest/great_expectations'
|
|
27
|
-
require 'mocha/setup'
|
|
28
|
-
|
|
29
|
-
class MiniTest::Spec
|
|
30
|
-
before do
|
|
31
|
-
ENV['FLOCK_DIR'] = Dir.mktmpdir
|
|
32
|
-
Tag.delete_all
|
|
33
|
-
TagAudit.delete_all
|
|
34
|
-
Label.delete_all
|
|
35
|
-
end
|
|
36
|
-
after do
|
|
37
|
-
FileUtils.remove_entry_secure ENV['FLOCK_DIR']
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
data/test/nesting_test.rb
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
require 'minitest_helper'
|
|
2
|
-
|
|
3
|
-
describe "lock nesting" do
|
|
4
|
-
# This simplifies what we expect from the lock name:
|
|
5
|
-
before :each do
|
|
6
|
-
@prior_prefix = ENV['WITH_ADVISORY_LOCK_PREFIX']
|
|
7
|
-
ENV['WITH_ADVISORY_LOCK_PREFIX'] = nil
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
after :each do
|
|
11
|
-
ENV['WITH_ADVISORY_LOCK_PREFIX'] = @prior_prefix
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
it "doesn't request the same lock twice" do
|
|
15
|
-
impl = WithAdvisoryLock::Base.new(nil, nil, nil)
|
|
16
|
-
impl.lock_stack.must_be_empty
|
|
17
|
-
Tag.with_advisory_lock("first") do
|
|
18
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
|
19
|
-
# Even MySQL should be OK with this:
|
|
20
|
-
Tag.with_advisory_lock("first") do
|
|
21
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
|
22
|
-
end
|
|
23
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
|
24
|
-
end
|
|
25
|
-
impl.lock_stack.must_be_empty
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
it "raises errors with MySQL < 5.7.5 when acquiring nested lock" do
|
|
29
|
-
skip unless env_db == :mysql && ENV['MYSQL_VERSION'] != '5.7'
|
|
30
|
-
exc = proc {
|
|
31
|
-
Tag.with_advisory_lock("first") do
|
|
32
|
-
Tag.with_advisory_lock("second") do
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
}.must_raise WithAdvisoryLock::NestedAdvisoryLockError
|
|
36
|
-
exc.lock_stack.map(&:name).must_equal %w(first)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
it "does not raise errors with MySQL < 5.7.5 when acquiring nested error force enabled" do
|
|
40
|
-
skip unless env_db == :mysql && ENV['MYSQL_VERSION'] != '5.7'
|
|
41
|
-
impl = WithAdvisoryLock::Base.new(nil, nil, nil)
|
|
42
|
-
impl.lock_stack.must_be_empty
|
|
43
|
-
Tag.with_advisory_lock("first", force_nested_lock_support: true) do
|
|
44
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
|
45
|
-
Tag.with_advisory_lock("second", force_nested_lock_support: true) do
|
|
46
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
|
47
|
-
Tag.with_advisory_lock("first", force_nested_lock_support: true) do
|
|
48
|
-
# Shouldn't ask for another lock:
|
|
49
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
|
50
|
-
Tag.with_advisory_lock("second", force_nested_lock_support: true) do
|
|
51
|
-
# Shouldn't ask for another lock:
|
|
52
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
|
57
|
-
end
|
|
58
|
-
impl.lock_stack.must_be_empty
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
it "supports nested advisory locks with !MySQL 5.6" do
|
|
62
|
-
skip if env_db == :mysql && ENV['MYSQL_VERSION'] != '5.7'
|
|
63
|
-
impl = WithAdvisoryLock::Base.new(nil, nil, nil)
|
|
64
|
-
impl.lock_stack.must_be_empty
|
|
65
|
-
Tag.with_advisory_lock("first") do
|
|
66
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
|
67
|
-
Tag.with_advisory_lock("second") do
|
|
68
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
|
69
|
-
Tag.with_advisory_lock("first") do
|
|
70
|
-
# Shouldn't ask for another lock:
|
|
71
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
|
72
|
-
Tag.with_advisory_lock("second") do
|
|
73
|
-
# Shouldn't ask for another lock:
|
|
74
|
-
impl.lock_stack.map(&:name).must_equal %w(first second)
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
impl.lock_stack.map(&:name).must_equal %w(first)
|
|
79
|
-
end
|
|
80
|
-
impl.lock_stack.must_be_empty
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
it "raises with !MySQL 5.6 and nested error force disabled" do
|
|
84
|
-
skip unless env_db == :mysql && ENV['MYSQL_VERSION'] != '5.7'
|
|
85
|
-
exc = proc {
|
|
86
|
-
Tag.with_advisory_lock("first", force_nested_lock_support: false) do
|
|
87
|
-
Tag.with_advisory_lock("second", force_nested_lock_support: false) do
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
}.must_raise WithAdvisoryLock::NestedAdvisoryLockError
|
|
91
|
-
exc.lock_stack.map(&:name).must_equal %w(first)
|
|
92
|
-
end
|
|
93
|
-
end
|
data/test/options_test.rb
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
require 'minitest_helper'
|
|
2
|
-
|
|
3
|
-
describe 'options parsing' do
|
|
4
|
-
def parse_options(options)
|
|
5
|
-
WithAdvisoryLock::Base.new(mock, mock, options)
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
specify 'defaults (empty hash)' do
|
|
9
|
-
impl = parse_options({})
|
|
10
|
-
impl.timeout_seconds.must_be_nil
|
|
11
|
-
impl.shared.must_equal false
|
|
12
|
-
impl.transaction.must_equal false
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
specify 'nil sets timeout to nil' do
|
|
16
|
-
impl = parse_options(nil)
|
|
17
|
-
impl.timeout_seconds.must_be_nil
|
|
18
|
-
impl.shared.must_equal false
|
|
19
|
-
impl.transaction.must_equal false
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
specify 'integer sets timeout to value' do
|
|
23
|
-
impl = parse_options(42)
|
|
24
|
-
impl.timeout_seconds.must_equal 42
|
|
25
|
-
impl.shared.must_equal false
|
|
26
|
-
impl.transaction.must_equal false
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
specify 'hash with invalid key errors' do
|
|
30
|
-
proc {
|
|
31
|
-
parse_options(foo: 42)
|
|
32
|
-
}.must_raise ArgumentError
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
specify 'hash with timeout_seconds sets timeout to value' do
|
|
36
|
-
impl = parse_options(timeout_seconds: 123)
|
|
37
|
-
impl.timeout_seconds.must_equal 123
|
|
38
|
-
impl.shared.must_equal false
|
|
39
|
-
impl.transaction.must_equal false
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
specify 'hash with shared option sets shared to true' do
|
|
43
|
-
impl = parse_options(shared: true)
|
|
44
|
-
impl.timeout_seconds.must_be_nil
|
|
45
|
-
impl.shared.must_equal true
|
|
46
|
-
impl.transaction.must_equal false
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
specify 'hash with transaction option set transaction to true' do
|
|
50
|
-
impl = parse_options(transaction: true)
|
|
51
|
-
impl.timeout_seconds.must_be_nil
|
|
52
|
-
impl.shared.must_equal false
|
|
53
|
-
impl.transaction.must_equal true
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
specify 'hash with multiple keys sets options' do
|
|
57
|
-
foo = mock
|
|
58
|
-
bar = mock
|
|
59
|
-
impl = parse_options(timeout_seconds: foo, shared: bar)
|
|
60
|
-
impl.timeout_seconds.must_equal foo
|
|
61
|
-
impl.shared.must_equal bar
|
|
62
|
-
impl.transaction.must_equal false
|
|
63
|
-
end
|
|
64
|
-
end
|
data/test/parallelism_test.rb
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
require 'minitest_helper'
|
|
2
|
-
require 'forwardable'
|
|
3
|
-
|
|
4
|
-
describe 'parallelism' do
|
|
5
|
-
class FindOrCreateWorker
|
|
6
|
-
extend Forwardable
|
|
7
|
-
def_delegators :@thread, :join, :wakeup, :status, :to_s
|
|
8
|
-
|
|
9
|
-
def initialize(name, use_advisory_lock)
|
|
10
|
-
@name = name
|
|
11
|
-
@use_advisory_lock = use_advisory_lock
|
|
12
|
-
@thread = Thread.new { work_later }
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def work_later
|
|
16
|
-
sleep
|
|
17
|
-
ActiveRecord::Base.connection_pool.with_connection do
|
|
18
|
-
if @use_advisory_lock
|
|
19
|
-
Tag.with_advisory_lock(@name) { work }
|
|
20
|
-
else
|
|
21
|
-
work
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def work
|
|
27
|
-
Tag.transaction do
|
|
28
|
-
Tag.where(name: @name).first_or_create
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def run_workers
|
|
34
|
-
@names = @iterations.times.map { |iter| "iteration ##{iter}" }
|
|
35
|
-
@names.each do |name|
|
|
36
|
-
workers = @workers.times.map do
|
|
37
|
-
FindOrCreateWorker.new(name, @use_advisory_lock)
|
|
38
|
-
end
|
|
39
|
-
# Wait for all the threads to get ready:
|
|
40
|
-
until workers.all? { |ea| ea.status == 'sleep' }
|
|
41
|
-
sleep(0.1)
|
|
42
|
-
end
|
|
43
|
-
# OK, GO!
|
|
44
|
-
workers.each(&:wakeup)
|
|
45
|
-
# Then wait for them to finish:
|
|
46
|
-
workers.each(&:join)
|
|
47
|
-
end
|
|
48
|
-
# Ensure we're still connected:
|
|
49
|
-
ActiveRecord::Base.connection_pool.connection
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
before :each do
|
|
53
|
-
ActiveRecord::Base.connection.reconnect!
|
|
54
|
-
@workers = 10
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# < SQLite, understandably, throws "The database file is locked (database is locked)"
|
|
58
|
-
|
|
59
|
-
it 'creates multiple duplicate rows without advisory locks' do
|
|
60
|
-
skip if env_db == :sqlite
|
|
61
|
-
@use_advisory_lock = false
|
|
62
|
-
@iterations = 1
|
|
63
|
-
run_workers
|
|
64
|
-
Tag.all.size.must_be :>, @iterations # <- any duplicated rows will make me happy.
|
|
65
|
-
TagAudit.all.size.must_be :>, @iterations # <- any duplicated rows will make me happy.
|
|
66
|
-
Label.all.size.must_be :>, @iterations # <- any duplicated rows will make me happy.
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
it "doesn't create multiple duplicate rows with advisory locks" do
|
|
70
|
-
@use_advisory_lock = true
|
|
71
|
-
@iterations = 10
|
|
72
|
-
run_workers
|
|
73
|
-
Tag.all.size.must_equal @iterations # <- any duplicated rows will NOT make me happy.
|
|
74
|
-
TagAudit.all.size.must_equal @iterations # <- any duplicated rows will NOT make me happy.
|
|
75
|
-
Label.all.size.must_equal @iterations # <- any duplicated rows will NOT make me happy.
|
|
76
|
-
end
|
|
77
|
-
end
|
data/test/shared_test.rb
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
require 'minitest_helper'
|
|
2
|
-
|
|
3
|
-
describe 'shared locks' do
|
|
4
|
-
def supported?
|
|
5
|
-
env_db != :mysql
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
class SharedTestWorker
|
|
9
|
-
def initialize(shared)
|
|
10
|
-
@shared = shared
|
|
11
|
-
|
|
12
|
-
@locked = nil
|
|
13
|
-
@cleanup = false
|
|
14
|
-
@thread = Thread.new { work }
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def locked?
|
|
18
|
-
sleep 0.01 while @locked.nil? && @thread.alive?
|
|
19
|
-
@locked
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def cleanup!
|
|
23
|
-
@cleanup = true
|
|
24
|
-
@thread.join
|
|
25
|
-
raise if @thread.status.nil?
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def work
|
|
31
|
-
ActiveRecord::Base.connection_pool.with_connection do
|
|
32
|
-
Tag.with_advisory_lock('test', timeout_seconds: 0, shared: @shared) do
|
|
33
|
-
@locked = true
|
|
34
|
-
sleep 0.01 until @cleanup
|
|
35
|
-
end
|
|
36
|
-
@locked = false
|
|
37
|
-
sleep 0.01 until @cleanup
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
it 'does not allow two exclusive locks' do
|
|
43
|
-
one = SharedTestWorker.new(false)
|
|
44
|
-
one.locked?.must_equal true
|
|
45
|
-
|
|
46
|
-
two = SharedTestWorker.new(false)
|
|
47
|
-
two.locked?.must_equal false
|
|
48
|
-
|
|
49
|
-
one.cleanup!
|
|
50
|
-
two.cleanup!
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
describe 'not supported' do
|
|
54
|
-
before do
|
|
55
|
-
skip if supported?
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
it 'raises an error when attempting to use a shared lock' do
|
|
59
|
-
one = SharedTestWorker.new(true)
|
|
60
|
-
one.locked?.must_be_nil
|
|
61
|
-
exception = proc {
|
|
62
|
-
one.cleanup!
|
|
63
|
-
}.must_raise ArgumentError
|
|
64
|
-
exception.message.must_include 'not supported'
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
describe 'supported' do
|
|
69
|
-
before do
|
|
70
|
-
skip unless supported?
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
it 'does allow two shared locks' do
|
|
74
|
-
one = SharedTestWorker.new(true)
|
|
75
|
-
one.locked?.must_equal true
|
|
76
|
-
|
|
77
|
-
two = SharedTestWorker.new(true)
|
|
78
|
-
two.locked?.must_equal true
|
|
79
|
-
|
|
80
|
-
one.cleanup!
|
|
81
|
-
two.cleanup!
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
it 'does not allow exclusive lock with shared lock' do
|
|
85
|
-
one = SharedTestWorker.new(true)
|
|
86
|
-
one.locked?.must_equal true
|
|
87
|
-
|
|
88
|
-
two = SharedTestWorker.new(false)
|
|
89
|
-
two.locked?.must_equal false
|
|
90
|
-
|
|
91
|
-
three = SharedTestWorker.new(true)
|
|
92
|
-
three.locked?.must_equal true
|
|
93
|
-
|
|
94
|
-
one.cleanup!
|
|
95
|
-
two.cleanup!
|
|
96
|
-
three.cleanup!
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
it 'does not allow shared lock with exclusive lock' do
|
|
100
|
-
one = SharedTestWorker.new(false)
|
|
101
|
-
one.locked?.must_equal true
|
|
102
|
-
|
|
103
|
-
two = SharedTestWorker.new(true)
|
|
104
|
-
two.locked?.must_equal false
|
|
105
|
-
|
|
106
|
-
one.cleanup!
|
|
107
|
-
two.cleanup!
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
describe 'PostgreSQL' do
|
|
111
|
-
before do
|
|
112
|
-
skip unless env_db == :postgresql
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def pg_lock_modes
|
|
116
|
-
ActiveRecord::Base.connection.select_values("SELECT mode FROM pg_locks WHERE locktype = 'advisory';")
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
it 'allows shared lock to be upgraded to an exclusive lock' do
|
|
120
|
-
pg_lock_modes.must_equal %w[]
|
|
121
|
-
Tag.with_advisory_lock 'test', shared: true do
|
|
122
|
-
pg_lock_modes.must_equal %w[ShareLock]
|
|
123
|
-
Tag.with_advisory_lock 'test', shared: false do
|
|
124
|
-
pg_lock_modes.must_equal %w[ShareLock ExclusiveLock]
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
pg_lock_modes.must_equal %w[]
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|
data/test/thread_test.rb
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
require 'minitest_helper'
|
|
2
|
-
|
|
3
|
-
describe 'separate thread tests' do
|
|
4
|
-
let(:lock_name) { 'testing 1,2,3' } # OMG COMMAS
|
|
5
|
-
|
|
6
|
-
before do
|
|
7
|
-
@mutex = Mutex.new
|
|
8
|
-
@t1_acquired_lock = false
|
|
9
|
-
@t1_return_value = nil
|
|
10
|
-
|
|
11
|
-
@t1 = Thread.new do
|
|
12
|
-
ActiveRecord::Base.connection_pool.with_connection do
|
|
13
|
-
@t1_return_value = Label.with_advisory_lock(lock_name) do
|
|
14
|
-
@mutex.synchronize { @t1_acquired_lock = true }
|
|
15
|
-
sleep
|
|
16
|
-
't1 finished'
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Wait for the thread to acquire the lock:
|
|
22
|
-
until @mutex.synchronize { @t1_acquired_lock } do
|
|
23
|
-
sleep(0.1)
|
|
24
|
-
end
|
|
25
|
-
ActiveRecord::Base.connection.reconnect!
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
after do
|
|
29
|
-
@t1.wakeup if @t1.status == 'sleep'
|
|
30
|
-
@t1.join
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
it '#with_advisory_lock with a 0 timeout returns false immediately' do
|
|
34
|
-
response = Label.with_advisory_lock(lock_name, 0) do
|
|
35
|
-
fail 'should not be yielded to'
|
|
36
|
-
end
|
|
37
|
-
response.must_be_false
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
it '#with_advisory_lock yields to the provided block' do
|
|
41
|
-
@t1_acquired_lock.must_be_true
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
it '#advisory_lock_exists? returns true when another thread has the lock' do
|
|
45
|
-
Tag.advisory_lock_exists?(lock_name).must_be_true
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
it 'can re-establish the lock after the other thread releases it' do
|
|
49
|
-
@t1.wakeup
|
|
50
|
-
@t1.join
|
|
51
|
-
@t1_return_value.must_equal 't1 finished'
|
|
52
|
-
|
|
53
|
-
# We should now be able to acquire the lock immediately:
|
|
54
|
-
reacquired = false
|
|
55
|
-
Label.with_advisory_lock(lock_name, 0) do
|
|
56
|
-
reacquired = true
|
|
57
|
-
end.must_be_true
|
|
58
|
-
reacquired.must_be_true
|
|
59
|
-
end
|
|
60
|
-
end
|