From 2c2a8755460ec3d32ece91c9766dbd0304ece028 Mon Sep 17 00:00:00 2001 From: Sam Davies Date: Thu, 29 Oct 2015 18:26:54 -0300 Subject: 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. --- .../connection_adapters/postgresql_adapter.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb') diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 236c067fd5..0af7d99a4b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -293,6 +293,10 @@ module ActiveRecord true end + def supports_advisory_locks? + true + end + def supports_explain? true end @@ -311,6 +315,20 @@ module ActiveRecord postgresql_version >= 90300 end + def get_advisory_lock(key) # :nodoc: + unless key.is_a?(Integer) && key.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock keys to be a signed 64 bit integer") + end + select_value("SELECT pg_try_advisory_lock(#{key});") + end + + def release_advisory_lock(key) # :nodoc: + unless key.is_a?(Integer) && key.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock keys to be a signed 64 bit integer") + end + select_value("SELECT pg_advisory_unlock(#{key})") + end + def enable_extension(name) exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap { reload_type_map -- cgit v1.2.3