master_slave_adapter_soundcloud 0.1.0

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,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
+