module Hijacker
Attributes
Public Class Methods
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
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
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
# File lib/hijacker.rb, line 26 def self.connect_to_master(db_name) connect(*Hijacker::Database.find_master_and_sister_for(db_name)) end
# File lib/hijacker.rb, line 177 def self.current_client sister || master end
# File lib/hijacker.rb, line 159 def self.database_configurations ActiveRecord::Base.configurations end
# File lib/hijacker.rb, line 181 def self.do_hijacking? (Hijacker.config[:hosted_environments] || %w[staging production]). include?(ENV['RAILS_ENV'] || Rails.env) end
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
# File lib/hijacker.rb, line 169 def self.processing_sister_site? !sister.nil? end
# File lib/hijacker.rb, line 155 def self.root_config database_configurations.fetch('root').with_indifferent_access end
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
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
# File lib/hijacker.rb, line 22 def self.valid_routes @valid_routes ||= {} end
Private Class Methods
# File lib/hijacker.rb, line 201 def self.already_connected?(new_master, new_sister) current_client == new_master && sister == new_sister end
# File lib/hijacker.rb, line 220 def self.cache_database_route(requested_db_name, actual_database) valid_routes[requested_db_name] ||= actual_database end
# 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
# 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
# File lib/hijacker.rb, line 224 def self.establish_connection_to_database(database) ::ActiveRecord::Base.establish_connection(connection_config(database)) end
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
# File lib/hijacker.rb, line 250 def self.run_after_hijack_callback config[:after_hijack].call if config[:after_hijack] end