aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/test/cases/adapters/mysql2/connection_test.rb
diff options
context:
space:
mode:
authorSam Davies <seivadmas@gmail.com>2015-10-29 18:26:54 -0300
committerSam Davies <seivadmas@gmail.com>2015-10-30 14:04:16 -0300
commit2c2a8755460ec3d32ece91c9766dbd0304ece028 (patch)
tree350a241e5ff5e07ad395455fe3d567dea70ede0f /activerecord/test/cases/adapters/mysql2/connection_test.rb
parent0174837bfa263c55cf727f23028d8f28da192d14 (diff)
downloadrails-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/connection_test.rb')
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb28
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