🔒 How I Debugged and Released a Stuck Rails Migration Lock in MySQL
Recently, I ran into a frustrating issue where Rails migrations wouldn’t run because the advisory lock used to prevent concurrent migrations was stuck. Here's how I investigated and forcefully released it using the Rails console.
🧠 The Problem
When Rails runs migrations, it uses MySQL advisory locks under the hood to prevent multiple migration processes from running at the same time. This is implemented using GET_LOCK(lock_id, timeout)
and RELEASE_LOCK(lock_id)
.
However, in my case, trying to acquire or release the lock failed:
ActiveRecord::Base.connection.get_advisory_lock(lock_id)
# => false
ActiveRecord::Base.connection.release_advisory_lock(lock_id)
# => false
This meant:
- I didn't currently hold the lock.
- Someone else (or some orphaned session) did.
- Rails migrations were blocked indefinitely.
🔍 Investigating the Lock
1. Find the advisory lock ID Rails uses
Rails 6.1 uses this pattern to generate the migration lock ID for MySQL:
lock_id = ActiveRecord::Migrator::MIGRATOR_SALT *
Zlib.crc32(ActiveRecord::Base.connection.current_database)
This ensures a unique lock ID per database. The salt or way this ID is generated might change so you have to check the source for your specific version.
2. Check who holds the lock
ActiveRecord::Base.connection.select_value("SELECT IS_USED_LOCK(#{lock_id})")
# => 1562 (example thread ID)
This tells you the MySQL thread ID of the session currently holding the lock.
🛠 Releasing the Stuck Lock
To forcefully release the lock, I killed the owning session:
ActiveRecord::Base.connection.execute("KILL 1562")
Then I confirmed the lock was gone:
ActiveRecord::Base.connection.select_value("SELECT IS_USED_LOCK(#{lock_id})")
# => nil
Now, I could acquire and release the lock normally:
ActiveRecord::Base.connection.get_advisory_lock(lock_id)
# => true
ActiveRecord::Base.connection.release_advisory_lock(lock_id)
# => true
✅ Full IRB Session
lock_id = ActiveRecord::Migrator::MIGRATOR_SALT *
Zlib.crc32(ActiveRecord::Base.connection.current_database)
ActiveRecord::Base.connection.get_advisory_lock(lock_id)
# => false
ActiveRecord::Base.connection.release_advisory_lock(lock_id)
# => false
ActiveRecord::Base.connection.select_value("SELECT IS_USED_LOCK(#{lock_id})")
# => 1562
ActiveRecord::Base.connection.execute("KILL 1562")
# => nil
ActiveRecord::Base.connection.select_value("SELECT IS_USED_LOCK(#{lock_id})")
# => nil
ActiveRecord::Base.connection.get_advisory_lock(lock_id)
# => true
ActiveRecord::Base.connection.release_advisory_lock(lock_id)
# => true
🧠 Takeaways
- Rails uses MySQL advisory locks for migration locking.
- These are connection-scoped and invisible unless you ask.
- Use
IS_USED_LOCK()
andKILL
to troubleshoot stuck locks. - Always check carefully before killing DB sessions.