master_slave_adapter_tcurdt 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .rvmrc
2
+ nbproject
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 Maurício Linhares, Torsten Curdt
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README ADDED
@@ -0,0 +1,43 @@
1
+ master_slave_adapter
2
+ ====
3
+
4
+ This simple plugin acts as a common ActiveRecord adapter and allows you to
5
+ setup a master-slave environment using any database you like (and is supported
6
+ by ActiveRecord).
7
+
8
+ This plugin works by handling two connections, one to a master database,
9
+ that will receive all non-"SELECT" statements, and another to a slave database
10
+ that that is going to receive all SELECT statements. It also tries to do as
11
+ little black magic as possible, it works just like any other ActiveRecord database
12
+ adapter and performs no monkeypatching at all, so it's easy and simple to use
13
+ and understand.
14
+
15
+ The master database connection will also receive SELECT calls if a transaction
16
+ is active at the moment or if a command is executed inside a "with_master" block:
17
+
18
+ ActiveRecord::Base.with_master do # :with_master instructs the adapter
19
+ @users = User.all # to use the master connection inside the block
20
+ end
21
+
22
+ To use this adapter you just have to install the plugin:
23
+
24
+ ruby script/plugin install git://github.com/tcurdt/master_slave_adapter_mauricio.git
25
+
26
+ And then configure it at your database.yml file:
27
+
28
+ development:
29
+ database: sample_development
30
+ username: root
31
+ adapter: master_slave # the adapter must be set to "master_slave"
32
+ host: 10.21.34.80
33
+ master_slave_adapter: mysql # here's where you'll place the real database adapter name
34
+ disable_connection_test: true # this will disable the connection test before use,
35
+ # can possibly improve the performance but you could also
36
+ # hit stale connections, default is false
37
+ eager_load_connections: true # connections are lazy loaded by default, you can load gem eagerly setting this to true
38
+ master: # and here's where you'll add the master database configuration
39
+ database: talkies_development # you shouldn't specify an "adapter" here, the
40
+ username: root # value at "master_slave_adapter" is going to be used
41
+ host: 10.21.34.82
42
+ adapter: postgresql # you can use another adapter for the master connection if needed
43
+ # if you don't set it the "master_slave_adapter" property will be used
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,6 @@
1
+ require 'active_record/connection_adapters/abstract/database_statements'
2
+ require 'active_record/connection_adapters/abstract/schema_statements'
3
+
4
+ require 'master_slave_adapter/adapter'
5
+ require 'master_slave_adapter/instance_methods_generation'
6
+ require 'master_slave_adapter/active_record_extensions'
@@ -0,0 +1,7 @@
1
+ require 'active_record/connection_adapters/master_slave_adapter'
2
+
3
+ require 'master_slave_adapter/active_record_extension'
4
+ require 'master_slave_adapter/adapter'
5
+ require 'master_slave_adapter/instance_methods_generation'
6
+
7
+ require 'master_slave_adapter/version'
@@ -0,0 +1,82 @@
1
+ ActiveRecord::Base.class_eval do
2
+
3
+ def reload_with_master( options = nil )
4
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.with_master do
5
+ reload_without_master( options )
6
+ end
7
+ end
8
+
9
+ alias_method_chain :reload, :master
10
+
11
+ class << self
12
+
13
+ # Call this method to force a block of code to use the master connection
14
+ #
15
+ # ActiveRecord::Base.with_master do
16
+ # User.count( :conditions => { :login => 'testuser' } )
17
+ # end
18
+ def with_master
19
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.with_master do
20
+ yield
21
+ end
22
+ end
23
+
24
+ # Call this method to force a block of code to use the slave connection
25
+ #
26
+ # ActiveRecord::Base.with_slave do
27
+ # User.count( :conditions => { :login => 'testuser' } )
28
+ # end
29
+ def with_slave
30
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.with_slave do
31
+ yield
32
+ end
33
+ end
34
+
35
+ # Call this method to force a certain binlog position.
36
+ # Going to the slave if it's already at the position otherwise
37
+ # falling back to master.
38
+ #
39
+ # consistency = ActiveRecord::Base.with_consistency(consistency) do
40
+ # User.count( :conditions => { :login => 'testuser' } )
41
+ # end
42
+ def with_consistency(consistency)
43
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.with_consistency(consistency) do
44
+ yield
45
+ end
46
+ end
47
+
48
+ def master_slave_connection( config )
49
+ config = config.symbolize_keys
50
+ raise "You must provide a configuration for the master database - #{config.inspect}" if config[:master].blank?
51
+ raise "You must provide a 'master_slave_adapter' value at your database config file" if config[:master_slave_adapter].blank?
52
+
53
+ unless self.respond_to?( "#{config[:master_slave_adapter]}_connection" )
54
+
55
+ begin
56
+ require 'rubygems'
57
+ gem "activerecord-#{config[:master_slave_adapter]}-adapter"
58
+ require "active_record/connection_adapters/#{config[:master_slave_adapter]}_adapter"
59
+ rescue LoadError
60
+ begin
61
+ require "active_record/connection_adapters/#{config[:master_slave_adapter]}_adapter"
62
+ rescue LoadError
63
+ raise "Please install the #{config[:master_slave_adapter]} adapter: `gem install activerecord-#{config[:master_slave_adapter]}-adapter` (#{$!})"
64
+ end
65
+ end
66
+
67
+ end
68
+
69
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.new( config )
70
+ end
71
+
72
+ def columns_with_master
73
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.with_master do
74
+ columns_without_master
75
+ end
76
+ end
77
+
78
+ alias_method_chain :columns, :master
79
+
80
+ end
81
+
82
+ end
@@ -0,0 +1,256 @@
1
+ module ActiveRecord
2
+
3
+ module ConnectionAdapters
4
+
5
+ class MasterSlaveAdapter
6
+
7
+ class Clock
8
+ include Comparable
9
+ attr_reader :file, :position
10
+ def initialize(file, position)
11
+ @file, @position = file, position.to_i
12
+ end
13
+ def <=>(other)
14
+ @file == other.file ? @position <=> other.position : @file <=> other.file
15
+ end
16
+ def to_s
17
+ "#{@file}@#{@position}"
18
+ end
19
+ def self.zero
20
+ @zero ||= Clock.new('', 0)
21
+ end
22
+ end
23
+
24
+ SELECT_METHODS = [ :select_all, :select_one, :select_rows, :select_value, :select_values ]
25
+
26
+ include ActiveSupport::Callbacks
27
+ define_callbacks :checkout, :checkin
28
+
29
+ checkout :test_connections
30
+
31
+ attr_accessor :connections
32
+ attr_accessor :master_config
33
+ attr_accessor :slave_config
34
+ attr_accessor :disable_connection_test
35
+
36
+
37
+ delegate :select_all, :select_one, :select_rows, :select_value, :select_values, :to => :select_connection
38
+
39
+ def initialize( config )
40
+ if config[:master].blank?
41
+ raise "There is no :master config in the database configuration provided -> #{config.inspect} "
42
+ end
43
+ self.slave_config = config.symbolize_keys
44
+ self.master_config = self.slave_config.delete(:master).symbolize_keys
45
+ self.slave_config[:adapter] = self.slave_config.delete(:master_slave_adapter)
46
+ self.master_config[:adapter] ||= self.slave_config[:adapter]
47
+ self.disable_connection_test = self.slave_config.delete( :disable_connection_test ) == 'true'
48
+ self.connections = []
49
+ if self.slave_config.delete( :eager_load_connections ) == 'true'
50
+ connect_to_master
51
+ connect_to_slave
52
+ end
53
+ end
54
+
55
+ def insert(sql, *args)
56
+ on_write do
57
+ self.master_connection.insert(sql, *args)
58
+ end
59
+ end
60
+
61
+ def update(sql, *args)
62
+ on_write do
63
+ self.master_connection.update(sql, *args)
64
+ end
65
+ end
66
+
67
+ def delete(sql, *args)
68
+ on_write do
69
+ self.master_connection.delete(sql, *args)
70
+ end
71
+ end
72
+
73
+ def commit_db_transaction()
74
+ on_write do
75
+ self.master_connection.commit_db_transaction()
76
+ end
77
+ end
78
+
79
+ def reconnect!
80
+ @active = true
81
+ self.connections.each { |c| c.reconnect! }
82
+ end
83
+
84
+ def disconnect!
85
+ @active = false
86
+ self.connections.each { |c| c.disconnect! }
87
+ end
88
+
89
+ def reset!
90
+ self.connections.each { |c| c.reset! }
91
+ end
92
+
93
+ def method_missing( name, *args, &block )
94
+ self.master_connection.send( name.to_sym, *args, &block )
95
+ end
96
+
97
+ def master_connection
98
+ connect_to_master
99
+ end
100
+
101
+ def slave_connection
102
+ connect_to_slave
103
+ end
104
+
105
+ def connections
106
+ [ @master_connection, @slave_connection ].compact
107
+ end
108
+
109
+ def test_connections
110
+ return if self.disable_connection_test
111
+ self.connections.each do |c|
112
+ begin
113
+ c.select_value( 'SELECT 1', 'test select' )
114
+ rescue
115
+ c.reconnect!
116
+ end
117
+ end
118
+ end
119
+
120
+ class << self
121
+
122
+ def with_master
123
+ Thread.current[:master_slave_select_connection] = [ :master ] + (Thread.current[:master_slave_select_connection]||[])
124
+ result = yield
125
+ Thread.current[:master_slave_select_connection] = Thread.current[:master_slave_select_connection].drop(1)
126
+ result
127
+ end
128
+
129
+ def with_slave
130
+ Thread.current[:master_slave_select_connection] = [ :slave ] + (Thread.current[:master_slave_select_connection]||[])
131
+ result = yield
132
+ Thread.current[:master_slave_select_connection] = Thread.current[:master_slave_select_connection].drop(1)
133
+ result
134
+ end
135
+
136
+
137
+ def with_consistency(clock)
138
+ raise ArgumentError, "consistency cannot be nil" if !clock
139
+
140
+ Thread.current[:master_slave_select_connection] = [ nil ] + (Thread.current[:master_slave_select_connection]||[])
141
+ Thread.current[:master_slave_clock] = [ clock || Clock::zero ] + (Thread.current[:master_slave_clock]||[])
142
+
143
+ yield
144
+ result = Thread.current[:master_slave_clock][0]
145
+
146
+ Thread.current[:master_slave_clock] = Thread.current[:master_slave_clock].drop(1)
147
+ Thread.current[:master_slave_select_connection] = Thread.current[:master_slave_select_connection].drop(1)
148
+ result
149
+ end
150
+
151
+ def reset!
152
+ Thread.current[:master_slave_select_connection] = nil
153
+ Thread.current[:master_slave_clock] = nil
154
+ end
155
+
156
+ def master_forced?
157
+ Thread.current[:master_slave_enabled] == true
158
+ end
159
+
160
+ def master_forced=(state)
161
+ Thread.current[:master_slave_enabled] = state ? true : nil
162
+ end
163
+
164
+ def using_master?
165
+ if Thread.current[:master_slave_select_connection]
166
+ Thread.current[:master_slave_select_connection][0] == :master
167
+ else
168
+ nil
169
+ end
170
+ end
171
+
172
+ end
173
+
174
+ private
175
+
176
+ def update_clock
177
+ # update the clock, if there was problem keep using the old one
178
+ Thread.current[:master_slave_clock][0] = master_clock || Thread.current[:master_slave_clock][0]
179
+ # it's a write so from now on we use the master connection
180
+ # as replication is not likely to be that fast
181
+ Thread.current[:master_slave_select_connection][0] = :master
182
+ end
183
+
184
+ def on_write
185
+ result = yield
186
+ if !MasterSlaveAdapter.master_forced? && @master_connection.open_transactions == 0
187
+ update_clock
188
+ end
189
+ result
190
+ end
191
+
192
+ def connection_for_clock(required_clock)
193
+ if required_clock
194
+ # check the slave for it's replication state
195
+ if clock = slave_clock
196
+ if clock >= required_clock
197
+ # slave is safe to use
198
+ :slave
199
+ else
200
+ # slave is not there yet
201
+ :master
202
+ end
203
+ else
204
+ # not getting slave status, better turn to master
205
+ # maybe this should be logged or raised?
206
+ :master
207
+ end
208
+ else
209
+ # no required clock so slave is good enough
210
+ :slave
211
+ end
212
+ end
213
+
214
+ def select_connection
215
+ connection_stack = Thread.current[:master_slave_select_connection] ||= []
216
+ clock_stack = Thread.current[:master_slave_clock] ||= []
217
+
218
+ # pick the right connection
219
+ if MasterSlaveAdapter.master_forced? || @master_connection.open_transactions > 0
220
+ connection_stack[0] = :master
221
+ end
222
+ connection_stack[0] ||= connection_for_clock(clock_stack[0])
223
+
224
+ # return the current connection
225
+ if connection_stack[0] == :slave
226
+ slave_connection
227
+ else
228
+ master_connection
229
+ end
230
+ end
231
+
232
+ def master_clock
233
+ if status = connect_to_master.select_one("SHOW MASTER STATUS")
234
+ Clock.new(status['File'], status['Position'])
235
+ end
236
+ end
237
+
238
+ def slave_clock
239
+ if status = connect_to_slave.select_one("SHOW SLAVE STATUS")
240
+ Clock.new(status['Master_Log_File'], status['Exec_Master_Log_Pos'])
241
+ end
242
+ end
243
+
244
+ def connect_to_master
245
+ @master_connection ||= ActiveRecord::Base.send( "#{self.master_config[:adapter]}_connection", self.master_config )
246
+ end
247
+
248
+ def connect_to_slave
249
+ @slave_connection ||= ActiveRecord::Base.send( "#{self.slave_config[:adapter]}_connection", self.slave_config)
250
+ end
251
+
252
+ end
253
+
254
+ end
255
+
256
+ end
@@ -0,0 +1,20 @@
1
+ ignored_methods = ActiveRecord::ConnectionAdapters::MasterSlaveAdapter::SELECT_METHODS + ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.instance_methods
2
+ ignored_methods.uniq!
3
+ ignored_methods.map! { |v| v.to_sym }
4
+
5
+ instance_methods = ActiveRecord::ConnectionAdapters::DatabaseStatements.instance_methods + ActiveRecord::ConnectionAdapters::SchemaStatements.instance_methods
6
+ instance_methods.uniq!
7
+ instance_methods.map! { |v| v.to_sym }
8
+ instance_methods.reject! { |v| ignored_methods.include?( v ) }
9
+
10
+ instance_methods.each do |method|
11
+
12
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.class_eval %Q!
13
+
14
+ def #{method}( *args, &block )
15
+ self.master_connection.#{method}( *args, &block )
16
+ end
17
+
18
+ !
19
+
20
+ end
@@ -0,0 +1,3 @@
1
+ module MasterSlaveAdapter
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "master_slave_adapter/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'master_slave_adapter_tcurdt'
7
+ s.version = MasterSlaveAdapter::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = [ 'Mauricio Linhares', 'Torsten Curdt' ]
10
+ s.email = 'tcurdt at vafer.org'
11
+ s.homepage = 'http://github.com/tcurdt/master_slave_adapter_mauricio'
12
+ s.summary = 'Master Slave Adapter'
13
+ s.description = 'Acts as a ActiveRecord adapter and allows you to setup a master-slave environment.'
14
+
15
+ s.rubyforge_project = "master_slave_adapter_tcurdt"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = [ "lib" ]
21
+
22
+ s.add_dependency('activerecord', [ "= 2.3.9" ])
23
+ end
data/specs/specs.rb ADDED
@@ -0,0 +1,407 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+ require 'spec'
4
+
5
+ $LOAD_PATH << File.expand_path(File.join( File.dirname( __FILE__ ), '..', 'lib' ))
6
+
7
+ require 'active_record/connection_adapters/master_slave_adapter'
8
+
9
+ ActiveRecord::Base.instance_eval do
10
+
11
+ def test_connection( config )
12
+ config[:database] == 'slave' ? _slave : _master
13
+ end
14
+
15
+ def _master=( new_master )
16
+ @_master = new_master
17
+ end
18
+
19
+ def _master
20
+ @_master
21
+ end
22
+
23
+ def _slave=( new_slave )
24
+ @_slave = new_slave
25
+ end
26
+
27
+ def _slave
28
+ @_slave
29
+ end
30
+
31
+ end
32
+
33
+ describe ActiveRecord::ConnectionAdapters::MasterSlaveAdapter do
34
+
35
+ before do
36
+
37
+ @mocked_methods = { :verify! => true, :reconnect! => true, :run_callbacks => true, :disconnect! => true }
38
+
39
+ ActiveRecord::Base._master = mock( 'master connection', @mocked_methods.merge( :open_transactions => 0 ) )
40
+ ActiveRecord::Base._slave = mock( 'slave connection', @mocked_methods )
41
+
42
+ @master_connection = ActiveRecord::Base._master
43
+ @slave_connection = ActiveRecord::Base._slave
44
+
45
+ end
46
+
47
+ after do
48
+ ActiveRecord::Base.connection_handler.clear_all_connections!
49
+ end
50
+
51
+ describe 'with common configuration' do
52
+
53
+
54
+ before do
55
+
56
+ @database_setup = {
57
+ :adapter => 'master_slave',
58
+ :username => 'root',
59
+ :database => 'slave',
60
+ :master_slave_adapter => 'test',
61
+ :master => { :username => 'root', :database => 'master' }
62
+ }
63
+
64
+ ActiveRecord::Base.establish_connection( @database_setup )
65
+
66
+ [ @master_connection, @slave_connection ].each do |c|
67
+ c.stub!( :select_value ).with( "SELECT 1", "test select" ).and_return( true )
68
+ end
69
+
70
+ end
71
+
72
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter::SELECT_METHODS.each do |method|
73
+
74
+ it "should send the method '#{method}' to the slave connection" do
75
+ @master_connection.stub!( :open_transactions ).and_return( 0 )
76
+ @slave_connection.should_receive( method ).with('testing').and_return( true )
77
+ ActiveRecord::Base.connection.send( method, 'testing' )
78
+ end
79
+
80
+ it "should send the method '#{method}' to the master connection if with_master was specified" do
81
+ @master_connection.should_receive( method ).with('testing').and_return( true )
82
+ ActiveRecord::Base.with_master do
83
+ ActiveRecord::Base.connection.send( method, 'testing' )
84
+ end
85
+ end
86
+
87
+ it "should send the method '#{method}' to the slave connection if with_slave was specified" do
88
+ @slave_connection.should_receive( method ).with('testing').and_return( true )
89
+ ActiveRecord::Base.with_slave do
90
+ ActiveRecord::Base.connection.send( method, 'testing' )
91
+ end
92
+ end
93
+
94
+ it "should send the method '#{method}' to the master connection if there are open transactions" do
95
+ @master_connection.stub!( :open_transactions ).and_return( 1 )
96
+ @master_connection.should_receive( method ).with('testing').and_return( true )
97
+ ActiveRecord::Base.with_master do
98
+ ActiveRecord::Base.connection.send( method, 'testing' )
99
+ end
100
+ end
101
+
102
+ it "should send the method '#{method}' to the master connection if there are open transactions, even in with_slave" do
103
+ @master_connection.stub!( :open_transactions ).and_return( 1 )
104
+ @master_connection.should_receive( method ).with('testing').and_return( true )
105
+ ActiveRecord::Base.with_slave do
106
+ ActiveRecord::Base.connection.send( method, 'testing' )
107
+ end
108
+ end
109
+
110
+ end
111
+
112
+ ActiveRecord::ConnectionAdapters::SchemaStatements.instance_methods.map(&:to_sym).each do |method|
113
+
114
+ it "should send the method '#{method}' from ActiveRecord::ConnectionAdapters::SchemaStatements to the master" do
115
+ @master_connection.should_receive( method ).and_return( true )
116
+ ActiveRecord::Base.connection.send( method )
117
+ end
118
+
119
+ end
120
+
121
+ (ActiveRecord::ConnectionAdapters::SchemaStatements.instance_methods.map(&:to_sym) - ActiveRecord::ConnectionAdapters::MasterSlaveAdapter::SELECT_METHODS).each do |method|
122
+
123
+ it "should send the method '#{method}' from ActiveRecord::ConnectionAdapters::DatabaseStatements to the master" do
124
+ @master_connection.should_receive( method ).and_return( true )
125
+ ActiveRecord::Base.connection.send( method )
126
+ end
127
+
128
+ end
129
+
130
+ it 'should be a master slave connection' do
131
+ ActiveRecord::Base.connection.class.should == ActiveRecord::ConnectionAdapters::MasterSlaveAdapter
132
+ end
133
+
134
+ it 'should have a master connection' do
135
+ ActiveRecord::Base.connection.master_connection.should == @master_connection
136
+ end
137
+
138
+ it 'should have a slave connection' do
139
+ @master_connection.stub!( :open_transactions ).and_return( 0 )
140
+ ActiveRecord::Base.connection.slave_connection.should == @slave_connection
141
+ end
142
+
143
+ end
144
+
145
+ describe 'with connection testing disabled' do
146
+
147
+ before do
148
+ @database_setup = {
149
+ :adapter => 'master_slave',
150
+ :username => 'root',
151
+ :database => 'slave',
152
+ :disable_connection_test => 'true',
153
+ :master_slave_adapter => 'test',
154
+ :master => { :username => 'root', :database => 'master' }
155
+ }
156
+
157
+ ActiveRecord::Base.establish_connection( @database_setup )
158
+
159
+ end
160
+
161
+ ActiveRecord::ConnectionAdapters::SchemaStatements.instance_methods.map(&:to_sym).each do |method|
162
+
163
+ it "should not perform the testing select on the master if #{method} is called" do
164
+ @master_connection.should_not_receive( :select_value ).with( "SELECT 1", "test select" )
165
+ @master_connection.should_receive( method ).with('testing').and_return(true)
166
+ ActiveRecord::Base.connection.send(method, 'testing')
167
+ end
168
+
169
+ end
170
+
171
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter::SELECT_METHODS.each do |method|
172
+
173
+ it "should not perform the testing select on the slave if #{method} is called" do
174
+ @slave_connection.should_not_receive( :select_value ).with( "SELECT 1", "test select" )
175
+ @slave_connection.should_receive( method ).with('testing').and_return(true)
176
+ ActiveRecord::Base.connection.send(method, 'testing')
177
+ end
178
+
179
+ end
180
+
181
+ end
182
+
183
+ describe 'with connection eager loading enabled' do
184
+
185
+ before do
186
+ @database_setup = {
187
+ :adapter => 'master_slave',
188
+ :username => 'root',
189
+ :database => 'slave',
190
+ :eager_load_connections => 'true',
191
+ :master_slave_adapter => 'test',
192
+ :master => { :username => 'root', :database => 'master' }
193
+ }
194
+
195
+ ActiveRecord::Base.establish_connection( @database_setup )
196
+
197
+ [ @master_connection, @slave_connection ].each do |c|
198
+ c.should_receive( :select_value ).with( "SELECT 1", "test select" ).and_return( true )
199
+ end
200
+
201
+ end
202
+
203
+ it 'should load the master connection before any method call' do
204
+ ActiveRecord::Base.connection.instance_variable_get(:@master_connection).should == @master_connection
205
+ end
206
+
207
+ it 'should load the slave connection before any method call' do
208
+ ActiveRecord::Base.connection.instance_variable_get(:@slave_connection).should == @slave_connection
209
+ end
210
+
211
+ end
212
+
213
+ describe 'with consistency' do
214
+ before do
215
+
216
+ @database_setup = {
217
+ :adapter => 'master_slave',
218
+ :username => 'root',
219
+ :database => 'slave',
220
+ :master_slave_adapter => 'test',
221
+ :master => { :username => 'root', :database => 'master' }
222
+ }
223
+
224
+ ActiveRecord::Base.establish_connection( @database_setup )
225
+
226
+ [ @master_connection, @slave_connection ].each do |c|
227
+ c.stub!( :select_value ).with( "SELECT 1", "test select" ).and_return( true )
228
+ end
229
+
230
+ end
231
+
232
+ def zero
233
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter::Clock.zero
234
+ end
235
+
236
+ def master_position(pos)
237
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter::Clock.new('', pos)
238
+ end
239
+
240
+ def slave_should_report_clock(pos)
241
+ if pos.instance_of? Fixnum
242
+ pos = [ pos ]
243
+ end
244
+ values = pos.map { |p| { 'Master_Log_File' => '', 'Exec_Master_Log_Pos' => p } }
245
+ @slave_connection.should_receive('select_one').exactly(pos.length).with('SHOW SLAVE STATUS').and_return(*values)
246
+ end
247
+
248
+ def master_should_report_clock(pos)
249
+ if pos.instance_of? Fixnum
250
+ pos = [ pos ]
251
+ end
252
+ values = pos.map { |p| { 'File' => '', 'Position' => p } }
253
+ @master_connection.should_receive('select_one').exactly(pos.length).with('SHOW MASTER STATUS').and_return(*values)
254
+ end
255
+
256
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter::SELECT_METHODS.each do |method|
257
+ it "should raise an exception if consistency is nil" do
258
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.reset!
259
+ lambda do
260
+ ActiveRecord::Base.with_consistency(nil) do
261
+ end
262
+ end.should raise_error(ArgumentError)
263
+ end
264
+
265
+ it "should send the method '#{method}' to the slave if clock.zero is given" do
266
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.reset!
267
+ slave_should_report_clock(0)
268
+ @slave_connection.should_receive(method).with('testing').and_return(true)
269
+ old_clock = zero
270
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
271
+ ActiveRecord::Base.connection.send(method, 'testing')
272
+ end
273
+ new_clock.should be_a(zero.class)
274
+ new_clock.should equal(zero)
275
+ end
276
+
277
+ it "should send the method '#{method}' to the master if slave hasn't cought up to required clock yet" do
278
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.reset!
279
+ slave_should_report_clock(0)
280
+ @master_connection.should_receive(method).with('testing').and_return(true)
281
+ old_clock = master_position(1)
282
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
283
+ ActiveRecord::Base.connection.send(method, 'testing' )
284
+ end
285
+ new_clock.should be_a(zero.class)
286
+ new_clock.should equal(old_clock)
287
+ end
288
+
289
+ it "should send the method '#{method}' to the master connection if there are open transactions" do
290
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.reset!
291
+ @master_connection.stub!(:open_transactions).and_return(1)
292
+ @master_connection.should_receive(method).with('testing').and_return(true)
293
+ old_clock = zero
294
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
295
+ ActiveRecord::Base.connection.send(method, 'testing')
296
+ end
297
+ new_clock.should be_a(zero.class)
298
+ new_clock.should equal(zero)
299
+ end
300
+
301
+ it "should send the method '#{method}' to the master after a write operation" do
302
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.reset!
303
+ slave_should_report_clock(0)
304
+ master_should_report_clock(2)
305
+ @slave_connection.should_receive(method).with('testing').and_return(true)
306
+ @master_connection.should_receive('update').with('testing').and_return(true)
307
+ @master_connection.should_receive(method).with('testing').and_return(true)
308
+ old_clock = zero
309
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
310
+ ActiveRecord::Base.connection.send(method, 'testing')
311
+ ActiveRecord::Base.connection.send('update', 'testing')
312
+ ActiveRecord::Base.connection.send(method, 'testing')
313
+ end
314
+ new_clock.should be_a(zero.class)
315
+ new_clock.should > old_clock
316
+ end
317
+
318
+ end
319
+
320
+ it "should do the right thing when nested inside with_consistency" do
321
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.reset!
322
+ slave_should_report_clock([ 0, 0 ])
323
+ @slave_connection.should_receive('select_one').exactly(3).times.with('testing').and_return(true)
324
+ old_clock = zero
325
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
326
+ ActiveRecord::Base.connection.send('select_one', 'testing')
327
+ ActiveRecord::Base.with_consistency(old_clock) do
328
+ ActiveRecord::Base.connection.send('select_one', 'testing')
329
+ end
330
+ ActiveRecord::Base.connection.send('select_one', 'testing')
331
+ end
332
+ new_clock.should equal(old_clock)
333
+
334
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.reset!
335
+ slave_should_report_clock([0,0])
336
+ @slave_connection.should_receive('select_all').exactly(2).times.with('testing').and_return(true)
337
+ @master_connection.should_receive('select_all').exactly(1).times.with('testing').and_return(true)
338
+ start_clock = zero
339
+ inner_clock = zero
340
+ outer_clock = ActiveRecord::Base.with_consistency(start_clock) do
341
+ ActiveRecord::Base.connection.send('select_all', 'testing') # slave
342
+ inner_clock = ActiveRecord::Base.with_consistency(master_position(1)) do
343
+ ActiveRecord::Base.connection.send('select_all', 'testing') # master
344
+ end
345
+ ActiveRecord::Base.connection.send('select_all', 'testing') # slave
346
+ end
347
+ start_clock.should equal(outer_clock)
348
+ inner_clock.should > start_clock
349
+ end
350
+
351
+ it "should do the right thing when nested inside with_master" do
352
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.reset!
353
+ slave_should_report_clock(0)
354
+ @slave_connection.should_receive('select_all').exactly(1).times.with('testing').and_return(true)
355
+ @master_connection.should_receive('select_all').exactly(2).times.with('testing').and_return(true)
356
+ ActiveRecord::Base.with_master do
357
+ ActiveRecord::Base.connection.send('select_all', 'testing') # master
358
+ ActiveRecord::Base.with_consistency(zero) do
359
+ ActiveRecord::Base.connection.send('select_all', 'testing') # slave
360
+ end
361
+ ActiveRecord::Base.connection.send('select_all', 'testing') # master
362
+ end
363
+ end
364
+
365
+ it "should do the right thing when nested inside with_slave" do
366
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.reset!
367
+ slave_should_report_clock(0)
368
+ @slave_connection.should_receive('select_all').exactly(3).times.with('testing').and_return(true)
369
+ ActiveRecord::Base.with_slave do
370
+ ActiveRecord::Base.connection.send('select_all', 'testing') # slave
371
+ ActiveRecord::Base.with_consistency(zero) do
372
+ ActiveRecord::Base.connection.send('select_all', 'testing') # slave
373
+ end
374
+ ActiveRecord::Base.connection.send('select_all', 'testing') # slave
375
+ end
376
+ end
377
+
378
+ it "should do the right thing when wrapping with_master" do
379
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.reset!
380
+ slave_should_report_clock(0)
381
+ @slave_connection.should_receive('select_all').exactly(2).times.with('testing').and_return(true)
382
+ @master_connection.should_receive('select_all').exactly(1).times.with('testing').and_return(true)
383
+ ActiveRecord::Base.with_consistency(zero) do
384
+ ActiveRecord::Base.connection.send('select_all', 'testing') # slave
385
+ ActiveRecord::Base.with_master do
386
+ ActiveRecord::Base.connection.send('select_all', 'testing') # master
387
+ end
388
+ ActiveRecord::Base.connection.send('select_all', 'testing') # slave
389
+ end
390
+ end
391
+
392
+ it "should do the right thing when wrapping with_slave" do
393
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.reset!
394
+ slave_should_report_clock(0)
395
+ @slave_connection.should_receive('select_all').exactly(1).times.with('testing').and_return(true)
396
+ @master_connection.should_receive('select_all').exactly(2).times.with('testing').and_return(true)
397
+ ActiveRecord::Base.with_consistency(master_position(1)) do
398
+ ActiveRecord::Base.connection.send('select_all', 'testing') # master
399
+ ActiveRecord::Base.with_slave do
400
+ ActiveRecord::Base.connection.send('select_all', 'testing') # slave
401
+ end
402
+ ActiveRecord::Base.connection.send('select_all', 'testing') # master
403
+ end
404
+ end
405
+
406
+ end
407
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: master_slave_adapter_tcurdt
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Mauricio Linhares
13
+ - Torsten Curdt
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-05-30 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activerecord
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - "="
28
+ - !ruby/object:Gem::Version
29
+ segments:
30
+ - 2
31
+ - 3
32
+ - 9
33
+ version: 2.3.9
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ description: Acts as a ActiveRecord adapter and allows you to setup a master-slave environment.
37
+ email: tcurdt at vafer.org
38
+ executables: []
39
+
40
+ extensions: []
41
+
42
+ extra_rdoc_files: []
43
+
44
+ files:
45
+ - .gitignore
46
+ - Gemfile
47
+ - LICENSE
48
+ - README
49
+ - Rakefile
50
+ - lib/active_record/connection_adapters/master_slave_adapter.rb
51
+ - lib/master_slave_adapter.rb
52
+ - lib/master_slave_adapter/active_record_extensions.rb
53
+ - lib/master_slave_adapter/adapter.rb
54
+ - lib/master_slave_adapter/instance_methods_generation.rb
55
+ - lib/master_slave_adapter/version.rb
56
+ - master_slave_adapter.gemspec
57
+ - specs/specs.rb
58
+ has_rdoc: true
59
+ homepage: http://github.com/tcurdt/master_slave_adapter_mauricio
60
+ licenses: []
61
+
62
+ post_install_message:
63
+ rdoc_options: []
64
+
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ segments:
81
+ - 0
82
+ version: "0"
83
+ requirements: []
84
+
85
+ rubyforge_project: master_slave_adapter_tcurdt
86
+ rubygems_version: 1.3.7
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: Master Slave Adapter
90
+ test_files: []
91
+