class MultiDb::ConnectionProxy

Constants

DEFAULT_MASTER_MODELS
SAFE_METHODS

Safe methods are those that should either go to the slave ONLY or go to the current active connection.

Attributes

defaults_to_master[RW]

if master should be the default db

environment[RW]

defaults to Rails.env if multi_db is used with Rails defaults to ‘development’ when used outside Rails

master_models[RW]

a list of models that should always go directly to the master

Example:

MultiDb::ConnectionProxy.master_models = ['MySessionStore', 'PaymentTransaction']
sticky_slave[RW]

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.

master[RW]

Public Class Methods

new(master, slaves, scheduler = Scheduler) click to toggle source
# 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
setup!(scheduler = Scheduler) click to toggle source

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

Protected Class Methods

init_slaves() click to toggle source

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

Public Instance Methods

method_missing(method, *args, &block) click to toggle source

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
next_reader!() click to toggle source

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
scheduler() click to toggle source
# File lib/multi_db/connection_proxy.rb, line 115
def scheduler
  @slaves
end
slave() click to toggle source
# File lib/multi_db/connection_proxy.rb, line 111
def slave
  @slaves.current
end
transaction(start_db_transaction = true, &block) click to toggle source
# 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
with_master() { || ... } click to toggle source
# 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
with_slave() { || ... } click to toggle source
# 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

Protected Instance Methods

create_delegation_method!(method) click to toggle source
# 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
logger() click to toggle source
# File lib/multi_db/connection_proxy.rb, line 216
def logger
  ActiveRecord::Base.logger
end
master?() click to toggle source
# File lib/multi_db/connection_proxy.rb, line 212
def master?
  current == @master
end
raise_master_error(error) click to toggle source
# 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
reconnect_master!() click to toggle source
# File lib/multi_db/connection_proxy.rb, line 197
def reconnect_master!
  @master.retrieve_connection.reconnect!
  @reconnect = false
end
send_to_current(method, *args, &block) click to toggle source
# 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
send_to_master(method, *args, &block) click to toggle source
# 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
target_method(method) click to toggle source
# File lib/multi_db/connection_proxy.rb, line 172
def target_method(method)
  unsafe?(method) ? :send_to_master : :send_to_current
end
unsafe?(method) click to toggle source
# File lib/multi_db/connection_proxy.rb, line 208
def unsafe?(method)
  !SAFE_METHODS.include?(method)
end