Skip to content

Commit 94253ca

Browse files
authored
fix: handle ActiveRecord's release_advisory_lock signature for Rails 7.2+ (#127)
Rails 7.2+ changed the signature of release_advisory_lock to require a lock_name: keyword argument. This causes an ArgumentError when migrations run: "missing keyword: :lock_name". This commit updates MySQL's implementation to handle both signatures, similar to how PostgreSQL already does it. The method now checks if it's being called with ActiveRecord's signature (single positional arg, no kwargs) or the gem's signature (lock_keys positional arg with keyword args). Fixes #126
1 parent 946a2ac commit 94253ca

File tree

2 files changed

+130
-2
lines changed

2 files changed

+130
-2
lines changed

lib/with_advisory_lock/mysql_advisory.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,17 @@ def try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seco
2525
execute_successful?("GET_LOCK(#{quote(lock_keys.first)}, #{mysql_timeout})")
2626
end
2727

28-
def release_advisory_lock(lock_keys, lock_name:, **)
29-
execute_successful?("RELEASE_LOCK(#{quote(lock_keys.first)})")
28+
def release_advisory_lock(*args, **kwargs)
29+
# Handle both signatures - ActiveRecord's built-in and ours
30+
if args.length == 1 && kwargs.empty?
31+
# ActiveRecord's signature: release_advisory_lock(lock_id)
32+
# Called by Rails migrations with a single positional argument
33+
super
34+
else
35+
# Our signature: release_advisory_lock(lock_keys, lock_name:, **)
36+
lock_keys = args.first
37+
execute_successful?("RELEASE_LOCK(#{quote(lock_keys.first)})")
38+
end
3039
rescue ActiveRecord::StatementInvalid => e
3140
# If the connection is broken, the lock is automatically released by MySQL
3241
# No need to fail the release operation
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+

0 commit comments

Comments
 (0)