class Dblint::Checks::LongHeldLock

Public Class Methods

new() click to toggle source
# File lib/dblint/checks/long_held_lock.rb, line 6
def initialize
  @locks_held = {}
  @existing_ids = {}
end

Public Instance Methods

statement_finished(_name, _id, payload) click to toggle source
# File lib/dblint/checks/long_held_lock.rb, line 15
def statement_finished(_name, _id, payload)
  if payload[:sql] == 'BEGIN'
    handle_begin
  elsif payload[:sql] == 'COMMIT'
    handle_commit
  elsif payload[:sql] == 'ROLLBACK'
    # do nothing
  elsif @existing_ids.present?
    increment_locks_held
    add_new_locks_held(payload)
  end
end
statement_started(_name, _id, _payload) click to toggle source
# File lib/dblint/checks/long_held_lock.rb, line 11
def statement_started(_name, _id, _payload)
  # Ignored
end

Private Instance Methods

add_new_locks_held(payload) click to toggle source
# File lib/dblint/checks/long_held_lock.rb, line 36
def add_new_locks_held(payload)
  locked_table = payload[:sql].match(/^UPDATE\s+"?([\w.]+)"?/i).try(:[], 1)

  return unless locked_table.present?

  bind = payload[:binds].find { |b| b[0].name == 'id' }
  return unless bind.present?

  tuple = [locked_table, bind[1]]

  # We only want tuples that were not created in this transaction
  existing_ids = @existing_ids[tuple[0]]
  return unless existing_ids.present? && existing_ids.include?(tuple[1])

  # We've done two UPDATEs to the same row in this transaction
  return if @locks_held[tuple].present?

  @locks_held[tuple] = {}
  @locks_held[tuple][:sql]   = payload[:sql]
  @locks_held[tuple][:count] = 0
  @locks_held[tuple][:started_at] = Time.now
end
handle_begin() click to toggle source
# File lib/dblint/checks/long_held_lock.rb, line 59
def handle_begin
  @locks_held = {}
  @existing_ids = {}

  ActiveRecord::Base.connection.tables.each do |table|
    next if %w(schema_migrations ar_internal_metadata).include?(table)
    @existing_ids[table] = ActiveRecord::Base.connection.select_values("SELECT id FROM #{table}", 'DBLINT').map(&:to_i)
  end
end
handle_commit() click to toggle source
# File lib/dblint/checks/long_held_lock.rb, line 69
def handle_commit
  @locks_held.each do |table, details|
    next if details[:count] < 10

    main_app_caller = find_main_app_caller(caller)
    next unless main_app_caller.present?

    next if ignored?(main_app_caller)

    error_msg = format("Lock on %s held for %d statements (%0.2f ms) by '%s', transaction started by %s",
                       table.inspect, details[:count], Time.now - details[:started_at], details[:sql],
                       main_app_caller)

    next unless details[:count] > 15

    # We need an explicit begin here since we're interrupting the transaction flow
    ActiveRecord::Base.connection.execute('BEGIN')
    raise Error, error_msg

    # TODO: Add a config setting for enabling this as a warning
    # puts format('Warning: %s', error_msg)
  end
end
increment_locks_held() click to toggle source
# File lib/dblint/checks/long_held_lock.rb, line 30
def increment_locks_held
  @locks_held.each do |_, lock|
    lock[:count] += 1
  end
end