module Hijacker

Attributes

config[RW]
master[RW]
sister[RW]
valid_routes[W]

Public Class Methods

check_connection() click to toggle source

just calling establish_connection doesn’t actually check to see if we’ve established a VALID connection. a call to connection will check this, and throw an error if the connection’s invalid. It is important to catch the error and reconnect to a known valid database or rails will get stuck. This is because once we establish a connection to an invalid database, the next request will do a courteousy touch to the invalid database before reaching establish_connection and throw an error, preventing us from retrying to establish a valid connection and effectively locking us out of the app.

# File lib/hijacker.rb, line 195
def self.check_connection
  ::ActiveRecord::Base.connection
end
connect(target_name, sister_name = nil, options = {}) click to toggle source

Manually establishes a new connection to the database.

Background: every time rails gets information from the database, it uses the last established connection. So, although we’ve already established a connection to a root db (“crystal”, in this case), if we establish a new connection, all subsequent database calls will use these settings instead (well, until it’s called again when it gets another request).

Note that you can manually call this from script/console (or wherever) to connect to the database you want, ex Hijacker.connect(“database”)

# File lib/hijacker.rb, line 41
def self.connect(target_name, sister_name = nil, options = {})
  original_database = Hijacker::Database.current

  begin
    raise InvalidDatabase.new(nil, 'master cannot be nil') if target_name.nil?

    target_name = target_name.downcase
    sister_name = sister_name.downcase unless sister_name.nil?

    if already_connected?(target_name, sister_name)
      run_after_hijack_callback
      return "Already connected to #{target_name}"
    end

    database = determine_database(target_name, sister_name)

    establish_connection_to_database(database)

    check_connection

    if database.sister?
      self.master = database.master.name
      self.sister = database.name
    else
      self.master = database.name
      self.sister = nil
    end

    # don't cache sister site
    cache_database_route(target_name, database) unless sister_name

    # Do this even on a site without a master so we reconnect these models
    connect_sister_site_models(database.master || database)

    reenable_query_caching

    run_after_hijack_callback
  rescue
    if original_database.present?
      establish_connection_to_database(original_database)
    else
      self.establish_root_connection
    end
    raise
  end
end
connect_sister_site_models(master_database) click to toggle source

very small chance this will raise, but if it does, we will still handle it the same as Hijacker.connect so we don’t lock up the app.

Also note that sister site models share a connection via minor management of AR’s connection_pool stuff, and will use ActiveRecord::Base.connection_pool if we’re not in a sister-site situation

# File lib/hijacker.rb, line 94
def self.connect_sister_site_models(master_database)
  master_db_connection_pool = if processing_sister_site?
                                nil
                              else
                                ActiveRecord::Base.connection_pool
                              end
  master_config = connection_config(master_database)

  config[:sister_site_models].each do |model_name|
    klass = model_name.constantize

    klass.establish_connection(master_config)

    if !master_db_connection_pool
      begin
        klass.connection
      rescue
        klass.establish_connection(root_config)
        raise Hijacker::InvalidDatabase.new(database.name)
      end
      master_db_connection_pool = klass.connection_pool
    else
      ActiveRecord::Base.connection_handler.connection_pools[model_name] = master_db_connection_pool
    end
  end
end
connect_to_master(db_name) click to toggle source
# File lib/hijacker.rb, line 26
def self.connect_to_master(db_name)
  connect(*Hijacker::Database.find_master_and_sister_for(db_name))
end
current_client() click to toggle source
# File lib/hijacker.rb, line 177
def self.current_client
  sister || master
end
database_configurations() click to toggle source
# File lib/hijacker.rb, line 159
def self.database_configurations
  ActiveRecord::Base.configurations
end
do_hijacking?() click to toggle source
# File lib/hijacker.rb, line 181
def self.do_hijacking?
  (Hijacker.config[:hosted_environments] || %w[staging production]).
    include?(ENV['RAILS_ENV'] || Rails.env)
end
establish_root_connection() click to toggle source

this should establish a connection to a database containing the bare minimum for loading the app, usually a sessions table if using sql-based sessions.

# File lib/hijacker.rb, line 165
def self.establish_root_connection
  ActiveRecord::Base.establish_connection('root')
end
processing_sister_site?() click to toggle source
# File lib/hijacker.rb, line 169
def self.processing_sister_site?
  !sister.nil?
end
root_config() click to toggle source
# File lib/hijacker.rb, line 155
def self.root_config
  database_configurations.fetch('root').with_indifferent_access
end
root_connection() click to toggle source

The advantage of using this over just calling ActiveRecord::Base.establish_connection (without arguments) to reconnect to the root database is that reusing the same connection greatly reduces context switching overhead etc involved with establishing a connection to the database. It may seem trivial, but it actually seems to speed things up by ~ 1/3 for already fast requests (probably less noticeable on slower pages).

Note: does not hijack, just returns the root connection (i.e. AR::Base will maintain its connection)

# File lib/hijacker.rb, line 144
def self.root_connection
  unless $hijacker_root_connection
    current_config = ActiveRecord::Base.connection.config
    ActiveRecord::Base.establish_connection('root') # establish with defaults
    $hijacker_root_connection = ActiveRecord::Base.connection
    ActiveRecord::Base.establish_connection(current_config) # reconnect, we don't intend to hijack
  end

  $hijacker_root_connection
end
temporary_sister_connect(db, &block) click to toggle source

connects the sister_site_models to db while calling the block if db and self.master differ

# File lib/hijacker.rb, line 123
def self.temporary_sister_connect(db, &block)
  processing_sister_site = (db != master && db != sister)
  self.sister = db if processing_sister_site
  self.connect_sister_site_models(db) if processing_sister_site
  result = block.call
  self.connect_sister_site_models(self.master) if processing_sister_site
  self.sister = nil if processing_sister_site

  result
end
valid_routes() click to toggle source
# File lib/hijacker.rb, line 22
def self.valid_routes
  @valid_routes ||= {}
end

Private Class Methods

already_connected?(new_master, new_sister) click to toggle source
# File lib/hijacker.rb, line 201
def self.already_connected?(new_master, new_sister)
  current_client == new_master && sister == new_sister
end
cache_database_route(requested_db_name, actual_database) click to toggle source
# File lib/hijacker.rb, line 220
def self.cache_database_route(requested_db_name, actual_database)
  valid_routes[requested_db_name] ||= actual_database
end
connection_config(database) click to toggle source
# File lib/hijacker.rb, line 242
def self.connection_config(database)
  hostname = database.host.hostname
  port = database.host.port || root_config['port']
  root_config.merge('database' => database.name,
                    'host' => hostname,
                    'port' => port)
end
determine_database(target_name, sister_name) click to toggle source
# File lib/hijacker.rb, line 205
def self.determine_database(target_name, sister_name)
  if sister_name
    database = Hijacker::Database.find_by_name(sister_name)
    raise(Hijacker::InvalidDatabase.new(sister_name)) if database.nil?
    database
  elsif valid_routes[target_name]
    valid_routes[target_name] # cached valid database
  else
    database = Hijacker::Alias.find_by_name(target_name).try(:database) ||
               Hijacker::Database.find_by_name(target_name)
    raise(Hijacker::InvalidDatabase.new(target_name)) if database.nil?
    database
  end
end
establish_connection_to_database(database) click to toggle source
# File lib/hijacker.rb, line 224
def self.establish_connection_to_database(database)
  ::ActiveRecord::Base.establish_connection(connection_config(database))
end
reenable_query_caching() click to toggle source

This is a hack to get query caching back on. For some reason when we reconnect the database during the request, it stops doing query caching. We couldn’t find how it’s used by rails originally, but if you turn on query caching then start a cache block to initialize the @query_cache instance variable in the connection, AR will from then on build on that empty @query_cache hash. You have to do both ‘cuz without the latter there will be no @query_cache available. Maybe someday we’ll submit a ticket to Rails.

# File lib/hijacker.rb, line 235
def self.reenable_query_caching
  if ::ActionController::Base.perform_caching
    ::ActiveRecord::Base.connection.instance_variable_set("@query_cache_enabled", true)
    ::ActiveRecord::Base.connection.cache do;end
  end
end
run_after_hijack_callback() click to toggle source
# File lib/hijacker.rb, line 250
def self.run_after_hijack_callback
  config[:after_hijack].call if config[:after_hijack]
end