Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mauricio committed Jun 12, 2009
0 parents commit df4a784
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 0 deletions.
19 changes: 19 additions & 0 deletions LICENSE
@@ -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.
37 changes: 37 additions & 0 deletions README
@@ -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
2 changes: 2 additions & 0 deletions init.rb
@@ -0,0 +1,2 @@
# The plugin should not require anything, ActiveRecord itself will try
# to load the active_record/connection_adapters/master_slave_adapter
6 changes: 6 additions & 0 deletions lib/active_record/connection_adapters/master_slave_adapter.rb
@@ -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'
64 changes: 64 additions & 0 deletions lib/master_slave_adapter/active_record_extensions.rb
@@ -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
100 changes: 100 additions & 0 deletions lib/master_slave_adapter/adapter.rb
@@ -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
20 changes: 20 additions & 0 deletions lib/master_slave_adapter/instance_methods_generation.rb
@@ -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
90 changes: 90 additions & 0 deletions specs/specs.rb
@@ -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.