master_slave_adapter_soundcloud 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ tags
2
+ test/*
3
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2011 Maurício Linhares,
2
+ Torsten Curdt,
3
+ Kim Altintop,
4
+ Omid Aladini,
5
+ SoundCloud Ltd
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ 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
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1 @@
1
+ require 'master_slave_adapter'
@@ -0,0 +1,380 @@
1
+ require 'active_record'
2
+
3
+ module ActiveRecord
4
+ class Base
5
+ class << self
6
+ def with_consistency(clock, &blk)
7
+ if connection.respond_to? :with_consistency
8
+ connection.with_consistency(clock, &blk)
9
+ else
10
+ yield
11
+ nil
12
+ end
13
+ end
14
+
15
+ def with_master(&blk)
16
+ if connection.respond_to? :with_master
17
+ connection.with_master(&blk)
18
+ else
19
+ yield
20
+ end
21
+ end
22
+
23
+ def with_slave(&blk)
24
+ if connection.respond_to? :with_slave
25
+ connection.with_slave(&blk)
26
+ else
27
+ yield
28
+ end
29
+ end
30
+
31
+ def on_commit(&blk)
32
+ connection.on_commit(&blk) if connection.respond_to? :on_commit
33
+ end
34
+
35
+ def on_rollback(&blk)
36
+ connection.on_rollback(&blk) if connection.respond_to? :on_rollback
37
+ end
38
+
39
+ def master_slave_connection(config)
40
+ config = massage(config)
41
+ load_adapter(config.fetch(:connection_adapter))
42
+ ConnectionAdapters::MasterSlaveAdapter.new(config, logger)
43
+ end
44
+
45
+ private
46
+
47
+ def massage(config)
48
+ config = config.symbolize_keys
49
+ skip = [ :adapter, :connection_adapter, :master, :slaves ]
50
+ defaults = config.reject { |k,_| skip.include?(k) }
51
+ .merge(:adapter => config.fetch(:connection_adapter))
52
+ ([config.fetch(:master)] + config.fetch(:slaves, [])).map do |cfg|
53
+ cfg.symbolize_keys!.reverse_merge!(defaults)
54
+ end
55
+ config
56
+ end
57
+
58
+ def load_adapter(adapter_name)
59
+ unless self.respond_to?("#{adapter_name}_connection")
60
+ begin
61
+ require 'rubygems'
62
+ gem "activerecord-#{adapter_name}-adapter"
63
+ require "active_record/connection_adapters/#{adapter_name}_adapter"
64
+ rescue LoadError
65
+ begin
66
+ require "active_record/connection_adapters/#{adapter_name}_adapter"
67
+ rescue LoadError
68
+ raise %Q{Please install the #{adapter_name} adapter:
69
+ `gem install activerecord-#{adapter_name}-adapter` (#{$!})"}
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ module ConnectionAdapters
78
+
79
+ class AbstractAdapter
80
+ alias_method :orig_log_info, :log_info
81
+ def log_info(sql, name, ms)
82
+ connection_name =
83
+ [ @config[:name], @config[:host], @config[:port] ].compact.join(":")
84
+ orig_log_info sql, "[#{connection_name}] #{name || 'SQL'}", ms
85
+ end
86
+ end
87
+
88
+ class MasterSlaveAdapter < AbstractAdapter
89
+
90
+ class Clock
91
+ include Comparable
92
+ attr_reader :file, :position
93
+
94
+ def initialize(file, position)
95
+ raise ArgumentError, "file and postion may not be nil" if file.nil? || position.nil?
96
+ @file, @position = file, position.to_i
97
+ end
98
+
99
+ def <=>(other)
100
+ @file == other.file ? @position <=> other.position : @file <=> other.file
101
+ end
102
+
103
+ def to_s
104
+ [ @file, @position ].join('@')
105
+ end
106
+
107
+ def self.zero
108
+ @zero ||= Clock.new('', 0)
109
+ end
110
+
111
+ def self.infinity
112
+ @infinity ||= Clock.new('', Float::MAX.to_i)
113
+ end
114
+ end
115
+
116
+ checkout :active?
117
+
118
+ def initialize(config, logger)
119
+ super(nil, logger)
120
+
121
+ @connections = {}
122
+ @connections[:master] = connect(config.fetch(:master), :master)
123
+ @connections[:slaves] = config.fetch(:slaves).map { |cfg| connect(cfg, :slave) }
124
+
125
+ @disable_connection_test = config.delete(:disable_connection_test) == 'true'
126
+
127
+ self.current_connection = slave_connection!
128
+ end
129
+
130
+ # MASTER SLAVE ADAPTER INTERFACE ========================================
131
+
132
+ def with_master
133
+ with(self.master_connection, :master) { yield }
134
+ end
135
+
136
+ def with_slave
137
+ with(self.slave_connection!, :slave) { yield }
138
+ end
139
+
140
+ def with_consistency(clock)
141
+ raise ArgumentError, "consistency cannot be nil" if clock.nil?
142
+ # try random slave, else fall back to master
143
+ slave = slave_connection!
144
+ conn =
145
+ if !open_transaction? && slave_clock(slave) >= clock
146
+ [ slave, :slave ]
147
+ else
148
+ [ master_connection, :master ]
149
+ end
150
+
151
+ with(*conn) { yield }
152
+
153
+ self.current_clock || clock
154
+ end
155
+
156
+ def on_commit(&blk)
157
+ on_commit_callbacks.push blk
158
+ end
159
+
160
+ def on_rollback(&blk)
161
+ on_rollback_callbacks.push blk
162
+ end
163
+
164
+
165
+ # backwards compatibility
166
+ class << self
167
+ def with_master(&blk)
168
+ ActiveRecord::Base.with_master(&blk)
169
+ end
170
+ def with_slave(&blk)
171
+ ActiveRecord::Base.with_slave(&blk)
172
+ end
173
+ def with_consistency(clock, &blk)
174
+ ActiveRecord::Base.with_consistency(clock, &blk)
175
+ end
176
+ def reset!
177
+ Thread.current[:master_slave_clock] =
178
+ Thread.current[:master_slave_connection] =
179
+ Thread.current[:on_commit_callbacks] =
180
+ Thread.current[:on_rollback_callbacks] =
181
+ nil
182
+ end
183
+ end
184
+
185
+ # ADAPTER INTERFACE OVERRIDES ===========================================
186
+
187
+ def insert(*args)
188
+ on_write { |conn| conn.insert(*args) }
189
+ end
190
+
191
+ def update(*args)
192
+ on_write { |conn| conn.update(*args) }
193
+ end
194
+
195
+ def delete(*args)
196
+ on_write { |conn| conn.delete(*args) }
197
+ end
198
+
199
+ def commit_db_transaction
200
+ on_write { |conn| conn.commit_db_transaction }
201
+ on_commit_callbacks.shift.call(current_clock) until on_commit_callbacks.blank?
202
+ end
203
+
204
+ def rollback_db_transaction
205
+ on_commit_callbacks.clear
206
+ with(master_connection, :master) { |conn| conn.rollback_db_transaction }
207
+ on_rollback_callbacks.shift.call until on_rollback_callbacks.blank?
208
+ end
209
+
210
+ def active?
211
+ return true if @disable_connection_test
212
+ self.connections.map { |c| c.active? }.reduce(true) { |m,s| s ? m : s }
213
+ end
214
+
215
+ def reconnect!
216
+ self.connections.each { |c| c.reconnect! }
217
+ end
218
+
219
+ def disconnect!
220
+ self.connections.each { |c| c.disconnect! }
221
+ end
222
+
223
+ def reset!
224
+ self.connections.each { |c| c.reset! }
225
+ end
226
+
227
+ # Someone calling execute directly on the connection is likely to be a
228
+ # write, respectively some DDL statement. People really shouldn't do that,
229
+ # but let's delegate this to master, just to be sure.
230
+ def execute(*args)
231
+ on_write { |conn| conn.execute(*args) }
232
+ end
233
+
234
+ # ADAPTER INTERFACE DELEGATES ===========================================
235
+
236
+ # === must go to master
237
+ delegate :adapter_name,
238
+ :supports_migrations?,
239
+ :supports_primary_key?,
240
+ :supports_savepoints?,
241
+ :native_database_types,
242
+ :raw_connection,
243
+ :open_transactions,
244
+ :increment_open_transactions,
245
+ :decrement_open_transactions,
246
+ :transaction_joinable=,
247
+ :create_savepoint,
248
+ :rollback_to_savepoint,
249
+ :release_savepoint,
250
+ :current_savepoint_name,
251
+ :begin_db_transaction,
252
+ :to => :master_connection
253
+ delegate *ActiveRecord::ConnectionAdapters::SchemaStatements.instance_methods,
254
+ :to => :master_connection
255
+ # silly: :tables is commented in SchemaStatements.
256
+ delegate :tables, :to => :master_connection
257
+ # monkey patch from databasecleaner gem
258
+ delegate :truncate_table, :to => :master_connection
259
+
260
+ # === determine read connection
261
+ delegate :select_all,
262
+ :select_one,
263
+ :select_rows,
264
+ :select_value,
265
+ :select_values,
266
+ :to => :connection_for_read
267
+
268
+ def connection_for_read
269
+ open_transaction? ? master_connection : self.current_connection
270
+ end
271
+ private :connection_for_read
272
+
273
+ # === doesn't really matter, but must be handled by underlying adapter
274
+ delegate *ActiveRecord::ConnectionAdapters::Quoting.instance_methods,
275
+ :to => :current_connection
276
+
277
+ # UTIL ==================================================================
278
+
279
+ def master_connection
280
+ @connections[:master]
281
+ end
282
+
283
+ # Returns a random slave connection
284
+ # Note: the method is not referentially transparent, hence the bang
285
+ def slave_connection!
286
+ @connections[:slaves].sample
287
+ end
288
+
289
+ def connections
290
+ @connections.values.inject([]) { |m,c| m << c }.flatten.compact
291
+ end
292
+
293
+ def current_connection
294
+ connection_stack.first
295
+ end
296
+
297
+ def current_connection=(conn)
298
+ connection_stack.unshift conn
299
+ end
300
+
301
+ def current_clock
302
+ Thread.current[:master_slave_clock]
303
+ end
304
+
305
+ def current_clock=(clock)
306
+ Thread.current[:master_slave_clock] = clock
307
+ end
308
+
309
+ def master_clock
310
+ conn = master_connection
311
+ if status = conn.uncached { conn.select_one("SHOW MASTER STATUS") }
312
+ Clock.new(status['File'], status['Position'])
313
+ end
314
+ end
315
+
316
+ def slave_clock(connection = nil)
317
+ conn ||= slave_connection!
318
+ if status = conn.uncached { conn.select_one("SHOW SLAVE STATUS") }
319
+ Clock.new(status['Relay_Master_Log_File'], status['Exec_Master_Log_Pos'])
320
+ end
321
+ end
322
+
323
+ protected
324
+
325
+ def on_write
326
+ with(master_connection, :master) do |conn|
327
+ yield(conn).tap do
328
+ unless open_transaction?
329
+ if mc = master_clock
330
+ self.current_clock = mc unless current_clock.try(:>=, mc)
331
+ end
332
+ # keep using master after write
333
+ self.current_connection = conn
334
+ end
335
+ end
336
+ end
337
+ end
338
+
339
+ def with(conn, name)
340
+ self.current_connection = conn
341
+ yield(conn).tap { connection_stack.shift }
342
+ end
343
+
344
+ private
345
+
346
+ def logger
347
+ @logger # ||= Logger.new(STDOUT)
348
+ end
349
+
350
+ def info(msg)
351
+ logger.try(:info, msg)
352
+ end
353
+
354
+ def debug(msg)
355
+ logger.debug(msg) if logger && logger.debug?
356
+ end
357
+
358
+ def connect(cfg, name)
359
+ adapter_method = "#{cfg.fetch(:adapter)}_connection".to_sym
360
+ ActiveRecord::Base.send(adapter_method, { :name => name }.merge(cfg))
361
+ end
362
+
363
+ def open_transaction?
364
+ master_connection.open_transactions > 0
365
+ end
366
+
367
+ def connection_stack
368
+ Thread.current[:master_slave_connection] ||= []
369
+ end
370
+
371
+ def on_commit_callbacks
372
+ Thread.current[:on_commit_callbacks] ||= []
373
+ end
374
+
375
+ def on_rollback_callbacks
376
+ Thread.current[:on_rollback_callbacks] ||= []
377
+ end
378
+ end
379
+ end
380
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'master_slave_adapter_soundcloud'
6
+ s.version = File.read('VERSION').to_s
7
+ s.date = '2011-06-21'
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = [ 'Mauricio Linhares', 'Torsten Curdt', 'Kim Altintop', 'Omid Aladini', 'SoundCloud' ]
10
+ s.email = %q{kim@soundcloud.com tcurdt@soundcloud.com omid@soundcloud.com}
11
+ s.homepage = 'http://github.com/soundcloud/master_slave_adapter'
12
+ s.summary = %q{Replication Aware Master/Slave Database Adapter for Rails/ActiveRecord}
13
+ s.description = %q{(MySQL) Replication Aware Master/Slave Database Adapter for Rails/ActiveRecord}
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_path = 'lib'
19
+
20
+ s.required_ruby_version = '>= 1.9.2'
21
+ s.required_rubygems_version = '1.3.7'
22
+ s.add_development_dependency 'rspec'
23
+
24
+ s.add_dependency 'activerecord', '= 2.3.9'
25
+ end
data/specs/specs.rb ADDED
@@ -0,0 +1,516 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+ require 'rspec'
4
+
5
+ ActiveRecord::Base.logger =
6
+ Logger.new(STDOUT).tap { |l| l.level = Logger::DEBUG }
7
+
8
+ $LOAD_PATH << File.expand_path(File.join( File.dirname( __FILE__ ), '..', 'lib' ))
9
+
10
+ require 'active_record/connection_adapters/master_slave_adapter'
11
+
12
+ class ActiveRecord::Base
13
+ cattr_accessor :master_mock, :slave_mock
14
+ def self.test_connection(config)
15
+ config[:database] == 'slave' ? slave_mock : master_mock
16
+ end
17
+ end
18
+
19
+ describe ActiveRecord::ConnectionAdapters::MasterSlaveAdapter do
20
+ let(:default_database_setup) do
21
+ {
22
+ :adapter => 'master_slave',
23
+ :username => 'root',
24
+ :database => 'slave',
25
+ :connection_adapter => 'test',
26
+ :master => { :username => 'root', :database => 'master' },
27
+ :slaves => [{ :database => 'slave' }],
28
+ }
29
+ end
30
+
31
+ let(:database_setup) { default_database_setup }
32
+
33
+ let(:mocked_methods) do
34
+ {
35
+ #:verify! => true,
36
+ :reconnect! => true,
37
+ #:run_callbacks => true,
38
+ :disconnect! => true,
39
+ }
40
+ end
41
+
42
+ let!(:master_connection) do
43
+ mock(
44
+ 'master connection',
45
+ mocked_methods.merge(:open_transactions => 0)
46
+ ).tap do |conn|
47
+ conn.stub!(:uncached) { |blk| blk.call }
48
+ ActiveRecord::Base.master_mock = conn
49
+ end
50
+ end
51
+
52
+ let!(:slave_connection) do
53
+ mock('slave connection', mocked_methods).tap do |conn|
54
+ conn.stub!(:uncached) { |blk| blk.call }
55
+ ActiveRecord::Base.slave_mock = conn
56
+ end
57
+ end
58
+
59
+ def adapter_connection
60
+ ActiveRecord::Base.connection
61
+ end
62
+
63
+ SchemaStatements = ActiveRecord::ConnectionAdapters::SchemaStatements.instance_methods.map(&:to_sym)
64
+ SelectMethods = [ :select_all, :select_one, :select_rows, :select_value, :select_values ]
65
+ Clock = ActiveRecord::ConnectionAdapters::MasterSlaveAdapter::Clock
66
+
67
+ before do
68
+ unless database_setup[:disable_connection_test] == 'true'
69
+ [ master_connection, slave_connection ].each do |c|
70
+ c.should_receive(:active?).exactly(2).times.and_return(true)
71
+ end
72
+ end
73
+ ActiveRecord::Base.establish_connection(database_setup)
74
+ end
75
+
76
+ after do
77
+ ActiveRecord::Base.connection_handler.clear_all_connections!
78
+ end
79
+
80
+ describe 'with common configuration' do
81
+ before do
82
+ [ master_connection, slave_connection ].each do |c|
83
+ c.stub!( :select_value ).with( "SELECT 1", "test select" ).and_return( true )
84
+ end
85
+ end
86
+
87
+ xit "should call 'columns' on master" do
88
+ end
89
+
90
+ SelectMethods.each do |method|
91
+ it "should send the method '#{method}' to the slave connection" do
92
+ master_connection.stub!( :open_transactions ).and_return( 0 )
93
+ slave_connection.should_receive( method ).with('testing').and_return( true )
94
+ adapter_connection.send( method, 'testing' )
95
+ end
96
+
97
+ it "should send the method '#{method}' to the master connection if with_master was specified" do
98
+ master_connection.should_receive( method ).with('testing').and_return( true )
99
+ ActiveRecord::Base.with_master do
100
+ adapter_connection.send( method, 'testing' )
101
+ end
102
+ end
103
+
104
+ it "should send the method '#{method}' to the slave connection if with_slave was specified" do
105
+ slave_connection.should_receive( method ).with('testing').and_return( true )
106
+ ActiveRecord::Base.with_slave do
107
+ adapter_connection.send( method, 'testing' )
108
+ end
109
+ end
110
+
111
+ it "should send the method '#{method}' to the master connection if there are open transactions" do
112
+ master_connection.stub!( :open_transactions ).and_return( 1 )
113
+ master_connection.should_receive( method ).with('testing').and_return( true )
114
+ ActiveRecord::Base.with_master do
115
+ adapter_connection.send( method, 'testing' )
116
+ end
117
+ end
118
+
119
+ it "should send the method '#{method}' to the master connection if there are open transactions, even in with_slave" do
120
+ master_connection.stub!( :open_transactions ).and_return( 1 )
121
+ master_connection.should_receive( method ).with('testing').and_return( true )
122
+ ActiveRecord::Base.with_slave do
123
+ adapter_connection.send( method, 'testing' )
124
+ end
125
+ end
126
+ end # /SelectMethods.each
127
+
128
+ SchemaStatements.each do |method|
129
+ it "should send the method '#{method}' from ActiveRecord::ConnectionAdapters::SchemaStatements to the master" do
130
+ master_connection.should_receive( method ).and_return( true )
131
+ adapter_connection.send( method )
132
+ end
133
+ end
134
+
135
+ (SchemaStatements - SelectMethods).each do |method|
136
+ it "should send the method '#{method}' from ActiveRecord::ConnectionAdapters::DatabaseStatements to the master" do
137
+ master_connection.should_receive( method ).and_return( true )
138
+ adapter_connection.send( method )
139
+ end
140
+ end
141
+
142
+ it 'should be a master slave connection' do
143
+ adapter_connection.class.should == ActiveRecord::ConnectionAdapters::MasterSlaveAdapter
144
+ end
145
+
146
+ it 'should have a master connection' do
147
+ adapter_connection.master_connection.should == master_connection
148
+ end
149
+
150
+ it 'should have a slave connection' do
151
+ master_connection.stub!( :open_transactions ).and_return( 0 )
152
+ adapter_connection.slave_connection!.should == slave_connection
153
+ end
154
+ end
155
+
156
+ context "connection testing" do
157
+ context "disabled" do
158
+ let(:database_setup) do
159
+ default_database_setup.merge(:disable_connection_test => 'true')
160
+ end
161
+
162
+ context "on master" do
163
+ SchemaStatements.each do |method|
164
+ it "should not perform the testing when #{method} is called" do
165
+ master_connection.tap do |c|
166
+ c.should_not_receive(:active?)
167
+ c.should_receive(method).with('testing').and_return(true)
168
+ end
169
+ adapter_connection.send(method, 'testing')
170
+ end
171
+ end
172
+ end
173
+
174
+ context "on slave" do
175
+ SelectMethods.each do |method|
176
+ it "should not perform the testing when #{method} is called" do
177
+ slave_connection.tap do |c|
178
+ c.should_not_receive(:active?)
179
+ c.should_receive(method).with('testing').and_return(true)
180
+ end
181
+ adapter_connection.send(method, 'testing')
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ describe 'with connection eager loading enabled' do
189
+ it 'should eager load the connections' do
190
+ adapter_connection.connections.should include(master_connection, slave_connection)
191
+ end
192
+ end
193
+
194
+ describe 'with consistency' do
195
+ before do
196
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.reset!
197
+
198
+ [ master_connection, slave_connection ].each do |c|
199
+ c.stub!(:select_value).with("SELECT 1", "test select").and_return(true)
200
+ end
201
+ end
202
+
203
+ def zero
204
+ Clock.zero
205
+ end
206
+
207
+ def master_position(pos)
208
+ Clock.new('', pos)
209
+ end
210
+
211
+ def slave_should_report_clock(pos)
212
+ pos = Array(pos)
213
+ values = pos.map { |p| { 'Relay_Master_Log_File' => '', 'Exec_Master_Log_Pos' => p } }
214
+ slave_connection
215
+ .should_receive('select_one').exactly(pos.length).with('SHOW SLAVE STATUS')
216
+ .and_return(*values)
217
+ end
218
+
219
+ def master_should_report_clock(pos)
220
+ pos = Array(pos)
221
+ values = pos.map { |p| { 'File' => '', 'Position' => p } }
222
+ master_connection
223
+ .should_receive('select_one').exactly(pos.length).with('SHOW MASTER STATUS')
224
+ .and_return(*values)
225
+ end
226
+
227
+ SelectMethods.each do |method|
228
+ it "should raise an exception if consistency is nil" do
229
+ lambda do
230
+ ActiveRecord::Base.with_consistency(nil) do
231
+ end
232
+ end.should raise_error(ArgumentError)
233
+ end
234
+
235
+ it "should send the method '#{method}' to the slave if clock.zero is given" do
236
+ slave_should_report_clock(0)
237
+ slave_connection.should_receive(method).with('testing').and_return(true)
238
+ old_clock = zero
239
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
240
+ adapter_connection.send(method, 'testing')
241
+ end
242
+ new_clock.should be_a(zero.class)
243
+ new_clock.should equal(zero)
244
+ end
245
+
246
+ it "should send the method '#{method}' to the master if slave hasn't cought up to required clock yet" do
247
+ slave_should_report_clock(0)
248
+ master_connection.should_receive(method).with('testing').and_return(true)
249
+ old_clock = master_position(1)
250
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
251
+ adapter_connection.send(method, 'testing' )
252
+ end
253
+ new_clock.should be_a(zero.class)
254
+ new_clock.should equal(old_clock)
255
+ end
256
+
257
+ it "should send the method '#{method}' to the master connection if there are open transactions" do
258
+ master_connection.stub!(:open_transactions).and_return(1)
259
+ master_connection.should_receive(method).with('testing').and_return(true)
260
+ old_clock = zero
261
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
262
+ adapter_connection.send(method, 'testing')
263
+ end
264
+ new_clock.should be_a(zero.class)
265
+ new_clock.should equal(zero)
266
+ end
267
+
268
+ it "should send the method '#{method}' to the master after a write operation" do
269
+ slave_should_report_clock(0)
270
+ master_should_report_clock(2)
271
+ slave_connection.should_receive(method).with('testing').and_return(true)
272
+ master_connection.should_receive('update').with('testing').and_return(true)
273
+ master_connection.should_receive(method).with('testing').and_return(true)
274
+ old_clock = zero
275
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
276
+ adapter_connection.send(method, 'testing') # slave
277
+ adapter_connection.send('update', 'testing') # master
278
+ adapter_connection.send(method, 'testing') # master
279
+ end
280
+ new_clock.should be_a(zero.class)
281
+ new_clock.should > old_clock
282
+ end
283
+
284
+ end
285
+
286
+ it "should update the clock after a transaction" do
287
+ slave_should_report_clock(0)
288
+ master_should_report_clock([0, 1, 1])
289
+
290
+ slave_connection
291
+ .should_receive('select_all').exactly(1).times.with('testing')
292
+ .and_return(true)
293
+
294
+ master_connection
295
+ .should_receive('update').exactly(3).times.with('testing')
296
+ .and_return(true)
297
+ master_connection
298
+ .should_receive('select_all').exactly(5).times.with('testing')
299
+ .and_return(true)
300
+ %w(begin_db_transaction
301
+ commit_db_transaction
302
+ increment_open_transactions
303
+ decrement_open_transactions).each do |txstmt|
304
+ master_connection.should_receive(txstmt).exactly(1).times
305
+ end
306
+
307
+ master_connection
308
+ .should_receive('open_transactions').exactly(13).times
309
+ .and_return(
310
+ # adapter: with_consistency, select_all, update, select_all
311
+ 0, 0, 0, 0,
312
+ # connection: transaction
313
+ 0,
314
+ # adapter: select_all, update, select_all, commit_db_transaction
315
+ 1, 1, 1, 0,
316
+ # connection: transaction (ensure)
317
+ 0,
318
+ # adapter: select_all, update, select_all
319
+ 0, 0, 0
320
+ )
321
+
322
+ old_clock = zero
323
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
324
+ adapter_connection.send('select_all', 'testing') # slave s=0 m=0
325
+ adapter_connection.send('update', 'testing') # master s=0 m=1
326
+ adapter_connection.send('select_all', 'testing') # master s=0 m=1
327
+
328
+ ActiveRecord::Base.transaction do
329
+ adapter_connection.send('select_all', 'testing') # master s=0 m=1
330
+ adapter_connection.send('update', 'testing') # master s=0 m=1
331
+ adapter_connection.send('select_all', 'testing') # master s=0 m=1
332
+ end
333
+
334
+ adapter_connection.send('select_all', 'testing') # master s=0 m=2
335
+ adapter_connection.send('update', 'testing') # master s=0 m=3
336
+ adapter_connection.send('select_all', 'testing') # master s=0 m=3
337
+ end
338
+
339
+ new_clock.should > old_clock
340
+ end
341
+
342
+ context "with nested with_consistency" do
343
+ it "should return the same clock if not writing and no lag" do
344
+ slave_should_report_clock([ 0, 0 ])
345
+ slave_connection
346
+ .should_receive('select_one').exactly(3).times.with('testing')
347
+ .and_return(true)
348
+
349
+ old_clock = zero
350
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
351
+ adapter_connection.send('select_one', 'testing')
352
+ ActiveRecord::Base.with_consistency(old_clock) do
353
+ adapter_connection.send('select_one', 'testing')
354
+ end
355
+ adapter_connection.send('select_one', 'testing')
356
+ end
357
+ new_clock.should equal(old_clock)
358
+ end
359
+
360
+ it "requesting a newer clock should return a new clock" do
361
+ slave_should_report_clock([0,0])
362
+ slave_connection
363
+ .should_receive('select_all').exactly(2).times.with('testing')
364
+ .and_return(true)
365
+ master_connection
366
+ .should_receive('select_all').exactly(1).times.with('testing')
367
+ .and_return(true)
368
+
369
+ start_clock = zero
370
+ inner_clock = zero
371
+ outer_clock = ActiveRecord::Base.with_consistency(start_clock) do
372
+ adapter_connection.send('select_all', 'testing') # slave
373
+ inner_clock = ActiveRecord::Base.with_consistency(master_position(1)) do
374
+ adapter_connection.send('select_all', 'testing') # master
375
+ end
376
+ adapter_connection.send('select_all', 'testing') # slave
377
+ end
378
+
379
+ start_clock.should equal(outer_clock)
380
+ inner_clock.should > start_clock
381
+ end
382
+ end
383
+
384
+ it "should do the right thing when nested inside with_master" do
385
+ slave_should_report_clock(0)
386
+ slave_connection.should_receive('select_all').exactly(1).times.with('testing').and_return(true)
387
+ master_connection.should_receive('select_all').exactly(2).times.with('testing').and_return(true)
388
+ ActiveRecord::Base.with_master do
389
+ adapter_connection.send('select_all', 'testing') # master
390
+ ActiveRecord::Base.with_consistency(zero) do
391
+ adapter_connection.send('select_all', 'testing') # slave
392
+ end
393
+ adapter_connection.send('select_all', 'testing') # master
394
+ end
395
+ end
396
+
397
+ it "should do the right thing when nested inside with_slave" do
398
+ slave_should_report_clock(0)
399
+ slave_connection.should_receive('select_all').exactly(3).times.with('testing').and_return(true)
400
+ ActiveRecord::Base.with_slave do
401
+ adapter_connection.send('select_all', 'testing') # slave
402
+ ActiveRecord::Base.with_consistency(zero) do
403
+ adapter_connection.send('select_all', 'testing') # slave
404
+ end
405
+ adapter_connection.send('select_all', 'testing') # slave
406
+ end
407
+ end
408
+
409
+ it "should do the right thing when wrapping with_master" do
410
+ slave_should_report_clock(0)
411
+ slave_connection.should_receive('select_all').exactly(2).times.with('testing').and_return(true)
412
+ master_connection.should_receive('select_all').exactly(1).times.with('testing').and_return(true)
413
+ ActiveRecord::Base.with_consistency(zero) do
414
+ adapter_connection.send('select_all', 'testing') # slave
415
+ ActiveRecord::Base.with_master do
416
+ adapter_connection.send('select_all', 'testing') # master
417
+ end
418
+ adapter_connection.send('select_all', 'testing') # slave
419
+ end
420
+ end
421
+
422
+ it "should do the right thing when wrapping with_slave" do
423
+ slave_should_report_clock(0)
424
+ slave_connection.should_receive('select_all').exactly(1).times.with('testing').and_return(true)
425
+ master_connection.should_receive('select_all').exactly(2).times.with('testing').and_return(true)
426
+ ActiveRecord::Base.with_consistency(master_position(1)) do
427
+ adapter_connection.send('select_all', 'testing') # master
428
+ ActiveRecord::Base.with_slave do
429
+ adapter_connection.send('select_all', 'testing') # slave
430
+ end
431
+ adapter_connection.send('select_all', 'testing') # master
432
+ end
433
+ end
434
+ end # /with_consistency
435
+
436
+ describe "transaction callbacks" do
437
+ before do
438
+ ActiveRecord::ConnectionAdapters::MasterSlaveAdapter.reset!
439
+ end
440
+
441
+ def run_tx
442
+ adapter_connection
443
+ .should_receive('master_clock')
444
+ .and_return(Clock.new('', 1))
445
+ %w(begin_db_transaction
446
+ commit_db_transaction
447
+ increment_open_transactions
448
+ decrement_open_transactions).each do |txstmt|
449
+ master_connection
450
+ .should_receive(txstmt).exactly(1).times
451
+ end
452
+ master_connection
453
+ .should_receive('open_transactions').exactly(4).times
454
+ .and_return(0, 1, 0, 0)
455
+
456
+ master_connection
457
+ .should_receive('update').with('testing')
458
+ .and_return(true)
459
+
460
+ ActiveRecord::Base.transaction do
461
+ adapter_connection.send('update', 'testing')
462
+ end
463
+ end
464
+
465
+ def fail_tx
466
+ %w(begin_db_transaction
467
+ rollback_db_transaction
468
+ increment_open_transactions
469
+ decrement_open_transactions).each do |txstmt|
470
+ master_connection
471
+ .should_receive(txstmt).exactly(1).times
472
+ end
473
+ master_connection
474
+ .should_receive('open_transactions').exactly(3).times
475
+ .and_return(0, 1, 0)
476
+ master_connection
477
+ .should_receive('update').with('testing')
478
+ .and_return(true)
479
+
480
+ ActiveRecord::Base.transaction do
481
+ adapter_connection.send('update', 'testing')
482
+ raise "rollback"
483
+ end
484
+ rescue
485
+ nil
486
+ end
487
+
488
+ context "on commit" do
489
+ it "on_commit callback should be called" do
490
+ x = false
491
+ adapter_connection.on_commit { x = true }
492
+ lambda { run_tx }.should change { x }.to(true)
493
+ end
494
+
495
+ it "on_rollback callback should not be called" do
496
+ x = false
497
+ adapter_connection.on_rollback { x = true }
498
+ lambda { run_tx }.should_not change { x }
499
+ end
500
+ end
501
+
502
+ context "rollback" do
503
+ it "on_commit callback should not be called" do
504
+ x = false
505
+ adapter_connection.on_commit { x = true }
506
+ lambda { fail_tx }.should_not change { x }
507
+ end
508
+
509
+ it "on_rollback callback should be called" do
510
+ x = false
511
+ adapter_connection.on_rollback { x = true }
512
+ lambda { fail_tx }.should change { x }.to(true)
513
+ end
514
+ end
515
+ end
516
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: master_slave_adapter_soundcloud
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Mauricio Linhares
13
+ - Torsten Curdt
14
+ - Kim Altintop
15
+ - Omid Aladini
16
+ - SoundCloud
17
+ autorequire:
18
+ bindir: bin
19
+ cert_chain: []
20
+
21
+ date: 2011-06-21 00:00:00 +02:00
22
+ default_executable:
23
+ dependencies:
24
+ - !ruby/object:Gem::Dependency
25
+ name: rspec
26
+ prerelease: false
27
+ requirement: &id001 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ segments:
33
+ - 0
34
+ version: "0"
35
+ type: :development
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: activerecord
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - "="
44
+ - !ruby/object:Gem::Version
45
+ segments:
46
+ - 2
47
+ - 3
48
+ - 9
49
+ version: 2.3.9
50
+ type: :runtime
51
+ version_requirements: *id002
52
+ description: (MySQL) Replication Aware Master/Slave Database Adapter for Rails/ActiveRecord
53
+ email: kim@soundcloud.com tcurdt@soundcloud.com omid@soundcloud.com
54
+ executables: []
55
+
56
+ extensions: []
57
+
58
+ extra_rdoc_files: []
59
+
60
+ files:
61
+ - .gitignore
62
+ - Gemfile
63
+ - LICENSE
64
+ - README
65
+ - Rakefile
66
+ - VERSION
67
+ - lib/active_record/connection_adapters/master_slave_adapter.rb
68
+ - lib/master_slave_adapter.rb
69
+ - master_slave_adapter.gemspec
70
+ - specs/specs.rb
71
+ has_rdoc: true
72
+ homepage: http://github.com/soundcloud/master_slave_adapter
73
+ licenses: []
74
+
75
+ post_install_message:
76
+ rdoc_options: []
77
+
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ segments:
86
+ - 1
87
+ - 9
88
+ - 2
89
+ version: 1.9.2
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - "="
94
+ - !ruby/object:Gem::Version
95
+ segments:
96
+ - 1
97
+ - 3
98
+ - 7
99
+ version: 1.3.7
100
+ requirements: []
101
+
102
+ rubyforge_project:
103
+ rubygems_version: 1.3.7
104
+ signing_key:
105
+ specification_version: 3
106
+ summary: Replication Aware Master/Slave Database Adapter for Rails/ActiveRecord
107
+ test_files: []
108
+