with_advisory_lock 5.3.0 → 7.0.1
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 +39 -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 +71 -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/mysql_release_lock_test.rb +119 -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 +55 -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,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class SanityCheckTest < GemTestCase
|
6
|
+
test 'PostgreSQL and MySQL databases are properly isolated' do
|
7
|
+
# Create a tag in PostgreSQL database
|
8
|
+
pg_tag = Tag.create!(name: 'postgresql-only-tag')
|
9
|
+
|
10
|
+
# Verify it exists in PostgreSQL
|
11
|
+
assert Tag.exists?(name: 'postgresql-only-tag')
|
12
|
+
assert_equal 1, Tag.where(name: 'postgresql-only-tag').count
|
13
|
+
|
14
|
+
# Verify it does NOT exist in MySQL database
|
15
|
+
assert_not MysqlTag.exists?(name: 'postgresql-only-tag')
|
16
|
+
assert_equal 0, MysqlTag.where(name: 'postgresql-only-tag').count
|
17
|
+
|
18
|
+
# Create a tag in MySQL database
|
19
|
+
mysql_tag = MysqlTag.create!(name: 'mysql-only-tag')
|
20
|
+
|
21
|
+
# Verify it exists in MySQL
|
22
|
+
assert MysqlTag.exists?(name: 'mysql-only-tag')
|
23
|
+
assert_equal 1, MysqlTag.where(name: 'mysql-only-tag').count
|
24
|
+
|
25
|
+
# Verify it does NOT exist in PostgreSQL database
|
26
|
+
assert_not Tag.exists?(name: 'mysql-only-tag')
|
27
|
+
assert_equal 0, Tag.where(name: 'mysql-only-tag').count
|
28
|
+
|
29
|
+
# Clean up
|
30
|
+
pg_tag.destroy
|
31
|
+
mysql_tag.destroy
|
32
|
+
end
|
33
|
+
|
34
|
+
test 'PostgreSQL models use PostgreSQL adapter' do
|
35
|
+
assert_equal 'PostgreSQL', Tag.connection.adapter_name
|
36
|
+
assert_equal 'PostgreSQL', TagAudit.connection.adapter_name
|
37
|
+
assert_equal 'PostgreSQL', Label.connection.adapter_name
|
38
|
+
end
|
39
|
+
|
40
|
+
test 'MySQL models use MySQL adapter' do
|
41
|
+
assert_equal 'Mysql2', MysqlTag.connection.adapter_name
|
42
|
+
assert_equal 'Mysql2', MysqlTagAudit.connection.adapter_name
|
43
|
+
assert_equal 'Mysql2', MysqlLabel.connection.adapter_name
|
44
|
+
end
|
45
|
+
|
46
|
+
test 'can write to both databases in same test' do
|
47
|
+
# Create records in both databases
|
48
|
+
pg_tag = Tag.create!(name: 'test-pg')
|
49
|
+
mysql_tag = MysqlTag.create!(name: 'test-mysql')
|
50
|
+
|
51
|
+
# Both should have IDs
|
52
|
+
assert pg_tag.persisted?
|
53
|
+
assert mysql_tag.persisted?
|
54
|
+
|
55
|
+
# IDs should be independent (both could be 1 if tables are empty)
|
56
|
+
assert_kind_of Integer, pg_tag.id
|
57
|
+
assert_kind_of Integer, mysql_tag.id
|
58
|
+
|
59
|
+
# Clean up
|
60
|
+
pg_tag.destroy
|
61
|
+
mysql_tag.destroy
|
62
|
+
end
|
63
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -1,66 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'erb'
|
4
|
-
require 'active_record'
|
5
|
-
require 'with_advisory_lock'
|
6
|
-
require 'tmpdir'
|
7
3
|
require 'securerandom'
|
8
|
-
begin
|
9
|
-
require 'activerecord-trilogy-adapter'
|
10
|
-
ActiveSupport.on_load(:active_record) do
|
11
|
-
require "trilogy_adapter/connection"
|
12
|
-
ActiveRecord::Base.public_send :extend, TrilogyAdapter::Connection
|
13
|
-
end
|
14
|
-
rescue LoadError
|
15
|
-
# do nothing
|
16
|
-
end
|
17
|
-
|
18
|
-
ActiveRecord::Base.configurations = {
|
19
|
-
default_env: {
|
20
|
-
url: ENV.fetch('DATABASE_URL', "sqlite3://#{Dir.tmpdir}/with_advisory_lock_test#{RUBY_VERSION}-#{ActiveRecord.gem_version}.sqlite3"),
|
21
|
-
pool: 20,
|
22
|
-
properties: { allowPublicKeyRetrieval: true } # for JRuby madness
|
23
|
-
}
|
24
|
-
}
|
25
4
|
|
5
|
+
ENV['RAILS_ENV'] = 'test'
|
26
6
|
ENV['WITH_ADVISORY_LOCK_PREFIX'] ||= SecureRandom.hex
|
27
7
|
|
28
|
-
|
29
|
-
|
30
|
-
def env_db
|
31
|
-
@env_db ||= ActiveRecord::Base.connection_db_config.adapter.to_sym
|
32
|
-
end
|
8
|
+
require 'dotenv'
|
9
|
+
Dotenv.load
|
33
10
|
|
34
|
-
|
11
|
+
require_relative 'dummy/config/environment'
|
12
|
+
require 'rails/test_help'
|
35
13
|
|
36
|
-
require '
|
37
|
-
require 'minitest'
|
14
|
+
require 'with_advisory_lock'
|
38
15
|
require 'maxitest/autorun'
|
39
16
|
require 'mocha/minitest'
|
40
17
|
|
41
18
|
class GemTestCase < ActiveSupport::TestCase
|
42
|
-
|
43
19
|
parallelize(workers: 1)
|
44
|
-
def adapter_support
|
45
|
-
@adapter_support ||= WithAdvisoryLock::DatabaseAdapterSupport.new(ActiveRecord::Base.connection)
|
46
|
-
end
|
47
|
-
def is_sqlite3_adapter?; adapter_support.sqlite?; end
|
48
|
-
def is_mysql_adapter?; adapter_support.mysql?; end
|
49
|
-
def is_postgresql_adapter?; adapter_support.postgresql?; end
|
50
20
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
Label.table_name
|
57
|
-
)
|
21
|
+
def self.startup
|
22
|
+
# Validate environment variables when tests actually start running
|
23
|
+
%w[DATABASE_URL_PG DATABASE_URL_MYSQL].each do |var|
|
24
|
+
abort "Missing required environment variable: #{var}" if ENV[var].nil? || ENV[var].empty?
|
25
|
+
end
|
58
26
|
end
|
59
27
|
|
60
|
-
|
61
|
-
|
62
|
-
end
|
28
|
+
# Override in test classes to clean only the tables you need
|
29
|
+
# This avoids unnecessary database operations
|
63
30
|
end
|
64
31
|
|
65
|
-
puts "Testing
|
32
|
+
puts "Testing ActiveRecord #{ActiveRecord.gem_version} and ruby #{RUBY_VERSION}"
|
66
33
|
puts "Connection Pool size: #{ActiveRecord::Base.connection_pool.size}"
|
@@ -2,32 +2,78 @@
|
|
2
2
|
|
3
3
|
require 'test_helper'
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
module ConcernTestCases
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
test 'adds with_advisory_lock to ActiveRecord classes' do
|
10
|
+
assert_respond_to(model_class, :with_advisory_lock)
|
11
|
+
end
|
12
|
+
|
13
|
+
test 'adds with_advisory_lock to ActiveRecord instances' do
|
14
|
+
assert_respond_to(model_class.new, :with_advisory_lock)
|
15
|
+
end
|
16
|
+
|
17
|
+
test 'adds advisory_lock_exists? to ActiveRecord classes' do
|
18
|
+
assert_respond_to(model_class, :advisory_lock_exists?)
|
19
|
+
end
|
9
20
|
|
10
|
-
|
11
|
-
|
21
|
+
test 'adds advisory_lock_exists? to ActiveRecord instances' do
|
22
|
+
assert_respond_to(model_class.new, :advisory_lock_exists?)
|
23
|
+
end
|
12
24
|
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class PostgreSQLConcernTest < GemTestCase
|
28
|
+
include ConcernTestCases
|
13
29
|
|
14
|
-
|
15
|
-
|
30
|
+
def model_class
|
31
|
+
Tag
|
16
32
|
end
|
33
|
+
end
|
17
34
|
|
18
|
-
|
19
|
-
|
35
|
+
class MySQLConcernTest < GemTestCase
|
36
|
+
include ConcernTestCases
|
37
|
+
|
38
|
+
def model_class
|
39
|
+
MysqlTag
|
20
40
|
end
|
21
41
|
end
|
22
42
|
|
43
|
+
# This test is adapter-agnostic, so we only need to test it once
|
23
44
|
class ActiveRecordQueryCacheTest < GemTestCase
|
45
|
+
self.use_transactional_tests = false
|
46
|
+
|
24
47
|
test 'does not disable quary cache by default' do
|
25
48
|
Tag.connection.expects(:uncached).never
|
26
49
|
Tag.with_advisory_lock('lock') { Tag.first }
|
27
50
|
end
|
28
51
|
|
29
52
|
test 'can disable ActiveRecord query cache' do
|
30
|
-
|
31
|
-
|
53
|
+
# Mocha expects needs to properly handle block return values
|
54
|
+
connection = Tag.connection
|
55
|
+
|
56
|
+
# Create a stub that properly yields and returns the block's result
|
57
|
+
connection.define_singleton_method(:uncached_with_mock) do |&block|
|
58
|
+
@uncached_called = true
|
59
|
+
uncached_without_mock(&block)
|
60
|
+
end
|
61
|
+
|
62
|
+
connection.define_singleton_method(:uncached_called?) do
|
63
|
+
@uncached_called || false
|
64
|
+
end
|
65
|
+
|
66
|
+
connection.singleton_class.alias_method :uncached_without_mock, :uncached
|
67
|
+
connection.singleton_class.alias_method :uncached, :uncached_with_mock
|
68
|
+
|
69
|
+
begin
|
70
|
+
Tag.with_advisory_lock('a-lock', disable_query_cache: true) { Tag.first }
|
71
|
+
assert connection.uncached_called?, 'uncached should have been called'
|
72
|
+
ensure
|
73
|
+
connection.singleton_class.alias_method :uncached, :uncached_without_mock
|
74
|
+
connection.singleton_class.remove_method :uncached_with_mock
|
75
|
+
connection.singleton_class.remove_method :uncached_without_mock
|
76
|
+
connection.singleton_class.remove_method :uncached_called?
|
77
|
+
end
|
32
78
|
end
|
33
79
|
end
|
@@ -2,110 +2,196 @@
|
|
2
2
|
|
3
3
|
require 'test_helper'
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
@lock_name = 'test lock'
|
8
|
-
@return_val = 1900
|
9
|
-
end
|
5
|
+
module LockTestCases
|
6
|
+
extend ActiveSupport::Concern
|
10
7
|
|
11
|
-
|
12
|
-
|
13
|
-
end
|
8
|
+
included do
|
9
|
+
self.use_transactional_tests = false
|
14
10
|
|
15
|
-
|
16
|
-
|
17
|
-
|
11
|
+
setup do
|
12
|
+
@lock_name = 'test lock'
|
13
|
+
@return_val = 1900
|
18
14
|
end
|
19
|
-
end
|
20
15
|
|
21
|
-
|
22
|
-
|
23
|
-
Tag.with_advisory_lock(dangerous_lock_name) do
|
24
|
-
assert_match(/#{Regexp.escape(dangerous_lock_name)}/, Tag.current_advisory_lock)
|
16
|
+
test 'returns nil outside an advisory lock request' do
|
17
|
+
assert_nil(model_class.current_advisory_lock)
|
25
18
|
end
|
26
|
-
end
|
27
19
|
|
28
|
-
|
29
|
-
|
30
|
-
|
20
|
+
test 'returns the name of the last lock acquired' do
|
21
|
+
model_class.with_advisory_lock(@lock_name) do
|
22
|
+
assert_match(/#{@lock_name}/, model_class.current_advisory_lock)
|
23
|
+
end
|
24
|
+
end
|
31
25
|
|
32
|
-
|
33
|
-
|
34
|
-
|
26
|
+
test 'can obtain a lock with a name that attempts to disrupt a SQL comment' do
|
27
|
+
dangerous_lock_name = 'test */ lock /*'
|
28
|
+
model_class.with_advisory_lock(dangerous_lock_name) do
|
29
|
+
assert_match(/#{Regexp.escape(dangerous_lock_name)}/, model_class.current_advisory_lock)
|
30
|
+
end
|
35
31
|
end
|
36
|
-
end
|
37
32
|
|
38
|
-
|
39
|
-
|
40
|
-
|
33
|
+
test 'returns false for an unacquired lock' do
|
34
|
+
refute(model_class.advisory_lock_exists?(@lock_name))
|
35
|
+
end
|
41
36
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
@locked_elsewhere = true
|
46
|
-
loop { sleep 0.01 }
|
37
|
+
test 'returns true for an acquired lock' do
|
38
|
+
model_class.with_advisory_lock(@lock_name) do
|
39
|
+
assert(model_class.advisory_lock_exists?(@lock_name))
|
47
40
|
end
|
48
41
|
end
|
49
42
|
|
50
|
-
|
51
|
-
|
43
|
+
test 'returns block return value if lock successful' do
|
44
|
+
assert_equal(@return_val, model_class.with_advisory_lock!(@lock_name) { @return_val })
|
45
|
+
end
|
46
|
+
|
47
|
+
test 'returns false on lock acquisition failure' do
|
48
|
+
thread_with_lock = Thread.new do
|
49
|
+
model_class.connection_pool.with_connection do
|
50
|
+
model_class.with_advisory_lock(@lock_name, timeout_seconds: 0) do
|
51
|
+
@locked_elsewhere = true
|
52
|
+
loop { sleep 0.01 }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
52
56
|
|
53
|
-
|
54
|
-
|
57
|
+
sleep 0.01 until @locked_elsewhere
|
58
|
+
model_class.connection.reconnect!
|
59
|
+
assert_not(model_class.with_advisory_lock(@lock_name, timeout_seconds: 0) { @return_val })
|
60
|
+
|
61
|
+
thread_with_lock.kill
|
62
|
+
end
|
63
|
+
|
64
|
+
test 'raises an error on lock acquisition failure' do
|
65
|
+
thread_with_lock = Thread.new do
|
66
|
+
model_class.connection_pool.with_connection do
|
67
|
+
model_class.with_advisory_lock(@lock_name, timeout_seconds: 0) do
|
68
|
+
@locked_elsewhere = true
|
69
|
+
loop { sleep 0.01 }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
55
73
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
@
|
60
|
-
loop { sleep 0.01 }
|
74
|
+
sleep 0.01 until @locked_elsewhere
|
75
|
+
model_class.connection.reconnect!
|
76
|
+
assert_raises(WithAdvisoryLock::FailedToAcquireLock) do
|
77
|
+
model_class.with_advisory_lock!(@lock_name, timeout_seconds: 0) { @return_val }
|
61
78
|
end
|
79
|
+
|
80
|
+
thread_with_lock.kill
|
62
81
|
end
|
63
82
|
|
64
|
-
|
65
|
-
|
66
|
-
|
83
|
+
test 'attempts the lock exactly once with no timeout' do
|
84
|
+
expected = SecureRandom.base64
|
85
|
+
actual = model_class.with_advisory_lock(@lock_name, 0) do
|
86
|
+
expected
|
87
|
+
end
|
88
|
+
|
89
|
+
assert_equal(expected, actual)
|
67
90
|
end
|
68
91
|
|
69
|
-
|
70
|
-
|
92
|
+
test 'current_advisory_locks returns empty array outside an advisory lock request' do
|
93
|
+
assert_equal([], model_class.current_advisory_locks)
|
94
|
+
end
|
71
95
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
96
|
+
test 'current_advisory_locks returns an array with names of the acquired locks' do
|
97
|
+
model_class.with_advisory_lock(@lock_name) do
|
98
|
+
locks = model_class.current_advisory_locks
|
99
|
+
assert_equal(1, locks.size)
|
100
|
+
assert_match(/#{@lock_name}/, locks.first)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
test 'current_advisory_locks returns array of all nested lock names' do
|
105
|
+
first_lock = 'outer lock'
|
106
|
+
second_lock = 'inner lock'
|
107
|
+
|
108
|
+
model_class.with_advisory_lock(first_lock) do
|
109
|
+
model_class.with_advisory_lock(second_lock) do
|
110
|
+
locks = model_class.current_advisory_locks
|
111
|
+
assert_equal(2, locks.size)
|
112
|
+
assert_match(/#{first_lock}/, locks.first)
|
113
|
+
assert_match(/#{second_lock}/, locks.last)
|
114
|
+
end
|
115
|
+
|
116
|
+
locks = model_class.current_advisory_locks
|
117
|
+
assert_equal(1, locks.size)
|
118
|
+
assert_match(/#{first_lock}/, locks.first)
|
119
|
+
end
|
120
|
+
assert_equal([], model_class.current_advisory_locks)
|
76
121
|
end
|
77
122
|
|
78
|
-
|
123
|
+
test 'handles connection disconnection gracefully during lock release' do
|
124
|
+
# This test ensures that if the connection is lost, lock release doesn't fail
|
125
|
+
# The lock will be automatically released by the database when the session ends
|
126
|
+
model_class.with_advisory_lock(@lock_name) do
|
127
|
+
# Simulate connection issues by testing the rescue logic
|
128
|
+
# We can't easily test actual disconnection in unit tests without side effects
|
129
|
+
# but we can test the error handling logic by testing with invalid connection state
|
130
|
+
assert_not_nil model_class.current_advisory_lock
|
131
|
+
end
|
132
|
+
|
133
|
+
# After the block, current_advisory_lock should be nil regardless
|
134
|
+
assert_nil model_class.current_advisory_lock
|
135
|
+
end
|
79
136
|
end
|
137
|
+
end
|
138
|
+
|
139
|
+
class PostgreSQLLockTest < GemTestCase
|
140
|
+
include LockTestCases
|
80
141
|
|
81
|
-
|
82
|
-
|
142
|
+
def model_class
|
143
|
+
Tag
|
83
144
|
end
|
84
145
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
146
|
+
def setup
|
147
|
+
super
|
148
|
+
Tag.delete_all
|
149
|
+
end
|
150
|
+
|
151
|
+
test 'does not support database timeout for PostgreSQL' do
|
152
|
+
assert_not model_class.connection.supports_database_timeout?
|
91
153
|
end
|
154
|
+
end
|
92
155
|
|
93
|
-
|
94
|
-
|
95
|
-
second_lock = 'inner lock'
|
156
|
+
class MySQLLockTest < GemTestCase
|
157
|
+
include LockTestCases
|
96
158
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
159
|
+
def model_class
|
160
|
+
MysqlTag
|
161
|
+
end
|
162
|
+
|
163
|
+
def setup
|
164
|
+
super
|
165
|
+
MysqlTag.delete_all
|
166
|
+
end
|
167
|
+
|
168
|
+
test 'uses database timeout for MySQL' do
|
169
|
+
assert model_class.connection.supports_database_timeout?
|
170
|
+
end
|
104
171
|
|
105
|
-
|
106
|
-
|
107
|
-
|
172
|
+
test 'mysql uses native timeout instead of polling' do
|
173
|
+
# This test verifies that MySQL bypasses Ruby-level polling
|
174
|
+
# when timeout is specified, relying on GET_LOCK's native timeout
|
175
|
+
lock_name = 'mysql_timeout_test'
|
176
|
+
|
177
|
+
# Hold a lock in another connection - need to use the same prefixed name as the gem
|
178
|
+
other_conn = model_class.connection_pool.checkout
|
179
|
+
lock_keys = other_conn.lock_keys_for(lock_name)
|
180
|
+
other_conn.select_value("SELECT GET_LOCK(#{other_conn.quote(lock_keys.first)}, 0)")
|
181
|
+
|
182
|
+
begin
|
183
|
+
# Attempt to acquire with a short timeout - should fail quickly
|
184
|
+
start_time = Time.now
|
185
|
+
result = model_class.with_advisory_lock(lock_name, timeout_seconds: 1) { 'success' }
|
186
|
+
elapsed = Time.now - start_time
|
187
|
+
|
188
|
+
# Should return false and complete within reasonable time (< 3 seconds)
|
189
|
+
# If it were using Ruby polling, it would take longer
|
190
|
+
assert_not result
|
191
|
+
assert elapsed < 3.0, "Expected quick timeout, but took #{elapsed} seconds"
|
192
|
+
ensure
|
193
|
+
other_conn.select_value("SELECT RELEASE_LOCK(#{other_conn.quote(lock_keys.first)})")
|
194
|
+
model_class.connection_pool.checkin(other_conn)
|
108
195
|
end
|
109
|
-
assert_equal([], Tag.current_advisory_locks)
|
110
196
|
end
|
111
197
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class MultiAdapterIsolationTest < GemTestCase
|
6
|
+
test 'postgresql and mysql adapters do not overlap' do
|
7
|
+
lock_name = 'multi-adapter-lock'
|
8
|
+
|
9
|
+
Tag.with_advisory_lock(lock_name) do
|
10
|
+
assert MysqlTag.with_advisory_lock(lock_name, timeout_seconds: 0) { true }
|
11
|
+
end
|
12
|
+
|
13
|
+
MysqlTag.with_advisory_lock(lock_name) do
|
14
|
+
assert Tag.with_advisory_lock(lock_name, timeout_seconds: 0) { true }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class MySQLReleaseLockTest < GemTestCase
|
6
|
+
self.use_transactional_tests = false
|
7
|
+
|
8
|
+
def model_class
|
9
|
+
MysqlTag
|
10
|
+
end
|
11
|
+
|
12
|
+
def setup
|
13
|
+
super
|
14
|
+
begin
|
15
|
+
skip unless model_class.connection.adapter_name =~ /mysql/i
|
16
|
+
MysqlTag.delete_all
|
17
|
+
rescue ActiveRecord::NoDatabaseError
|
18
|
+
skip "MySQL database not available. Please create the database first."
|
19
|
+
rescue StandardError => e
|
20
|
+
skip "MySQL connection failed: #{e.message}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
test 'release_advisory_lock handles gem signature with lock_keys' do
|
25
|
+
lock_name = 'test_gem_signature'
|
26
|
+
lock_keys = model_class.connection.lock_keys_for(lock_name)
|
27
|
+
|
28
|
+
# Acquire the lock
|
29
|
+
result = model_class.connection.try_advisory_lock(
|
30
|
+
lock_keys,
|
31
|
+
lock_name: lock_name,
|
32
|
+
shared: false,
|
33
|
+
transaction: false
|
34
|
+
)
|
35
|
+
assert result, 'Failed to acquire lock'
|
36
|
+
|
37
|
+
# Release using gem signature
|
38
|
+
released = model_class.connection.release_advisory_lock(
|
39
|
+
lock_keys,
|
40
|
+
lock_name: lock_name,
|
41
|
+
shared: false,
|
42
|
+
transaction: false
|
43
|
+
)
|
44
|
+
assert released, 'Failed to release lock using gem signature'
|
45
|
+
|
46
|
+
# Verify lock is released by trying to acquire it again
|
47
|
+
result = model_class.connection.try_advisory_lock(
|
48
|
+
lock_keys,
|
49
|
+
lock_name: lock_name,
|
50
|
+
shared: false,
|
51
|
+
transaction: false
|
52
|
+
)
|
53
|
+
assert result, 'Lock was not properly released'
|
54
|
+
|
55
|
+
# Clean up
|
56
|
+
model_class.connection.release_advisory_lock(
|
57
|
+
lock_keys,
|
58
|
+
lock_name: lock_name,
|
59
|
+
shared: false,
|
60
|
+
transaction: false
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
test 'release_advisory_lock handles ActiveRecord signature' do
|
65
|
+
# Rails calls release_advisory_lock with a positional argument (lock_id)
|
66
|
+
# This test ensures our override doesn't break Rails' migration locking
|
67
|
+
|
68
|
+
lock_name = 'test_rails_signature'
|
69
|
+
|
70
|
+
# Acquire lock using SQL (ActiveRecord doesn't provide get_advisory_lock method)
|
71
|
+
lock_keys = model_class.connection.lock_keys_for(lock_name)
|
72
|
+
result = model_class.connection.select_value("SELECT GET_LOCK(#{model_class.connection.quote(lock_keys.first)}, 0)")
|
73
|
+
assert_equal 1, result, 'Failed to acquire lock using SQL'
|
74
|
+
|
75
|
+
# Release using ActiveRecord signature (positional argument, as Rails does)
|
76
|
+
released = model_class.connection.release_advisory_lock(lock_keys.first)
|
77
|
+
assert released, 'Failed to release lock using ActiveRecord signature'
|
78
|
+
|
79
|
+
# Verify lock is released
|
80
|
+
lock_keys = model_class.connection.lock_keys_for(lock_name)
|
81
|
+
result = model_class.connection.select_value("SELECT GET_LOCK(#{model_class.connection.quote(lock_keys.first)}, 0)")
|
82
|
+
assert_equal 1, result, 'Lock was not properly released'
|
83
|
+
|
84
|
+
# Clean up
|
85
|
+
model_class.connection.select_value("SELECT RELEASE_LOCK(#{model_class.connection.quote(lock_keys.first)})")
|
86
|
+
end
|
87
|
+
|
88
|
+
test 'release_advisory_lock handles connection errors gracefully' do
|
89
|
+
lock_name = 'test_connection_error'
|
90
|
+
lock_keys = model_class.connection.lock_keys_for(lock_name)
|
91
|
+
|
92
|
+
# Acquire the lock
|
93
|
+
result = model_class.connection.try_advisory_lock(
|
94
|
+
lock_keys,
|
95
|
+
lock_name: lock_name,
|
96
|
+
shared: false,
|
97
|
+
transaction: false
|
98
|
+
)
|
99
|
+
assert result, 'Failed to acquire lock'
|
100
|
+
|
101
|
+
# Simulate connection error handling
|
102
|
+
# The method should handle various connection error types without raising
|
103
|
+
begin
|
104
|
+
# Try to release - even if we can't simulate a real connection error,
|
105
|
+
# the code path exists and should work
|
106
|
+
model_class.connection.release_advisory_lock(
|
107
|
+
lock_keys,
|
108
|
+
lock_name: lock_name,
|
109
|
+
shared: false,
|
110
|
+
transaction: false
|
111
|
+
)
|
112
|
+
rescue StandardError => e
|
113
|
+
# Should not raise connection-related errors
|
114
|
+
refute_match(/Lost connection|MySQL server has gone away|Connection refused/i, e.message)
|
115
|
+
raise
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|