Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit df4a784ab50b7a2372030cad28ea47923f2939a8 @mauricio committed Jun 12, 2009
@@ -0,0 +1,19 @@
+Copyright (c) 2009 Maurício Linhares
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
@@ -0,0 +1,37 @@
+master_slave_adapter - maurício DOT linhares AT gmail DOT com
+====
+
+This simple plugin acts as a common ActiveRecord adapter and allows you to
+setup a master-slave environment using any database you like (and is supported
+by ActiveRecord).
+
+This plugin works by handling two connections, one to a master database,
+that will receive all non-"SELECT" statements, and another to a slave database
+that that is going to receive all SELECT statements. It also tries to do as
+little black magic as possible, it works just like any other ActiveRecord database
+adapter and performs no monkeypatching at all, so it's easy and simple to use
+and understand.
+
+The master database connection will also receive SELECT calls if a transaction
+is active at the moment or if a command is executed inside a "with_master" block:
+
+ActiveRecord::Base.with_master do # :with_master instructs the adapter
+ @users = User.all # to use the master connection inside the block
+end
+
+To use this adapter you just have to install the plugin:
+
+ruby script/plugin install git://github.com/mauricio/master_database_adapter.git
+
+And then configure it at your database.yml file:
+
+development:
+ database: sample_development
+ username: root
+ adapter: master_slave # the adapter must be set to "master_slave"
+ host: 10.21.34.80
+ master_slave_adapter: mysql # here's where you'll place the real database adapter name
+ master: # and here's where you'll add the master database configuration
+ database: talkies_development # you shouldn't specify an "adapter" here, the value at "master_slave_adapter" is going to be used
+ username: root
+ host: 10.21.34.82
@@ -0,0 +1,2 @@
+# The plugin should not require anything, ActiveRecord itself will try
+# to load the active_record/connection_adapters/master_slave_adapter
@@ -0,0 +1,6 @@
+require 'active_record/connection_adapters/abstract/database_statements'
+require 'active_record/connection_adapters/abstract/schema_statements'
+
+require 'master_slave_adapter/adapter'
+require 'master_slave_adapter/instance_methods_generation'
+require 'master_slave_adapter/active_record_extensions'
@@ -0,0 +1,64 @@
+ActiveRecord::Base.class_eval do
+
+ def reload_with_master( options = nil )
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.with_master do
+ reload_without_master( options )
+ end
+ end
+
+ alias_method_chain :reload, :master
+
+ class << self
+
+ #
+ # Call this method to force a block of code to use the master connection
+ # instead of the slave:
+ #
+ # ActiveRecord::Base.with_master do
+ # User.count( :conditions => { :login => 'testuser' } )
+ # end
+ #
+ #
+ def with_master
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.with_master do
+ yield
+ end
+ end
+
+ def master_slave_connection( config )
+ config = config.symbolize_keys
+ raise "You must provide a configuration for the master database" if config[:master].blank?
+ raise "You must provide a 'master_slave_adapter' value at your database config file" if config[:master_slave_adapter].blank?
+
+ unless self.respond_to?( "#{config[:master_slave_adapter]}_connection" )
+
+ raise "there's no method called #{config[:master_slave_adapter]}_connection"
+
+ begin
+ require 'rubygems'
+ gem "activerecord-#{config[:master_slave_adapter]}-adapter"
+ require "active_record/connection_adapters/#{config[:master_slave_adapter]}_adapter"
+ rescue LoadError
+ begin
+ require "active_record/connection_adapters/#{config[:master_slave_adapter]}_adapter"
+ rescue LoadError
+ raise "Please install the #{config[:master_slave_adapter]} adapter: `gem install activerecord-#{config[:master_slave_adapter]}-adapter` (#{$!})"
+ end
+ end
+
+ end
+
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.new( config )
+ end
+
+ def columns_with_master
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.with_master do
+ columns_without_master
+ end
+ end
+
+ alias_method_chain :columns, :master
+
+ end
+
+end
@@ -0,0 +1,100 @@
+module ActiveRecord
+
+ module ConnectionAdapters
+
+ class MasterSlaveAdapter
+
+ SELECT_METHODS = [ :select_all, :select_one, :select_rows, :select_value, :select_values ]
+
+ include ActiveSupport::Callbacks
+ define_callbacks :checkout, :checkin
+
+ checkout :test_connections
+
+ attr_accessor :connections
+ attr_accessor :database_config
+
+
+ delegate :select_all, :select_one, :select_rows, :select_value, :select_values, :to => :slave_connection
+
+ def initialize( config )
+ self.database_config = config
+ self.connections = []
+ end
+
+ def slave_connection
+ if ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.master_enabled?
+ master_connection
+ elsif @master_connection && @master_connection.open_transactions > 0
+ master_connection
+ else
+ @slave_connection ||= ActiveRecord::Base.send( "#{self.database_config[:master_slave_adapter]}_connection", self.database_config.symbolize_keys )
+ end
+ end
+
+ def reconnect!
+ @active = true
+ self.connections.each { |c| c.reconnect! }
+ end
+
+ def disconnect!
+ @active = false
+ self.connections.each { |c| c.disconnect! }
+ end
+
+ def reset!
+ self.connections.each { |c| c.reset! }
+ end
+
+ def method_missing( name, *args, &block )
+ self.master_connection.send( name.to_sym, *args, &block )
+ end
+
+ def master_connection
+ @master_connection ||= ActiveRecord::Base.send( "#{self.database_config[:master_slave_adapter]}_connection", self.database_config[:master].symbolize_keys )
+ end
+
+ def connections
+ [ @master_connection, @slave_connection ].compact
+ end
+
+ def test_connections
+ self.connections.each do |c|
+ begin
+ c.select_value( 'SELECT 1', 'test select' )
+ rescue
+ c.reconnect!
+ end
+ end
+ end
+
+ class << self
+
+ def with_master
+ enable_master
+ begin
+ yield
+ ensure
+ disable_master
+ end
+ end
+
+ def master_enabled?
+ Thread.current[ :master_slave_enabled ]
+ end
+
+ def enable_master
+ Thread.current[ :master_slave_enabled ] = true
+ end
+
+ def disable_master
+ Thread.current[ :master_slave_enabled ] = nil
+ end
+
+ end
+
+ end
+
+ end
+
+end
@@ -0,0 +1,20 @@
+ignored_methods = ActiveRecord::ConnectionAdapters::MasterSlaveAdapter::SELECT_METHODS + ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.instance_methods
+ignored_methods.uniq!
+ignored_methods.map! { |v| v.to_sym }
+
+instance_methods = ActiveRecord::ConnectionAdapters::DatabaseStatements.instance_methods + ActiveRecord::ConnectionAdapters::SchemaStatements.instance_methods
+instance_methods.uniq!
+instance_methods.map! { |v| v.to_sym }
+instance_methods.reject! { |v| ignored_methods.include?( v ) }
+
+instance_methods.each do |method|
+
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.class_eval %Q!
+
+ def #{method}( *args, &block )
+ self.master_connection.#{method}( *args, &block )
+ end
+
+ !
+
+end
@@ -0,0 +1,90 @@
+require 'rubygems'
+require 'active_record'
+require 'spec'
+
+$LOAD_PATH << File.expand_path(File.join( File.dirname( __FILE__ ), '..', 'lib' ))
+
+require 'active_record/connection_adapters/master_slave_adapter'
+
+ActiveRecord::Base.instance_eval do
+
+ def test_connection( config )
+ config = config.symbolize_keys
+
+ config[:master_slave_adapter] ? _slave : _master
+ end
+
+ def _master=( new_master )
+ @master = new_master
+ end
+
+ def _master
+ @master
+ end
+
+ def _slave=( new_slave )
+ @slave = new_slave
+ end
+
+ def _slave
+ @slave
+ end
+
+end
+
+describe ActiveRecord::ConnectionAdapters::MasterSlaveAdapter do
+
+ before(:all) do
+
+ @mocked_methods = { :verify! => true, :reconnect! => true, :run_callbacks => true, :disconnect! => true }
+
+ ActiveRecord::Base._master = mock( 'master connection', @mocked_methods.merge( :open_transactions => 0 ) )
+ ActiveRecord::Base._slave = mock( 'slave connection', @mocked_methods )
+
+ @master_connection = ActiveRecord::Base._master
+ @slave_connection = ActiveRecord::Base._slave
+
+ @database_setup = {
+ :adapter => 'master_slave',
+ :username => 'root',
+ :database => 'master_slave_test',
+ :master_slave_adapter => 'test',
+ :master => { :username => 'root', :database => 'master_slave_test' }
+ }
+
+ ActiveRecord::Base.establish_connection( @database_setup )
+
+ end
+
+ [ :select_all, :select_one, :select_rows, :select_value, :select_values ].each do |method|
+
+ it "Should send the method '#{method}' to the slave connection" do
+ @master_connection.stub!( :open_transactions ).and_return( 0 )
+ @slave_connection.should_receive( method ).and_return( true )
+ ActiveRecord::Base.connection.send( method )
+ end
+
+ it "Should send the method '#{method}' to the master connection if with_master was specified" do
+ @master_connection.should_receive( method ).and_return( true )
+ ActiveRecord::Base.with_master do
+ ActiveRecord::Base.connection.send( method )
+ end
+ end
+
+ end
+
+ it 'Should be a master slave connection' do
+ ActiveRecord::Base.connection.class.should == ActiveRecord::ConnectionAdapters::MasterSlaveAdapter
+ end
+
+ it 'Should have a master connection' do
+ ActiveRecord::Base.connection.master_connection.should == @master_connection
+ end
+
+ it 'Should have a slave connection' do
+ @master_connection.stub!( :open_transactions ).and_return( 0 )
+ ActiveRecord::Base.connection.slave_connection.should == @slave_connection
+ end
+
+
+end

0 comments on commit df4a784

Please sign in to comment.