diff options
author | Sam Davies <seivadmas@gmail.com> | 2015-10-29 18:26:54 -0300 |
---|---|---|
committer | Sam Davies <seivadmas@gmail.com> | 2015-10-30 14:04:16 -0300 |
commit | 2c2a8755460ec3d32ece91c9766dbd0304ece028 (patch) | |
tree | 350a241e5ff5e07ad395455fe3d567dea70ede0f /activerecord/test/cases/adapters/mysql2 | |
parent | 0174837bfa263c55cf727f23028d8f28da192d14 (diff) | |
download | rails-2c2a8755460ec3d32ece91c9766dbd0304ece028.tar.gz rails-2c2a8755460ec3d32ece91c9766dbd0304ece028.tar.bz2 rails-2c2a8755460ec3d32ece91c9766dbd0304ece028.zip |
Use advisory locks to prevent concurrent migrations
- Addresses issue #22092
- Works on Postgres and MySQL
- Uses advisory locks because of two important properties:
1. The can be obtained outside of the context of a transaction
2. They are automatically released when the session ends, so if a
migration process crashed for whatever reason the lock is not left
open perpetually
- Adds get_advisory_lock and release_advisory_lock methods to database
adapters
- Attempting to run a migration while another one is in process will
raise a ConcurrentMigrationError instead of attempting to run in
parallel with undefined behavior. This could be rescued and
the migration could exit cleanly instead. Perhaps as a configuration
option?
Technical Notes
==============
The Migrator uses generate_migrator_advisory_lock_key to build the key
for the lock. In order to be compatible across multiple adapters there
are some constraints on this key.
- Postgres limits us to 64 bit signed integers
- MySQL advisory locks are server-wide so we have to scope to the
database
- To fulfil these requirements we use a Migrator salt (a randomly
chosen signed integer with max length of 31 bits) that identifies
the Rails migration process as the owner of the lock. We multiply
this salt with a CRC32 unsigned integer hash of the database name to
get a signed 64 bit integer that can also be converted to a string
to act as a lock key in MySQL databases.
- It is important for subsequent versions of the Migrator to use the
same salt, otherwise different versions of the Migrator will not see
each other's locks.
Diffstat (limited to 'activerecord/test/cases/adapters/mysql2')
-rw-r--r-- | activerecord/test/cases/adapters/mysql2/connection_test.rb | 28 |
1 files changed, 28 insertions, 0 deletions
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 000bcadebe..71c4028675 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -131,4 +131,32 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase ensure @connection.execute "DROP TABLE `bar_baz`" end + + def test_get_and_release_advisory_lock + key = "test_key" + + got_lock = @connection.get_advisory_lock(key) + assert got_lock, "get_advisory_lock should have returned true but it didn't" + + assert_equal test_lock_free(key), false, + "expected the test advisory lock to be held but it wasn't" + + released_lock = @connection.release_advisory_lock(key) + assert released_lock, "expected release_advisory_lock to return true but it didn't" + + assert test_lock_free(key), 'expected the test key to be available after releasing' + end + + def test_release_non_existent_advisory_lock + fake_key = "fake_key" + released_non_existent_lock = @connection.release_advisory_lock(fake_key) + assert_equal released_non_existent_lock, false, + 'expected release_advisory_lock to return false when there was no lock to release' + end + + protected + + def test_lock_free(key) + @connection.select_value("SELECT IS_FREE_LOCK('#{key}');") == 1 + end end |