đ Wie ich einen festhĂ€ngenden Rails-Migrations-Lock in MySQL debuggt und gelöst habe
KĂŒrzlich bin ich auf ein frustrierendes Problem gestoĂen: Rails-Migrationen lieĂen sich nicht mehr ausfĂŒhren, weil der Advisory Lock, der parallele Migrationen verhindern soll, festhing. Hier zeige ich, wie ich das Problem untersucht und den Lock manuell ĂŒber die Rails-Konsole gelöst habe.
đ§ Das Problem
Rails verwendet bei Migrationen MySQL Advisory Locks, um zu verhindern, dass mehrere Prozesse gleichzeitig Migrationen ausfĂŒhren. Dabei kommen GET_LOCK(lock_id, timeout)
und RELEASE_LOCK(lock_id)
zum Einsatz.
In meinem Fall schlugen jedoch sowohl das Erhalten als auch das Freigeben des Locks fehl:
ActiveRecord::Base.connection.get_advisory_lock(lock_id)
# => false
ActiveRecord::Base.connection.release_advisory_lock(lock_id)
# => false
Das bedeutete:
- Ich selbst hielt den Lock nicht.
- Jemand anderes (oder eine verwaiste Session) hielt ihn noch.
- Rails-Migrationen waren dadurch blockiert.
đ Lock untersuchen
1. Die Lock-ID herausfinden, die Rails verwendet
Rails 6.1 generiert die Lock-ID fĂŒr MySQL wie folgt:
lock_id = ActiveRecord::Migrator::MIGRATOR_SALT *
Zlib.crc32(ActiveRecord::Base.connection.current_database)
Damit ist die Lock-ID eindeutig je nach Datenbanknamen. Hinweis: Die genaue Berechnungsweise kann sich je nach Rails-Version Ă€ndern â am besten im Quellcode der eigenen Version nachsehen.
2. Herausfinden, wer den Lock hÀlt
ActiveRecord::Base.connection.select_value("SELECT IS_USED_LOCK(#{lock_id})")
# => 1562 (Beispiel: Thread-ID)
Dieser Befehl gibt die MySQL Thread-ID der Session zurĂŒck, die aktuell den Lock hĂ€lt.
đ Den festhĂ€ngenden Lock freigeben
Um den Lock manuell freizugeben, habe ich die Session, die ihn hielt, beendet:
ActiveRecord::Base.connection.execute("KILL 1562")
Danach habe ich geprĂŒft, ob der Lock wirklich weg ist:
ActiveRecord::Base.connection.select_value("SELECT IS_USED_LOCK(#{lock_id})")
# => nil
Jetzt konnte ich den Lock wieder ganz normal anfordern und freigeben:
ActiveRecord::Base.connection.get_advisory_lock(lock_id)
# => true
ActiveRecord::Base.connection.release_advisory_lock(lock_id)
# => true
â VollstĂ€ndige 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
đ§ Fazit
- Rails nutzt MySQL Advisory Locks zur Steuerung von Migrationen.
- Diese Locks sind verbindungsspezifisch und unsichtbar, solange man sie nicht explizit abfragt.
- Mit
IS_USED_LOCK()
undKILL
kann man festhĂ€ngende Locks identifizieren und auflösen. - Vorsicht beim Beenden von DB-Sessions â prĂŒfe genau, ob es sicher ist.