module Isolator

Isolator detects unsafe operations performed within DB transactions.

Constants

VERSION

Attributes

backtrace_cleaner[RW]
backtrace_length[RW]
debug_enabled[RW]
default_connection_id[RW]
default_threshold[RW]
state[RW]

Public Class Methods

adapters() click to toggle source
# File lib/isolator.rb, line 166
def adapters
  @adapters ||= Isolator::SimpleHashie.new
end
clear_transactions!() click to toggle source
# File lib/isolator.rb, line 147
def clear_transactions!
  state[:transactions]&.clear
end
config() click to toggle source
# File lib/isolator.rb, line 39
def config
  @config ||= Configuration.new
end
configure() { |config| ... } click to toggle source
# File lib/isolator.rb, line 43
def configure
  yield config
end
current_transactions(connection_id = default_connection_id.call) click to toggle source
# File lib/isolator.rb, line 93
def current_transactions(connection_id = default_connection_id.call)
  state[:transactions]&.[](connection_id) || 0
end
decr_thresholds!() click to toggle source
# File lib/isolator.rb, line 113
def decr_thresholds!
  self.default_threshold -= 1
  return unless state[:thresholds]

  state[:thresholds].transform_values!(&:pred)

  debug!("Thresholds were decremented")
end
decr_transactions!(connection_id = default_connection_id.call) click to toggle source
# File lib/isolator.rb, line 137
def decr_transactions!(connection_id = default_connection_id.call)
  state[:transactions][connection_id] -= 1

  finish! if current_transactions(connection_id) == (connection_threshold(connection_id) - 1)

  state[:transactions].delete(connection_id) if state[:transactions][connection_id].zero?

  debug!("Transaction closed for connection #{connection_id} (total: #{state[:transactions][connection_id]}, threshold: #{state[:thresholds]&.[](connection_id) || default_threshold})")
end
disable() { || ... } click to toggle source

Accepts block and disable Isolator within

# File lib/isolator.rb, line 60
def disable
  return yield if disabled?
  res = nil
  begin
    disable!
    res = yield
  ensure
    enable!
  end
  res
end
disable!() click to toggle source
# File lib/isolator.rb, line 55
def disable!
  state[:disabled] = true
end
disabled?() click to toggle source
# File lib/isolator.rb, line 162
def disabled?
  state[:disabled] == true
end
enable() { || ... } click to toggle source

Accepts block and enable Isolator within

# File lib/isolator.rb, line 73
def enable
  return yield if enabled?
  res = nil
  begin
    enable!
    res = yield
  ensure
    disable!
  end
  res
end
enable!() click to toggle source
# File lib/isolator.rb, line 51
def enable!
  state[:disabled] = false
end
enabled?() click to toggle source
# File lib/isolator.rb, line 158
def enabled?
  !disabled?
end
incr_thresholds!() click to toggle source
# File lib/isolator.rb, line 104
def incr_thresholds!
  self.default_threshold += 1
  return unless state[:thresholds]

  state[:thresholds].transform_values!(&:succ)

  debug!("Thresholds were incremented")
end
incr_transactions!(connection_id = default_connection_id.call) click to toggle source
# File lib/isolator.rb, line 122
def incr_transactions!(connection_id = default_connection_id.call)
  state[:transactions] ||= Hash.new { |h, k| h[k] = 0 }
  state[:transactions][connection_id] += 1

  # Workaround to track threshold changes made before opening a connection
  pending_threshold = state[:thresholds]&.delete(0)
  if pending_threshold
    state[:thresholds][connection_id] = pending_threshold
  end

  debug!("Transaction opened for connection #{connection_id} (total: #{state[:transactions][connection_id]}, threshold: #{state[:thresholds]&.fetch(connection_id, default_threshold)})")

  start! if current_transactions(connection_id) == connection_threshold(connection_id)
end
load_ignore_config(path) click to toggle source
# File lib/isolator.rb, line 170
def load_ignore_config(path)
  warn "[DEPRECATION] `load_ignore_config` is deprecated. Please use `Isolator::Ignorer.prepare` instead."
  Isolator::Ignorer.prepare(path: path)
end
notify(exception:, backtrace:) click to toggle source
# File lib/isolator.rb, line 47
def notify(exception:, backtrace:)
  Notifier.new(exception, backtrace).call
end
set_connection_threshold(val, connection_id = default_connection_id.call) click to toggle source
# File lib/isolator.rb, line 97
def set_connection_threshold(val, connection_id = default_connection_id.call)
  state[:thresholds] ||= Hash.new { |h, k| h[k] = Isolator.default_threshold }
  state[:thresholds][connection_id] = val

  debug!("Threshold value was changed for connection #{connection_id}: #{val}")
end
transactions_threshold(connection_id = default_connection_id.call) click to toggle source
# File lib/isolator.rb, line 89
def transactions_threshold(connection_id = default_connection_id.call)
  connection_threshold(connection_id)
end
transactions_threshold=(val) click to toggle source
# File lib/isolator.rb, line 85
def transactions_threshold=(val)
  set_connection_threshold(val)
end
within_transaction?() click to toggle source
# File lib/isolator.rb, line 151
def within_transaction?
  state[:transactions]&.each do |connection_id, transaction_count|
    return true if transaction_count >= connection_threshold(connection_id)
  end
  false
end

Private Class Methods

colorize_debug(msg) click to toggle source
# File lib/isolator.rb, line 206
def colorize_debug(msg)
  return msg unless $stdout.tty?

  "\u001b[31;1m#{msg}\u001b[0m"
end
connection_threshold(connection_id) click to toggle source
# File lib/isolator.rb, line 184
def connection_threshold(connection_id)
  state[:thresholds]&.[](connection_id) || default_threshold
end
debug!(msg) click to toggle source
# File lib/isolator.rb, line 188
def debug!(msg)
  return unless debug_enabled
  msg = "[ISOLATOR DEBUG] #{msg}"

  if backtrace_cleaner && backtrace_length.positive?
    source = extract_source_location(caller)

    msg = "#{msg}\n  ↳ #{source.join("\n")}" unless source.empty?
  end

  $stdout.puts(colorize_debug(msg))
end
extract_source_location(locations) click to toggle source
# File lib/isolator.rb, line 201
def extract_source_location(locations)
  backtrace_cleaner.call(locations.lazy)
    .take(backtrace_length).to_a
end