Safe methods are those that should either go to the slave ONLY or go to the current active connection.
if master should be the default db
defaults to Rails.env if multi_db is used with Rails defaults to ‘development’ when used outside Rails
a list of models that should always go directly to the master
Example:
MultiDb::ConnectionProxy.master_models = ['MySessionStore', 'PaymentTransaction']
decides if we should switch to the next reader automatically. If set to false, an after|before_filter in the ApplicationController has to do this. This will not affect failover if a master is unavailable.
# File lib/multi_db/connection_proxy.rb, line 97 def initialize(master, slaves, scheduler = Scheduler) @slaves = scheduler.new(slaves) @master = master @reconnect = false @query_cache = {} if self.class.defaults_to_master self.current = @master self.master_depth = 1 else self.current = @slaves.current self.master_depth = 0 end end
Replaces the connection of ActiveRecord::Base with a proxy and establishes the connections to the slaves.
# File lib/multi_db/connection_proxy.rb, line 48 def setup!(scheduler = Scheduler) self.master_models ||= DEFAULT_MASTER_MODELS self.environment ||= (defined?(Rails) ? Rails.env : 'development') self.sticky_slave ||= false master = ActiveRecord::Base slaves = init_slaves raise "No slaves databases defined for environment: #{self.environment}" if slaves.empty? master.send :include, MultiDb::ActiveRecordExtensions ActiveRecord::Observer.send :include, MultiDb::ObserverExtensions master.connection_proxy = new(master, slaves, scheduler) master.logger.info("** multi_db with master and #{slaves.length} slave#{"s" if slaves.length > 1} loaded.") end
Slave entries in the database.yml must be named like this
development_slave_database:
or
development_slave_database1:
or
production_slave_database_someserver:
These would be available later as MultiDb::SlaveDatabaseSomeserver
# File lib/multi_db/connection_proxy.rb, line 71 def init_slaves [].tap do |slaves| ActiveRecord::Base.configurations.each do |name, values| if name.to_s =~ /#{self.environment}_(slave_database.*)/ weight = if values['weight'].blank? 1 else (v=values['weight'].to_i.abs).zero?? 1 : v end MultiDb.module_eval %Q{ class #{$1.camelize} < ActiveRecord::Base self.abstract_class = true establish_connection :#{name} WEIGHT = #{weight} unless const_defined?('WEIGHT') end }, __FILE__, __LINE__ slaves << "MultiDb::#{$1.camelize}".constantize end end end end
Calls the method on master/slave and dynamically creates a new method on success to speed up subsequent calls
# File lib/multi_db/connection_proxy.rb, line 145 def method_missing(method, *args, &block) send(target_method(method), method, *args, &block).tap do create_delegation_method!(method) end end
Switches to the next slave database for read operations. Fails over to the master database if all slaves are unavailable.
# File lib/multi_db/connection_proxy.rb, line 153 def next_reader! return if master_depth > 0 # don't if in with_master block self.current = @slaves.next rescue Scheduler::NoMoreItems logger.warn "[MULTIDB] All slaves are blacklisted. Reading from master" self.current = @master end
# File lib/multi_db/connection_proxy.rb, line 115 def scheduler @slaves end
# File lib/multi_db/connection_proxy.rb, line 111 def slave @slaves.current end
# File lib/multi_db/connection_proxy.rb, line 139 def transaction(start_db_transaction = true, &block) with_master { @master.retrieve_connection.transaction(start_db_transaction, &block) } end
# File lib/multi_db/connection_proxy.rb, line 120 def with_master self.current = @master self.master_depth += 1 yield ensure self.master_depth -= 1 self.current = slave if (master_depth <= 0) end
# File lib/multi_db/connection_proxy.rb, line 130 def with_slave self.current = slave self.master_depth -= 1 yield ensure self.master_depth += 1 self.current = @master if (master_depth > 0) end
# File lib/multi_db/connection_proxy.rb, line 163 def create_delegation_method!(method) self.instance_eval %Q{ def #{method}(*args, &block) #{'next_reader!' unless self.class.sticky_slave || unsafe?(method)} #{target_method(method)}(:#{method}, *args, &block) end }, __FILE__, __LINE__ end
# File lib/multi_db/connection_proxy.rb, line 216 def logger ActiveRecord::Base.logger end
# File lib/multi_db/connection_proxy.rb, line 212 def master? current == @master end
# File lib/multi_db/connection_proxy.rb, line 202 def raise_master_error(error) logger.fatal "[MULTIDB] Error accessing master database. Scheduling reconnect" @reconnect = true raise error end
# File lib/multi_db/connection_proxy.rb, line 197 def reconnect_master! @master.retrieve_connection.reconnect! @reconnect = false end
# File lib/multi_db/connection_proxy.rb, line 183 def send_to_current(method, *args, &block) reconnect_master! if @reconnect && master? current.retrieve_connection.send(method, *args, &block) rescue NotImplementedError, NoMethodError raise rescue => e # TODO don't rescue everything raise_master_error(e) if master? logger.warn "[MULTIDB] Error reading from slave database" logger.error %Q(#{e.message}\n#{e.backtrace.join("\n")}) @slaves.blacklist!(current) next_reader! retry end
# File lib/multi_db/connection_proxy.rb, line 176 def send_to_master(method, *args, &block) reconnect_master! if @reconnect @master.retrieve_connection.send(method, *args, &block) rescue => e raise_master_error(e) end
# File lib/multi_db/connection_proxy.rb, line 172 def target_method(method) unsafe?(method) ? :send_to_master : :send_to_current end
# File lib/multi_db/connection_proxy.rb, line 208 def unsafe?(method) !SAFE_METHODS.include?(method) end