Module: Gitlab::Database::MigrationHelpers::RequireDisableDdlTransactionForMultipleLocks

Extended by:
ActiveSupport::Concern
Included in:
Gitlab::Database::Migration::V2_3
Defined in:
lib/gitlab/database/migration_helpers/require_disable_ddl_transaction_for_multiple_locks.rb

Overview

This cop detects multiple table locks across different statements in a single migration. This scenario has caused incidents in the past due to deadlocks (for example app.incident.io/gitlab/incidents/202).

Constant Summary collapse

LOCK_ACQUIRING_COMMANDS =
%w[ALTER CREATE DROP TRUNCATE LOCK UPDATE DELETE INSERT].freeze
LOCK_TYPES =
{
  high_severity: [
    'AccessExclusiveLock',  # Conflicts with all lock modes
    'ExclusiveLock'         # Conflicts with all except ROW SHARE
  ],

  low_severity: [
    'RowShareLock',         # Conflicts with EXCLUSIVE
    'AccessShareLock'       # Conflicts with ACCESS EXCLUSIVE only
  ]
}.freeze

Instance Method Summary collapse

Instance Method Details

#exec_migration(connection, direction) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/gitlab/database/migration_helpers/require_disable_ddl_transaction_for_multiple_locks.rb', line 39

def exec_migration(connection, direction)
  return super if should_skip_check?

  # In-memory tracking structures
  statement_tracking = []
  tables_locked_up_till_now = Set.new

  begin
    # Subscribe to SQL execution to track each statement
    subscription_id = ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|
      event = ActiveSupport::Notifications::Event.new(*args)
      sql = event.payload[:sql].strip

      next if should_skip_sql_statement?(sql)

      newly_locked_tables = []
      if likely_to_acquire_locks?(sql)
        newly_locked_tables = check_current_locks(connection).excluding(tables_locked_up_till_now.to_a)
      end

      newly_locked_tables.each { |table| tables_locked_up_till_now.add(table) }

      # Record new statement
      current_statement = {
        number: 1 + statement_tracking.size,
        sql: sql,
        locked_tables: newly_locked_tables.uniq
      }
      statement_tracking << current_statement
    end

    # Run the migration
    super.tap do
      # After the migration completes, analyze the collected lock data
      verify_single_table_per_statement(statement_tracking)
    end
  ensure
    # Cleanup
    ActiveSupport::Notifications.unsubscribe(subscription_id) if subscription_id
  end
end