master_slave_adapter_tcurdt 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+