with_advisory_lock 4.6.0 → 5.0.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.yml +80 -0
- data/.gitignore +2 -0
- data/.tool-versions +1 -1
- data/Appraisals +34 -18
- data/CHANGELOG.md +9 -0
- data/Gemfile +0 -12
- data/README.md +17 -6
- 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 +16 -2
- data/lib/with_advisory_lock/concern.rb +16 -16
- data/lib/with_advisory_lock/database_adapter_support.rb +4 -41
- data/lib/with_advisory_lock/failed_to_acquire_lock.rb +7 -0
- data/lib/with_advisory_lock/flock.rb +4 -3
- data/lib/with_advisory_lock/mysql.rb +5 -5
- data/lib/with_advisory_lock/postgresql.rb +9 -7
- data/lib/with_advisory_lock/version.rb +3 -1
- data/lib/with_advisory_lock.rb +1 -2
- data/test/concern_test.rb +23 -10
- data/test/lock_test.rb +61 -28
- data/test/nesting_test.rb +14 -79
- data/test/options_test.rb +35 -33
- data/test/parallelism_test.rb +35 -37
- data/test/shared_test.rb +93 -90
- data/test/test_helper.rb +52 -0
- data/test/test_models.rb +9 -7
- data/test/thread_test.rb +23 -22
- data/test/transaction_test.rb +34 -36
- data/with_advisory_lock.gemspec +24 -23
- metadata +25 -41
- 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/database.yml +0 -17
- data/test/minitest_helper.rb +0 -40
- data/tests.sh +0 -11
data/test/lock_test.rb
CHANGED
@@ -1,47 +1,80 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
let(:lock_name) { 'test lock' }
|
3
|
+
require 'test_helper'
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
class LockTest < GemTestCase
|
6
|
+
setup do
|
7
|
+
@lock_name = 'test lock'
|
8
|
+
@return_val = 1900
|
9
|
+
end
|
10
|
+
|
11
|
+
test 'returns nil outside an advisory lock request' do
|
12
|
+
assert_nil(Tag.current_advisory_lock)
|
13
|
+
end
|
14
|
+
|
15
|
+
test 'returns the name of the last lock acquired' do
|
16
|
+
Tag.with_advisory_lock(@lock_name) do
|
17
|
+
assert_match(/#{@lock_name}/, Tag.current_advisory_lock)
|
9
18
|
end
|
19
|
+
end
|
10
20
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
21
|
+
test 'can obtain a lock with a name that attempts to disrupt a SQL comment' do
|
22
|
+
dangerous_lock_name = 'test */ lock /*'
|
23
|
+
Tag.with_advisory_lock(dangerous_lock_name) do
|
24
|
+
assert_match(/#{Regexp.escape(dangerous_lock_name)}/, Tag.current_advisory_lock)
|
16
25
|
end
|
26
|
+
end
|
17
27
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
Tag.current_advisory_lock.must_match(/#{Regexp.escape(dangerous_lock_name)}/)
|
22
|
-
end
|
28
|
+
test 'returns false for an unacquired lock' do
|
29
|
+
refute(Tag.advisory_lock_exists?(@lock_name))
|
30
|
+
end
|
23
31
|
|
32
|
+
test 'returns true for an acquired lock' do
|
33
|
+
Tag.with_advisory_lock(@lock_name) do
|
34
|
+
assert(Tag.advisory_lock_exists?(@lock_name))
|
24
35
|
end
|
25
36
|
end
|
26
37
|
|
27
|
-
|
28
|
-
|
29
|
-
|
38
|
+
test 'returns block return value if lock successful' do
|
39
|
+
assert_equal(@return_val, Tag.with_advisory_lock!(@lock_name) { @return_val })
|
40
|
+
end
|
41
|
+
|
42
|
+
test 'returns false on lock acquisition failure' do
|
43
|
+
thread_with_lock = Thread.new do
|
44
|
+
Tag.with_advisory_lock(@lock_name, timeout_seconds: 0) do
|
45
|
+
@locked_elsewhere = true
|
46
|
+
loop { sleep 0.01 }
|
47
|
+
end
|
30
48
|
end
|
31
49
|
|
32
|
-
|
33
|
-
|
34
|
-
|
50
|
+
sleep 0.01 until @locked_elsewhere
|
51
|
+
assert_not(Tag.with_advisory_lock(@lock_name, timeout_seconds: 0) { @return_val })
|
52
|
+
|
53
|
+
thread_with_lock.kill
|
54
|
+
end
|
55
|
+
|
56
|
+
test 'raises an error on lock acquisition failure' do
|
57
|
+
thread_with_lock = Thread.new do
|
58
|
+
Tag.with_advisory_lock(@lock_name, timeout_seconds: 0) do
|
59
|
+
@locked_elsewhere = true
|
60
|
+
loop { sleep 0.01 }
|
35
61
|
end
|
36
62
|
end
|
63
|
+
|
64
|
+
sleep 0.01 until @locked_elsewhere
|
65
|
+
assert_raises(WithAdvisoryLock::FailedToAcquireLock) do
|
66
|
+
Tag.with_advisory_lock!(@lock_name, timeout_seconds: 0) { @return_val }
|
67
|
+
end
|
68
|
+
|
69
|
+
thread_with_lock.kill
|
37
70
|
end
|
38
71
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
expected
|
44
|
-
end.must_equal expected
|
72
|
+
test 'attempts the lock exactly once with no timeout' do
|
73
|
+
expected = SecureRandom.base64
|
74
|
+
actual = Tag.with_advisory_lock(@lock_name, 0) do
|
75
|
+
expected
|
45
76
|
end
|
77
|
+
|
78
|
+
assert_equal(expected, actual)
|
46
79
|
end
|
47
80
|
end
|
data/test/nesting_test.rb
CHANGED
@@ -1,93 +1,28 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class LockNestingTest < GemTestCase
|
6
|
+
setup do
|
6
7
|
@prior_prefix = ENV['WITH_ADVISORY_LOCK_PREFIX']
|
7
8
|
ENV['WITH_ADVISORY_LOCK_PREFIX'] = nil
|
8
9
|
end
|
9
10
|
|
10
|
-
|
11
|
+
teardown do
|
11
12
|
ENV['WITH_ADVISORY_LOCK_PREFIX'] = @prior_prefix
|
12
13
|
end
|
13
14
|
|
14
|
-
|
15
|
+
test "doesn't request the same lock twice" do
|
15
16
|
impl = WithAdvisoryLock::Base.new(nil, nil, nil)
|
16
|
-
impl.lock_stack
|
17
|
-
Tag.with_advisory_lock(
|
18
|
-
impl.lock_stack.map(&:name)
|
17
|
+
assert_empty(impl.lock_stack)
|
18
|
+
Tag.with_advisory_lock('first') do
|
19
|
+
assert_equal(%w[first], impl.lock_stack.map(&:name))
|
19
20
|
# Even MySQL should be OK with this:
|
20
|
-
Tag.with_advisory_lock(
|
21
|
-
impl.lock_stack.map(&:name)
|
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
|
21
|
+
Tag.with_advisory_lock('first') do
|
22
|
+
assert_equal(%w[first], impl.lock_stack.map(&:name))
|
55
23
|
end
|
56
|
-
impl.lock_stack.map(&:name)
|
24
|
+
assert_equal(%w[first], impl.lock_stack.map(&:name))
|
57
25
|
end
|
58
|
-
impl.lock_stack
|
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)
|
26
|
+
assert_empty(impl.lock_stack)
|
92
27
|
end
|
93
28
|
end
|
data/test/options_test.rb
CHANGED
@@ -1,64 +1,66 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class OptionsParsingTest < GemTestCase
|
4
6
|
def parse_options(options)
|
5
7
|
WithAdvisoryLock::Base.new(mock, mock, options)
|
6
8
|
end
|
7
9
|
|
8
|
-
|
10
|
+
test 'defaults (empty hash)' do
|
9
11
|
impl = parse_options({})
|
10
|
-
impl.timeout_seconds
|
11
|
-
impl.shared
|
12
|
-
impl.transaction
|
12
|
+
assert_nil(impl.timeout_seconds)
|
13
|
+
assert_not(impl.shared)
|
14
|
+
assert_not(impl.transaction)
|
13
15
|
end
|
14
16
|
|
15
|
-
|
17
|
+
test 'nil sets timeout to nil' do
|
16
18
|
impl = parse_options(nil)
|
17
|
-
impl.timeout_seconds
|
18
|
-
impl.shared
|
19
|
-
impl.transaction
|
19
|
+
assert_nil(impl.timeout_seconds)
|
20
|
+
assert_not(impl.shared)
|
21
|
+
assert_not(impl.transaction)
|
20
22
|
end
|
21
23
|
|
22
|
-
|
24
|
+
test 'integer sets timeout to value' do
|
23
25
|
impl = parse_options(42)
|
24
|
-
impl.timeout_seconds
|
25
|
-
impl.shared
|
26
|
-
impl.transaction
|
26
|
+
assert_equal(42, impl.timeout_seconds)
|
27
|
+
assert_not(impl.shared)
|
28
|
+
assert_not(impl.transaction)
|
27
29
|
end
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
+
test 'hash with invalid key errors' do
|
32
|
+
assert_raises(ArgumentError) do
|
31
33
|
parse_options(foo: 42)
|
32
|
-
|
34
|
+
end
|
33
35
|
end
|
34
36
|
|
35
|
-
|
37
|
+
test 'hash with timeout_seconds sets timeout to value' do
|
36
38
|
impl = parse_options(timeout_seconds: 123)
|
37
|
-
impl.timeout_seconds
|
38
|
-
impl.shared
|
39
|
-
impl.transaction
|
39
|
+
assert_equal(123, impl.timeout_seconds)
|
40
|
+
assert_not(impl.shared)
|
41
|
+
assert_not(impl.transaction)
|
40
42
|
end
|
41
43
|
|
42
|
-
|
44
|
+
test 'hash with shared option sets shared to true' do
|
43
45
|
impl = parse_options(shared: true)
|
44
|
-
impl.timeout_seconds
|
45
|
-
impl.shared
|
46
|
-
impl.transaction
|
46
|
+
assert_nil(impl.timeout_seconds)
|
47
|
+
assert(impl.shared)
|
48
|
+
assert_not(impl.transaction)
|
47
49
|
end
|
48
50
|
|
49
|
-
|
51
|
+
test 'hash with transaction option set transaction to true' do
|
50
52
|
impl = parse_options(transaction: true)
|
51
|
-
impl.timeout_seconds
|
52
|
-
impl.shared
|
53
|
-
impl.transaction
|
53
|
+
assert_nil(impl.timeout_seconds)
|
54
|
+
assert_not(impl.shared)
|
55
|
+
assert(impl.transaction)
|
54
56
|
end
|
55
57
|
|
56
|
-
|
58
|
+
test 'hash with multiple keys sets options' do
|
57
59
|
foo = mock
|
58
60
|
bar = mock
|
59
61
|
impl = parse_options(timeout_seconds: foo, shared: bar)
|
60
|
-
impl.timeout_seconds
|
61
|
-
impl.shared
|
62
|
-
impl.transaction
|
62
|
+
assert_equal(foo, impl.timeout_seconds)
|
63
|
+
assert_equal(bar, impl.shared)
|
64
|
+
assert_not(impl.transaction)
|
63
65
|
end
|
64
66
|
end
|
data/test/parallelism_test.rb
CHANGED
@@ -1,35 +1,37 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
2
4
|
require 'forwardable'
|
3
5
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
def_delegators :@thread, :join, :wakeup, :status, :to_s
|
6
|
+
class FindOrCreateWorker
|
7
|
+
extend Forwardable
|
8
|
+
def_delegators :@thread, :join, :wakeup, :status, :to_s
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
def initialize(name, use_advisory_lock)
|
11
|
+
@name = name
|
12
|
+
@use_advisory_lock = use_advisory_lock
|
13
|
+
@thread = Thread.new { work_later }
|
14
|
+
end
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
16
|
+
def work_later
|
17
|
+
sleep
|
18
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
19
|
+
if @use_advisory_lock
|
20
|
+
Tag.with_advisory_lock(@name) { work }
|
21
|
+
else
|
22
|
+
work
|
23
23
|
end
|
24
24
|
end
|
25
|
+
end
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
27
|
+
def work
|
28
|
+
Tag.transaction do
|
29
|
+
Tag.where(name: @name).first_or_create
|
30
30
|
end
|
31
31
|
end
|
32
|
+
end
|
32
33
|
|
34
|
+
class ParallelismTest < GemTestCase
|
33
35
|
def run_workers
|
34
36
|
@names = @iterations.times.map { |iter| "iteration ##{iter}" }
|
35
37
|
@names.each do |name|
|
@@ -37,9 +39,7 @@ describe 'parallelism' do
|
|
37
39
|
FindOrCreateWorker.new(name, @use_advisory_lock)
|
38
40
|
end
|
39
41
|
# Wait for all the threads to get ready:
|
40
|
-
until workers.all? { |ea| ea.status == 'sleep' }
|
41
|
-
sleep(0.1)
|
42
|
-
end
|
42
|
+
sleep(0.1) until workers.all? { |ea| ea.status == 'sleep' }
|
43
43
|
# OK, GO!
|
44
44
|
workers.each(&:wakeup)
|
45
45
|
# Then wait for them to finish:
|
@@ -49,29 +49,27 @@ describe 'parallelism' do
|
|
49
49
|
ActiveRecord::Base.connection_pool.connection
|
50
50
|
end
|
51
51
|
|
52
|
-
|
52
|
+
setup do
|
53
53
|
ActiveRecord::Base.connection.reconnect!
|
54
54
|
@workers = 10
|
55
55
|
end
|
56
56
|
|
57
|
-
|
58
|
-
|
59
|
-
it 'creates multiple duplicate rows without advisory locks' do
|
60
|
-
skip if env_db == :sqlite
|
57
|
+
test 'creates multiple duplicate rows without advisory locks' do
|
58
|
+
skip if %i[sqlite3 jdbcsqlite3].include?(env_db)
|
61
59
|
@use_advisory_lock = false
|
62
60
|
@iterations = 1
|
63
61
|
run_workers
|
64
|
-
Tag.all.size
|
65
|
-
TagAudit.all.size
|
66
|
-
Label.all.size
|
62
|
+
assert_operator(Tag.all.size, :>, @iterations) # <- any duplicated rows will make me happy.
|
63
|
+
assert_operator(TagAudit.all.size, :>, @iterations) # <- any duplicated rows will make me happy.
|
64
|
+
assert_operator(Label.all.size, :>, @iterations) # <- any duplicated rows will make me happy.
|
67
65
|
end
|
68
66
|
|
69
|
-
|
67
|
+
test "doesn't create multiple duplicate rows with advisory locks" do
|
70
68
|
@use_advisory_lock = true
|
71
69
|
@iterations = 10
|
72
70
|
run_workers
|
73
|
-
Tag.all.size
|
74
|
-
TagAudit.all.size
|
75
|
-
Label.all.size
|
71
|
+
assert_equal(@iterations, Tag.all.size) # <- any duplicated rows will NOT make me happy.
|
72
|
+
assert_equal(@iterations, TagAudit.all.size) # <- any duplicated rows will NOT make me happy.
|
73
|
+
assert_equal(@iterations, Label.all.size) # <- any duplicated rows will NOT make me happy.
|
76
74
|
end
|
77
75
|
end
|
data/test/shared_test.rb
CHANGED
@@ -1,131 +1,134 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
class SharedTestWorker
|
9
|
-
def initialize(shared)
|
10
|
-
@shared = shared
|
3
|
+
require 'test_helper'
|
4
|
+
class SharedTestWorker
|
5
|
+
def initialize(shared)
|
6
|
+
@shared = shared
|
11
7
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
8
|
+
@locked = nil
|
9
|
+
@cleanup = false
|
10
|
+
@thread = Thread.new { work }
|
11
|
+
end
|
16
12
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
13
|
+
def locked?
|
14
|
+
sleep 0.01 while @locked.nil? && @thread.alive?
|
15
|
+
@locked
|
16
|
+
end
|
21
17
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
18
|
+
def cleanup!
|
19
|
+
@cleanup = true
|
20
|
+
@thread.join
|
21
|
+
raise if @thread.status.nil?
|
22
|
+
end
|
27
23
|
|
28
|
-
|
24
|
+
private
|
29
25
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
sleep 0.01 until @cleanup
|
35
|
-
end
|
36
|
-
@locked = false
|
26
|
+
def work
|
27
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
28
|
+
Tag.with_advisory_lock('test', timeout_seconds: 0, shared: @shared) do
|
29
|
+
@locked = true
|
37
30
|
sleep 0.01 until @cleanup
|
38
31
|
end
|
32
|
+
@locked = false
|
33
|
+
sleep 0.01 until @cleanup
|
39
34
|
end
|
40
35
|
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class SharedLocksTest < GemTestCase
|
39
|
+
def supported?
|
40
|
+
%i[trilogy mysql2 jdbcmysql].exclude?(env_db)
|
41
|
+
end
|
41
42
|
|
42
|
-
|
43
|
+
test 'does not allow two exclusive locks' do
|
43
44
|
one = SharedTestWorker.new(false)
|
44
|
-
one
|
45
|
+
assert_predicate(one, :locked?)
|
45
46
|
|
46
47
|
two = SharedTestWorker.new(false)
|
47
|
-
two.locked
|
48
|
+
refute(two.locked?)
|
48
49
|
|
49
50
|
one.cleanup!
|
50
51
|
two.cleanup!
|
51
52
|
end
|
53
|
+
end
|
52
54
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
55
|
+
class NotSupportedEnvironmentTest < SharedLocksTest
|
56
|
+
setup do
|
57
|
+
skip if supported?
|
58
|
+
end
|
57
59
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
exception.message.must_include 'not supported'
|
60
|
+
test 'raises an error when attempting to use a shared lock' do
|
61
|
+
one = SharedTestWorker.new(true)
|
62
|
+
assert_nil(one.locked?)
|
63
|
+
|
64
|
+
exception = assert_raises(ArgumentError) do
|
65
|
+
one.cleanup!
|
65
66
|
end
|
67
|
+
|
68
|
+
assert_match(/#{Regexp.escape('not supported')}/, exception.message)
|
66
69
|
end
|
70
|
+
end
|
67
71
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
+
class SupportedEnvironmentTest < SharedLocksTest
|
73
|
+
setup do
|
74
|
+
skip unless supported?
|
75
|
+
end
|
72
76
|
|
73
|
-
|
74
|
-
|
75
|
-
|
77
|
+
test 'does allow two shared locks' do
|
78
|
+
one = SharedTestWorker.new(true)
|
79
|
+
assert_predicate(one, :locked?)
|
76
80
|
|
77
|
-
|
78
|
-
|
81
|
+
two = SharedTestWorker.new(true)
|
82
|
+
assert_predicate(two, :locked?)
|
79
83
|
|
80
|
-
|
81
|
-
|
82
|
-
|
84
|
+
one.cleanup!
|
85
|
+
two.cleanup!
|
86
|
+
end
|
83
87
|
|
84
|
-
|
85
|
-
|
86
|
-
|
88
|
+
test 'does not allow exclusive lock with shared lock' do
|
89
|
+
one = SharedTestWorker.new(true)
|
90
|
+
assert_predicate(one, :locked?)
|
87
91
|
|
88
|
-
|
89
|
-
|
92
|
+
two = SharedTestWorker.new(false)
|
93
|
+
refute(two.locked?)
|
90
94
|
|
91
|
-
|
92
|
-
|
95
|
+
three = SharedTestWorker.new(true)
|
96
|
+
assert_predicate(three, :locked?)
|
93
97
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
+
one.cleanup!
|
99
|
+
two.cleanup!
|
100
|
+
three.cleanup!
|
101
|
+
end
|
98
102
|
|
99
|
-
|
100
|
-
|
101
|
-
|
103
|
+
test 'does not allow shared lock with exclusive lock' do
|
104
|
+
one = SharedTestWorker.new(false)
|
105
|
+
assert_predicate(one, :locked?)
|
102
106
|
|
103
|
-
|
104
|
-
|
107
|
+
two = SharedTestWorker.new(true)
|
108
|
+
refute(two.locked?)
|
105
109
|
|
106
|
-
|
107
|
-
|
108
|
-
|
110
|
+
one.cleanup!
|
111
|
+
two.cleanup!
|
112
|
+
end
|
109
113
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
+
class PostgreSQLTest < SupportedEnvironmentTest
|
115
|
+
setup do
|
116
|
+
skip unless env_db == :postgresql
|
117
|
+
end
|
114
118
|
|
115
|
-
|
116
|
-
|
117
|
-
|
119
|
+
def pg_lock_modes
|
120
|
+
ActiveRecord::Base.connection.select_values("SELECT mode FROM pg_locks WHERE locktype = 'advisory';")
|
121
|
+
end
|
118
122
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
end
|
123
|
+
test 'allows shared lock to be upgraded to an exclusive lock' do
|
124
|
+
assert_empty(pg_lock_modes)
|
125
|
+
Tag.with_advisory_lock 'test', shared: true do
|
126
|
+
assert_equal(%w[ShareLock], pg_lock_modes)
|
127
|
+
Tag.with_advisory_lock 'test', shared: false do
|
128
|
+
assert_equal(%w[ShareLock ExclusiveLock], pg_lock_modes)
|
126
129
|
end
|
127
|
-
pg_lock_modes.must_equal %w[]
|
128
130
|
end
|
131
|
+
assert_empty(pg_lock_modes)
|
129
132
|
end
|
130
133
|
end
|
131
134
|
end
|