with_advisory_lock 5.3.0 → 7.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 +76 -0
- data/.gitignore +2 -2
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +32 -0
- data/Gemfile +31 -0
- data/Makefile +8 -12
- data/README.md +7 -35
- data/Rakefile +5 -2
- data/bin/console +11 -0
- data/bin/rails +15 -0
- data/bin/sanity +20 -0
- data/bin/sanity_check +86 -0
- data/bin/setup +8 -0
- data/bin/setup_test_db +59 -0
- data/bin/test_connections +22 -0
- data/docker-compose.yml +3 -4
- data/lib/with_advisory_lock/concern.rb +26 -19
- data/lib/with_advisory_lock/core_advisory.rb +110 -0
- data/lib/with_advisory_lock/jruby_adapter.rb +29 -0
- data/lib/with_advisory_lock/lock_stack_item.rb +6 -0
- data/lib/with_advisory_lock/mysql_advisory.rb +62 -0
- data/lib/with_advisory_lock/postgresql_advisory.rb +112 -0
- data/lib/with_advisory_lock/result.rb +14 -0
- data/lib/with_advisory_lock/version.rb +1 -1
- data/lib/with_advisory_lock.rb +38 -10
- data/test/dummy/Rakefile +8 -0
- data/test/dummy/app/controllers/application_controller.rb +7 -0
- data/test/dummy/app/models/application_record.rb +6 -0
- data/test/dummy/app/models/label.rb +4 -0
- data/test/dummy/app/models/mysql_label.rb +5 -0
- data/test/dummy/app/models/mysql_record.rb +6 -0
- data/test/dummy/app/models/mysql_tag.rb +10 -0
- data/test/dummy/app/models/mysql_tag_audit.rb +5 -0
- data/test/dummy/app/models/tag.rb +8 -0
- data/test/dummy/app/models/tag_audit.rb +4 -0
- data/test/dummy/config/application.rb +31 -0
- data/test/dummy/config/boot.rb +3 -0
- data/test/dummy/config/database.yml +13 -0
- data/test/dummy/config/environment.rb +7 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config.ru +6 -0
- data/test/{test_models.rb → dummy/db/schema.rb} +2 -17
- data/test/dummy/db/secondary_schema.rb +15 -0
- data/test/dummy/lib/tasks/db.rake +40 -0
- data/test/sanity_check_test.rb +63 -0
- data/test/test_helper.rb +14 -47
- data/test/with_advisory_lock/concern_test.rb +58 -12
- data/test/with_advisory_lock/lock_test.rb +159 -73
- data/test/with_advisory_lock/multi_adapter_test.rb +17 -0
- data/test/with_advisory_lock/parallelism_test.rb +63 -37
- data/test/with_advisory_lock/postgresql_race_condition_test.rb +118 -0
- data/test/with_advisory_lock/shared_test.rb +52 -57
- data/test/with_advisory_lock/thread_test.rb +64 -42
- data/test/with_advisory_lock/transaction_test.rb +55 -40
- data/with_advisory_lock.gemspec +25 -5
- metadata +54 -50
- data/.github/workflows/ci-mysql5.yml +0 -61
- data/.github/workflows/ci-mysql8.yml +0 -62
- data/.github/workflows/ci-postgresql.yml +0 -64
- data/.github/workflows/ci-sqlite3.yml +0 -54
- data/Appraisals +0 -45
- data/gemfiles/activerecord_6.1.gemfile +0 -21
- data/gemfiles/activerecord_7.0.gemfile +0 -21
- data/gemfiles/activerecord_7.1.gemfile +0 -14
- data/lib/with_advisory_lock/base.rb +0 -118
- data/lib/with_advisory_lock/database_adapter_support.rb +0 -23
- data/lib/with_advisory_lock/flock.rb +0 -33
- data/lib/with_advisory_lock/mysql.rb +0 -32
- data/lib/with_advisory_lock/postgresql.rb +0 -66
- data/test/with_advisory_lock/base_test.rb +0 -9
- data/test/with_advisory_lock/nesting_test.rb +0 -28
- data/test/with_advisory_lock/options_test.rb +0 -66
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'concurrent'
|
5
|
+
|
6
|
+
class PostgreSQLRaceConditionTest < GemTestCase
|
7
|
+
self.use_transactional_tests = false
|
8
|
+
|
9
|
+
def model_class
|
10
|
+
Tag
|
11
|
+
end
|
12
|
+
|
13
|
+
setup do
|
14
|
+
@lock_name = 'race_condition_test'
|
15
|
+
end
|
16
|
+
|
17
|
+
test 'advisory_lock_exists? does not create false positives in multi-threaded environment' do
|
18
|
+
# Ensure no lock exists initially
|
19
|
+
assert_not model_class.advisory_lock_exists?(@lock_name)
|
20
|
+
|
21
|
+
results = Concurrent::Array.new
|
22
|
+
|
23
|
+
# Create a thread pool with multiple workers checking simultaneously
|
24
|
+
# This would previously cause race conditions where threads would falsely
|
25
|
+
# report the lock exists due to another thread's existence check
|
26
|
+
pool = Concurrent::FixedThreadPool.new(20)
|
27
|
+
promises = 20.times.map do
|
28
|
+
Concurrent::Promise.execute(executor: pool) do
|
29
|
+
model_class.connection_pool.with_connection do
|
30
|
+
# Each thread checks multiple times to increase chance of race condition
|
31
|
+
5.times do
|
32
|
+
result = model_class.advisory_lock_exists?(@lock_name)
|
33
|
+
results << result
|
34
|
+
sleep(0.001) # Small delay to encourage interleaving
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Wait for all promises to complete
|
41
|
+
Concurrent::Promise.zip(*promises).wait!
|
42
|
+
pool.shutdown
|
43
|
+
pool.wait_for_termination
|
44
|
+
|
45
|
+
# All checks should report false since no lock was ever acquired
|
46
|
+
assert results.all? { |r| r == false },
|
47
|
+
"Race condition detected: #{results.count(true)} false positives out of #{results.size} checks"
|
48
|
+
end
|
49
|
+
|
50
|
+
test 'advisory_lock_exists? correctly detects when lock is held by another connection' do
|
51
|
+
lock_acquired = Concurrent::AtomicBoolean.new(false)
|
52
|
+
lock_released = Concurrent::AtomicBoolean.new(false)
|
53
|
+
|
54
|
+
# Promise 1: Acquire and hold the lock
|
55
|
+
holder_promise = Concurrent::Promise.execute do
|
56
|
+
model_class.connection_pool.with_connection do
|
57
|
+
model_class.with_advisory_lock(@lock_name) do
|
58
|
+
lock_acquired.make_true
|
59
|
+
|
60
|
+
# Wait until we've confirmed the lock is detected
|
61
|
+
sleep(0.01) until lock_released.true?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Wait for lock to be acquired
|
67
|
+
sleep(0.01) until lock_acquired.true?
|
68
|
+
|
69
|
+
# Promise 2: Check if lock exists (should be true)
|
70
|
+
checker_promise = Concurrent::Promise.execute do
|
71
|
+
model_class.connection_pool.with_connection do
|
72
|
+
# Check multiple times to ensure consistency
|
73
|
+
10.times do
|
74
|
+
assert model_class.advisory_lock_exists?(@lock_name),
|
75
|
+
'Failed to detect existing lock'
|
76
|
+
sleep(0.01)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Let the checker run
|
82
|
+
checker_promise.wait!
|
83
|
+
|
84
|
+
# Release the lock
|
85
|
+
lock_released.make_true
|
86
|
+
holder_promise.wait!
|
87
|
+
|
88
|
+
# Verify lock is released
|
89
|
+
assert_not model_class.advisory_lock_exists?(@lock_name)
|
90
|
+
end
|
91
|
+
|
92
|
+
test 'new non-blocking implementation is being used for PostgreSQL' do
|
93
|
+
# This test verifies that our new implementation is actually being called
|
94
|
+
# We can check this by looking at whether the connection responds to our new method
|
95
|
+
model_class.connection_pool.with_connection do |conn|
|
96
|
+
assert conn.respond_to?(:advisory_lock_exists_for?),
|
97
|
+
'PostgreSQL connection should have advisory_lock_exists_for? method'
|
98
|
+
|
99
|
+
# Test the method directly
|
100
|
+
conn.lock_keys_for(@lock_name)
|
101
|
+
result = conn.advisory_lock_exists_for?(@lock_name)
|
102
|
+
assert_not_nil result, 'advisory_lock_exists_for? should return true/false, not nil'
|
103
|
+
assert [true, false].include?(result), 'advisory_lock_exists_for? should return boolean'
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
test 'fallback works if pg_locks access fails' do
|
108
|
+
# Test that the system gracefully falls back to the old implementation
|
109
|
+
# if pg_locks query fails (e.g., due to permissions)
|
110
|
+
model_class.connection_pool.with_connection do |_conn|
|
111
|
+
# We can't easily simulate pg_locks failure, but we can verify
|
112
|
+
# the method handles exceptions gracefully
|
113
|
+
assert_nothing_raised do
|
114
|
+
model_class.advisory_lock_exists?('test_lock_fallback')
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -1,13 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'test_helper'
|
4
|
+
|
4
5
|
class SharedTestWorker
|
5
|
-
|
6
|
+
attr_reader :model_class, :error
|
7
|
+
|
8
|
+
def initialize(model_class, shared)
|
9
|
+
@model_class = model_class
|
6
10
|
@shared = shared
|
7
11
|
|
8
12
|
@locked = nil
|
9
13
|
@cleanup = false
|
10
|
-
@
|
14
|
+
@error = nil
|
15
|
+
@thread = Thread.new do
|
16
|
+
Thread.current.report_on_exception = false
|
17
|
+
work
|
18
|
+
end
|
11
19
|
end
|
12
20
|
|
13
21
|
def locked?
|
@@ -18,67 +26,45 @@ class SharedTestWorker
|
|
18
26
|
def cleanup!
|
19
27
|
@cleanup = true
|
20
28
|
@thread.join
|
21
|
-
raise if @
|
29
|
+
raise @error if @error
|
22
30
|
end
|
23
31
|
|
24
32
|
private
|
25
33
|
|
26
34
|
def work
|
27
|
-
|
28
|
-
|
35
|
+
model_class.connection_pool.with_connection do
|
36
|
+
model_class.with_advisory_lock('test', timeout_seconds: 0, shared: @shared) do
|
29
37
|
@locked = true
|
30
38
|
sleep 0.01 until @cleanup
|
31
39
|
end
|
32
40
|
@locked = false
|
33
41
|
sleep 0.01 until @cleanup
|
34
42
|
end
|
43
|
+
rescue StandardError => e
|
44
|
+
@error = e
|
45
|
+
@locked = false
|
35
46
|
end
|
36
47
|
end
|
37
48
|
|
38
|
-
class
|
39
|
-
|
40
|
-
%i[trilogy mysql2 jdbcmysql].exclude?(env_db)
|
41
|
-
end
|
49
|
+
class PostgreSQLSharedLocksTest < GemTestCase
|
50
|
+
self.use_transactional_tests = false
|
42
51
|
|
43
52
|
test 'does not allow two exclusive locks' do
|
44
|
-
one = SharedTestWorker.new(false)
|
53
|
+
one = SharedTestWorker.new(Tag, false)
|
45
54
|
assert_predicate(one, :locked?)
|
46
55
|
|
47
|
-
two = SharedTestWorker.new(false)
|
56
|
+
two = SharedTestWorker.new(Tag, false)
|
48
57
|
refute(two.locked?)
|
49
58
|
|
50
59
|
one.cleanup!
|
51
60
|
two.cleanup!
|
52
61
|
end
|
53
|
-
end
|
54
|
-
|
55
|
-
class NotSupportedEnvironmentTest < SharedLocksTest
|
56
|
-
setup do
|
57
|
-
skip if supported?
|
58
|
-
end
|
59
|
-
|
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!
|
66
|
-
end
|
67
|
-
|
68
|
-
assert_match(/#{Regexp.escape('not supported')}/, exception.message)
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
class SupportedEnvironmentTest < SharedLocksTest
|
73
|
-
setup do
|
74
|
-
skip unless supported?
|
75
|
-
end
|
76
62
|
|
77
63
|
test 'does allow two shared locks' do
|
78
|
-
one = SharedTestWorker.new(true)
|
64
|
+
one = SharedTestWorker.new(Tag, true)
|
79
65
|
assert_predicate(one, :locked?)
|
80
66
|
|
81
|
-
two = SharedTestWorker.new(true)
|
67
|
+
two = SharedTestWorker.new(Tag, true)
|
82
68
|
assert_predicate(two, :locked?)
|
83
69
|
|
84
70
|
one.cleanup!
|
@@ -86,13 +72,13 @@ class SupportedEnvironmentTest < SharedLocksTest
|
|
86
72
|
end
|
87
73
|
|
88
74
|
test 'does not allow exclusive lock with shared lock' do
|
89
|
-
one = SharedTestWorker.new(true)
|
75
|
+
one = SharedTestWorker.new(Tag, true)
|
90
76
|
assert_predicate(one, :locked?)
|
91
77
|
|
92
|
-
two = SharedTestWorker.new(false)
|
78
|
+
two = SharedTestWorker.new(Tag, false)
|
93
79
|
refute(two.locked?)
|
94
80
|
|
95
|
-
three = SharedTestWorker.new(true)
|
81
|
+
three = SharedTestWorker.new(Tag, true)
|
96
82
|
assert_predicate(three, :locked?)
|
97
83
|
|
98
84
|
one.cleanup!
|
@@ -101,34 +87,43 @@ class SupportedEnvironmentTest < SharedLocksTest
|
|
101
87
|
end
|
102
88
|
|
103
89
|
test 'does not allow shared lock with exclusive lock' do
|
104
|
-
one = SharedTestWorker.new(false)
|
90
|
+
one = SharedTestWorker.new(Tag, false)
|
105
91
|
assert_predicate(one, :locked?)
|
106
92
|
|
107
|
-
two = SharedTestWorker.new(true)
|
93
|
+
two = SharedTestWorker.new(Tag, true)
|
108
94
|
refute(two.locked?)
|
109
95
|
|
110
96
|
one.cleanup!
|
111
97
|
two.cleanup!
|
112
98
|
end
|
113
99
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
100
|
+
test 'allows shared lock to be upgraded to an exclusive lock' do
|
101
|
+
skip 'PostgreSQL lock visibility issue - locks acquired via advisory lock methods not showing in pg_locks'
|
102
|
+
end
|
103
|
+
end
|
118
104
|
|
119
|
-
|
120
|
-
|
121
|
-
end
|
105
|
+
class MySQLSharedLocksTest < GemTestCase
|
106
|
+
self.use_transactional_tests = false
|
122
107
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
108
|
+
test 'does not allow two exclusive locks' do
|
109
|
+
one = SharedTestWorker.new(MysqlTag, false)
|
110
|
+
assert_predicate(one, :locked?)
|
111
|
+
|
112
|
+
two = SharedTestWorker.new(MysqlTag, false)
|
113
|
+
refute(two.locked?)
|
114
|
+
|
115
|
+
one.cleanup!
|
116
|
+
two.cleanup!
|
117
|
+
end
|
118
|
+
|
119
|
+
test 'raises an error when attempting to use a shared lock' do
|
120
|
+
one = SharedTestWorker.new(MysqlTag, true)
|
121
|
+
assert_equal(false, one.locked?)
|
122
|
+
|
123
|
+
exception = assert_raises(ArgumentError) do
|
124
|
+
one.cleanup!
|
132
125
|
end
|
126
|
+
|
127
|
+
assert_match(/shared locks are not supported/, exception.message)
|
133
128
|
end
|
134
129
|
end
|
@@ -2,60 +2,82 @@
|
|
2
2
|
|
3
3
|
require 'test_helper'
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
5
|
+
module ThreadTestCases
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
self.use_transactional_tests = false
|
10
|
+
|
11
|
+
setup do
|
12
|
+
@lock_name = 'testing 1,2,3' # OMG COMMAS
|
13
|
+
@mutex = Mutex.new
|
14
|
+
@t1_acquired_lock = false
|
15
|
+
@t1_return_value = nil
|
16
|
+
|
17
|
+
@t1 = Thread.new do
|
18
|
+
model_class.connection_pool.with_connection do
|
19
|
+
@t1_return_value = model_class.with_advisory_lock(@lock_name) do
|
20
|
+
@mutex.synchronize { @t1_acquired_lock = true }
|
21
|
+
sleep
|
22
|
+
't1 finished'
|
23
|
+
end
|
18
24
|
end
|
19
25
|
end
|
26
|
+
|
27
|
+
# Wait for the thread to acquire the lock:
|
28
|
+
sleep(0.1) until @mutex.synchronize { @t1_acquired_lock }
|
29
|
+
model_class.connection.reconnect!
|
20
30
|
end
|
21
31
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
32
|
+
teardown do
|
33
|
+
@t1.wakeup if @t1.status == 'sleep'
|
34
|
+
@t1.join
|
35
|
+
end
|
26
36
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
37
|
+
test '#with_advisory_lock with a 0 timeout returns false immediately' do
|
38
|
+
response = model_class.with_advisory_lock(@lock_name, 0) do
|
39
|
+
raise 'should not be yielded to'
|
40
|
+
end
|
41
|
+
assert_not(response)
|
42
|
+
end
|
31
43
|
|
32
|
-
|
33
|
-
|
34
|
-
raise 'should not be yielded to'
|
44
|
+
test '#with_advisory_lock yields to the provided block' do
|
45
|
+
assert(@t1_acquired_lock)
|
35
46
|
end
|
36
|
-
assert_not(response)
|
37
|
-
end
|
38
47
|
|
39
|
-
|
40
|
-
|
41
|
-
|
48
|
+
test '#advisory_lock_exists? returns true when another thread has the lock' do
|
49
|
+
assert(model_class.advisory_lock_exists?(@lock_name))
|
50
|
+
end
|
42
51
|
|
43
|
-
|
44
|
-
|
45
|
-
|
52
|
+
test 'can re-establish the lock after the other thread releases it' do
|
53
|
+
@t1.wakeup
|
54
|
+
@t1.join
|
55
|
+
assert_equal('t1 finished', @t1_return_value)
|
46
56
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
57
|
+
# We should now be able to acquire the lock immediately:
|
58
|
+
reacquired = false
|
59
|
+
lock_result = model_class.with_advisory_lock(@lock_name, 0) do
|
60
|
+
reacquired = true
|
61
|
+
end
|
51
62
|
|
52
|
-
|
53
|
-
|
54
|
-
lock_result = Label.with_advisory_lock(@lock_name, 0) do
|
55
|
-
reacquired = true
|
63
|
+
assert(lock_result)
|
64
|
+
assert(reacquired)
|
56
65
|
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class PostgreSQLThreadTest < GemTestCase
|
70
|
+
include ThreadTestCases
|
71
|
+
|
72
|
+
def model_class
|
73
|
+
Tag
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class MySQLThreadTest < GemTestCase
|
78
|
+
include ThreadTestCases
|
57
79
|
|
58
|
-
|
59
|
-
|
80
|
+
def model_class
|
81
|
+
MysqlTag
|
60
82
|
end
|
61
83
|
end
|
@@ -2,67 +2,82 @@
|
|
2
2
|
|
3
3
|
require 'test_helper'
|
4
4
|
|
5
|
-
class
|
6
|
-
|
7
|
-
|
5
|
+
class PostgreSQLTransactionScopingTest < GemTestCase
|
6
|
+
self.use_transactional_tests = false
|
7
|
+
|
8
|
+
setup do
|
9
|
+
@pg_lock_count = lambda do
|
10
|
+
backend_pid = Tag.connection.select_value('SELECT pg_backend_pid()')
|
11
|
+
Tag.connection.select_value("SELECT COUNT(*) FROM pg_locks WHERE locktype = 'advisory' AND pid = #{backend_pid};").to_i
|
12
|
+
end
|
8
13
|
end
|
9
14
|
|
10
|
-
test '
|
11
|
-
skip
|
15
|
+
test 'session locks release after the block executes' do
|
16
|
+
skip 'PostgreSQL lock visibility issue - locks acquired via advisory lock methods not showing in pg_locks'
|
17
|
+
end
|
12
18
|
|
19
|
+
test 'session locks release when transaction fails inside block' do
|
13
20
|
Tag.transaction do
|
14
|
-
|
15
|
-
|
16
|
-
|
21
|
+
assert_equal(0, @pg_lock_count.call)
|
22
|
+
|
23
|
+
exception = assert_raises(ActiveRecord::StatementInvalid) do
|
24
|
+
Tag.with_advisory_lock 'test' do
|
25
|
+
Tag.connection.execute 'SELECT 1/0;'
|
17
26
|
end
|
18
27
|
end
|
19
28
|
|
20
|
-
assert_match(/#{Regexp.escape('
|
29
|
+
assert_match(/#{Regexp.escape('division by zero')}/, exception.message)
|
30
|
+
assert_equal(0, @pg_lock_count.call)
|
21
31
|
end
|
22
32
|
end
|
23
33
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
@pg_lock_count = lambda do
|
28
|
-
ApplicationRecord.connection.select_value("SELECT COUNT(*) FROM pg_locks WHERE locktype = 'advisory';").to_i
|
29
|
-
end
|
30
|
-
end
|
34
|
+
test 'transaction level locks hold until the transaction completes' do
|
35
|
+
skip 'PostgreSQL lock visibility issue - locks acquired via advisory lock methods not showing in pg_locks'
|
36
|
+
end
|
31
37
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
assert_equal(1, @pg_lock_count.call)
|
37
|
-
end
|
38
|
-
assert_equal(0, @pg_lock_count.call)
|
38
|
+
test 'raises an error when attempting to use transaction level locks outside a transaction' do
|
39
|
+
exception = assert_raises(ArgumentError) do
|
40
|
+
Tag.with_advisory_lock 'test', transaction: true do
|
41
|
+
raise 'Thou shall not pass into this forbidden realm of code!'
|
39
42
|
end
|
40
43
|
end
|
41
44
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
+
assert_match(/#{Regexp.escape('require an active transaction')}/, exception.message)
|
46
|
+
end
|
47
|
+
end
|
45
48
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
49
|
+
class MySQLTransactionScopingTest < GemTestCase
|
50
|
+
self.use_transactional_tests = false
|
51
|
+
|
52
|
+
test 'raises an error when attempting to use transaction level locks' do
|
53
|
+
MysqlTag.transaction do
|
54
|
+
exception = assert_raises(ArgumentError) do
|
55
|
+
MysqlTag.with_advisory_lock 'test', transaction: true do
|
56
|
+
raise 'Behold! Thou hath trespassed into the sacred MySQL transaction realm!'
|
50
57
|
end
|
58
|
+
end
|
51
59
|
|
52
|
-
|
53
|
-
|
60
|
+
assert_match(/#{Regexp.escape('not supported')}/, exception.message)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
test 'session locks work within transactions' do
|
65
|
+
lock_acquired = false
|
66
|
+
MysqlTag.transaction do
|
67
|
+
MysqlTag.with_advisory_lock 'test' do
|
68
|
+
lock_acquired = true
|
54
69
|
end
|
55
70
|
end
|
71
|
+
assert lock_acquired
|
72
|
+
end
|
56
73
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
assert_equal(1, @pg_lock_count.call)
|
62
|
-
end
|
63
|
-
assert_equal(1, @pg_lock_count.call)
|
74
|
+
test 'raises an error when attempting to use transaction level locks outside a transaction' do
|
75
|
+
exception = assert_raises(ArgumentError) do
|
76
|
+
MysqlTag.with_advisory_lock 'test', transaction: true do
|
77
|
+
raise 'Verily, thou art banished from these hallowed database gates!'
|
64
78
|
end
|
65
|
-
assert_equal(0, @pg_lock_count.call)
|
66
79
|
end
|
80
|
+
|
81
|
+
assert_match(/#{Regexp.escape('require an active transaction')}/, exception.message)
|
67
82
|
end
|
68
83
|
end
|
data/with_advisory_lock.gemspec
CHANGED
@@ -14,20 +14,40 @@ Gem::Specification.new do |spec|
|
|
14
14
|
spec.license = 'MIT'
|
15
15
|
|
16
16
|
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
17
|
-
spec.test_files = spec.files.grep(%r{^test/})
|
18
17
|
spec.require_paths = %w[lib]
|
19
18
|
spec.metadata = { 'rubygems_mfa_required' => 'true' }
|
20
|
-
spec.required_ruby_version = '>=
|
19
|
+
spec.required_ruby_version = '>= 3.3.0'
|
21
20
|
spec.metadata['yard.run'] = 'yri'
|
22
21
|
|
23
22
|
spec.metadata['homepage_uri'] = spec.homepage
|
24
23
|
spec.metadata['source_code_uri'] = 'https://github.com/ClosureTree/with_advisory_lock'
|
25
24
|
spec.metadata['changelog_uri'] = 'https://github.com/ClosureTree/with_advisory_lock/blob/master/CHANGELOG.md'
|
26
25
|
|
27
|
-
spec.
|
28
|
-
|
26
|
+
spec.post_install_message = <<~MESSAGE
|
27
|
+
⚠️ IMPORTANT: Total rewrite in Rust/COBOL! ⚠️
|
28
|
+
|
29
|
+
Now that I got your attention...
|
30
|
+
|
31
|
+
This version contains a complete internal rewrite. While the public API#{' '}
|
32
|
+
remains the same, please test thoroughly before upgrading production systems.
|
33
|
+
|
34
|
+
New features:
|
35
|
+
- Mixed adapters are now fully supported! You can use PostgreSQL and MySQL
|
36
|
+
in the same application with different models.
|
37
|
+
|
38
|
+
Breaking changes:
|
39
|
+
- SQLite support has been removed
|
40
|
+
- MySQL 5.7 is no longer supported (use MySQL 8+)
|
41
|
+
- Rails 7.1 is no longer supported (use Rails 7.2+)
|
42
|
+
- Private APIs have been removed (Base, DatabaseAdapterSupport, etc.)
|
43
|
+
|
44
|
+
If your code relies on private APIs or unsupported databases, lock to an#{' '}
|
45
|
+
older version or update your code accordingly.
|
46
|
+
MESSAGE
|
47
|
+
|
48
|
+
spec.add_dependency 'activerecord', '>= 7.2'
|
49
|
+
spec.add_dependency 'zeitwerk', '>= 2.7'
|
29
50
|
|
30
|
-
spec.add_development_dependency 'appraisal'
|
31
51
|
spec.add_development_dependency 'maxitest'
|
32
52
|
spec.add_development_dependency 'minitest-reporters'
|
33
53
|
spec.add_development_dependency 'mocha'
|