master_slave_adapter 1.0.0.beta1 → 1.0.0.beta2

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.
@@ -1,11 +1,9 @@
1
1
  $: << File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
 
3
3
  require 'rspec'
4
+ require 'logger'
4
5
  require 'active_record/connection_adapters/master_slave_adapter'
5
6
 
6
- ActiveRecord::Base.logger =
7
- Logger.new($stdout).tap { |l| l.level = Logger::DEBUG }
8
-
9
7
  module ActiveRecord
10
8
  class Base
11
9
  cattr_accessor :master_mock, :slave_mock
@@ -14,12 +12,14 @@ module ActiveRecord
14
12
  end
15
13
 
16
14
  def self.test_master_slave_connection(config)
17
- TestMasterSlaveAdapter.new(config, logger)
15
+ ConnectionAdapters::TestMasterSlaveAdapter.new(config, logger)
18
16
  end
19
17
  end
20
18
 
21
19
  module ConnectionAdapters
22
- class TestMasterSlaveAdapter < MasterSlaveAdapter::Base
20
+ class TestMasterSlaveAdapter < AbstractAdapter
21
+ include MasterSlaveAdapter
22
+
23
23
  def master_clock
24
24
  end
25
25
 
@@ -77,7 +77,7 @@ describe ActiveRecord::ConnectionAdapters::MasterSlaveAdapter do
77
77
  end
78
78
 
79
79
  SchemaStatements = ActiveRecord::ConnectionAdapters::SchemaStatements.public_instance_methods.map(&:to_sym)
80
- SelectMethods = [ :select_all, :select_one, :select_rows, :select_value, :select_values ]
80
+ SelectMethods = [ :select_all, :select_one, :select_rows, :select_value, :select_values ] unless defined?(SelectMethods)
81
81
 
82
82
  before do
83
83
  ActiveRecord::Base.establish_connection(database_setup)
@@ -114,19 +114,31 @@ describe ActiveRecord::ConnectionAdapters::MasterSlaveAdapter do
114
114
  end
115
115
  end
116
116
 
117
- it "should send the method '#{method}' to the master connection if there are open transactions" do
118
- master_connection.stub!( :open_transactions ).and_return( 1 )
119
- master_connection.should_receive( method ).with('testing').and_return( true )
120
- ActiveRecord::Base.with_master do
117
+ context "given there are open transactions" do
118
+ it "should send the method '#{method}' to the master connection" do
119
+ master_connection.stub!( :open_transactions ).and_return( 1 )
120
+ master_connection.should_receive( method ).with('testing').and_return( true )
121
+
121
122
  adapter_connection.send( method, 'testing' )
122
123
  end
123
- end
124
124
 
125
- it "should send the method '#{method}' to the master connection if there are open transactions, even in with_slave" do
126
- master_connection.stub!( :open_transactions ).and_return( 1 )
127
- master_connection.should_receive( method ).with('testing').and_return( true )
128
- ActiveRecord::Base.with_slave do
129
- adapter_connection.send( method, 'testing' )
125
+ it "should send the method '#{method}' to the master connection, even in with_slave" do
126
+ master_connection.stub!( :open_transactions ).and_return( 1 )
127
+ master_connection.should_receive( method ).with('testing').and_return( true )
128
+
129
+ ActiveRecord::Base.with_slave do
130
+ adapter_connection.send( method, 'testing' )
131
+ end
132
+ end
133
+
134
+ it "raises MasterUnavailable if master is not available" do
135
+ master_connection.stub(:open_transactions).and_return(1)
136
+ master_connection.stub(:connection_error?).and_return(true)
137
+ master_connection.should_receive(method).with('testing').and_raise(ActiveRecord::StatementInvalid)
138
+
139
+ expect do
140
+ adapter_connection.send(method, 'testing')
141
+ end.to raise_error(ActiveRecord::MasterUnavailable)
130
142
  end
131
143
  end
132
144
  end # /SelectMethods.each
@@ -136,12 +148,14 @@ describe ActiveRecord::ConnectionAdapters::MasterSlaveAdapter do
136
148
  master_connection.should_receive( method ).and_return( true )
137
149
  adapter_connection.send( method )
138
150
  end
139
- end
140
151
 
141
- (SchemaStatements - SelectMethods).each do |method|
142
- it "should send the method '#{method}' from ActiveRecord::ConnectionAdapters::DatabaseStatements to the master" do
143
- master_connection.should_receive( method ).and_return( true )
144
- adapter_connection.send( method )
152
+ it "should raise MasterSlaveAdapter if master is not available" do
153
+ master_connection.stub(:connection_error?).and_return(true)
154
+ master_connection.should_receive(method).and_raise(ActiveRecord::StatementInvalid)
155
+
156
+ expect do
157
+ adapter_connection.send(method)
158
+ end.to raise_error(ActiveRecord::MasterUnavailable)
145
159
  end
146
160
  end
147
161
 
@@ -315,4 +329,44 @@ describe ActiveRecord::ConnectionAdapters::MasterSlaveAdapter do
315
329
  end
316
330
  end
317
331
  end
332
+
333
+ describe "connection stack" do
334
+ it "should start with the slave connection on top" do
335
+ adapter_connection.current_connection.should == slave_connection
336
+ end
337
+
338
+ it "should keep the current connection on top" do
339
+ ActiveRecord::Base.with_master do
340
+ adapter_connection.current_connection.should == master_connection
341
+ ActiveRecord::Base.with_slave do
342
+ adapter_connection.current_connection.should == slave_connection
343
+ ActiveRecord::Base.with_master do
344
+ adapter_connection.current_connection.should == master_connection
345
+ end
346
+ adapter_connection.current_connection.should == slave_connection
347
+ end
348
+ adapter_connection.current_connection.should == master_connection
349
+ end
350
+ adapter_connection.current_connection.should == slave_connection
351
+ end
352
+
353
+ it "should continue to use master connection after a write" do
354
+ master_connection.should_receive(:execute).with("INSERT 42")
355
+
356
+ ActiveRecord::Base.with_slave do
357
+ adapter_connection.current_connection.should == slave_connection
358
+ ActiveRecord::Base.with_master do
359
+ adapter_connection.current_connection.should == master_connection
360
+ ActiveRecord::Base.with_slave do
361
+ adapter_connection.current_connection.should == slave_connection
362
+ adapter_connection.execute("INSERT 42")
363
+ adapter_connection.current_connection.should == master_connection
364
+ end
365
+ adapter_connection.current_connection.should == master_connection
366
+ end
367
+ adapter_connection.current_connection.should == master_connection
368
+ end
369
+ adapter_connection.current_connection.should == master_connection
370
+ end
371
+ end
318
372
  end
@@ -0,0 +1,372 @@
1
+ $: << File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+
3
+ require 'rspec'
4
+ require 'logger'
5
+ require 'active_record/connection_adapters/mysql2_master_slave_adapter'
6
+
7
+ module ActiveRecord
8
+ class Base
9
+ cattr_accessor :master_mock, :slave_mock
10
+ def self.mysql2_connection(config)
11
+ config[:database] == 'slave' ? slave_mock : master_mock
12
+ end
13
+ end
14
+ end
15
+
16
+ describe ActiveRecord::ConnectionAdapters::Mysql2MasterSlaveAdapter do
17
+ let(:database_setup) do
18
+ {
19
+ :adapter => 'master_slave',
20
+ :username => 'root',
21
+ :database => 'slave',
22
+ :connection_adapter => 'mysql2',
23
+ :master => { :username => 'root', :database => 'master' },
24
+ :slaves => [{ :database => 'slave' }],
25
+ }
26
+ end
27
+
28
+ let(:mocked_methods) do
29
+ {
30
+ :reconnect! => true,
31
+ :disconnect! => true,
32
+ :active? => true,
33
+ }
34
+ end
35
+
36
+ let!(:master_connection) do
37
+ mock(
38
+ 'master connection',
39
+ mocked_methods.merge(:open_transactions => 0)
40
+ ).tap do |conn|
41
+ conn.stub!(:uncached).and_yield
42
+ ActiveRecord::Base.master_mock = conn
43
+ end
44
+ end
45
+
46
+ let!(:slave_connection) do
47
+ mock('slave connection', mocked_methods).tap do |conn|
48
+ conn.stub!(:uncached).and_yield
49
+ ActiveRecord::Base.slave_mock = conn
50
+ end
51
+ end
52
+
53
+ def adapter_connection
54
+ ActiveRecord::Base.connection
55
+ end
56
+
57
+ SelectMethods = [ :select_all, :select_one, :select_rows, :select_value, :select_values ] unless defined?(SelectMethods)
58
+ Clock = ActiveRecord::ConnectionAdapters::MasterSlaveAdapter::Clock unless defined?(Clock)
59
+
60
+ before do
61
+ ActiveRecord::Base.establish_connection(database_setup)
62
+ end
63
+
64
+ after do
65
+ ActiveRecord::Base.connection_handler.clear_all_connections!
66
+ end
67
+
68
+ describe 'consistency' do
69
+ def zero
70
+ Clock.zero
71
+ end
72
+
73
+ def master_position(pos)
74
+ Clock.new('', pos)
75
+ end
76
+
77
+ def select_method
78
+ :select_one
79
+ end
80
+
81
+ def should_report_clock(pos, connection, log_file, log_pos, sql)
82
+ pos = Array(pos)
83
+ values = pos.map { |p| { log_file => '', log_pos => p } }
84
+
85
+ connection.
86
+ should_receive(select_method).exactly(pos.length).times.
87
+ with(sql).
88
+ and_return(*values)
89
+ end
90
+
91
+ def slave_should_report_clock(pos)
92
+ should_report_clock(pos, slave_connection, 'Relay_Master_Log_File', 'Exec_Master_Log_Pos', 'SHOW SLAVE STATUS')
93
+ end
94
+
95
+ def master_should_report_clock(pos)
96
+ should_report_clock(pos, master_connection, 'File', 'Position', 'SHOW MASTER STATUS')
97
+ end
98
+
99
+ SelectMethods.each do |method|
100
+ it "should send the method '#{method}' to the slave if nil is given" do
101
+ slave_should_report_clock(0)
102
+ slave_connection.should_receive(method).with('testing').and_return(true)
103
+ new_clock = ActiveRecord::Base.with_consistency(nil) do
104
+ adapter_connection.send(method, 'testing')
105
+ end
106
+ new_clock.should be_a(Clock)
107
+ new_clock.should equal(zero)
108
+ end
109
+
110
+ it "should send the method '#{method}' to the slave if clock.zero is given" do
111
+ slave_should_report_clock(0)
112
+ slave_connection.should_receive(method).with('testing').and_return(true)
113
+ old_clock = zero
114
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
115
+ adapter_connection.send(method, 'testing')
116
+ end
117
+ new_clock.should be_a(Clock)
118
+ new_clock.should equal(old_clock)
119
+ end
120
+
121
+ it "should send the method '#{method}' to the master if slave hasn't cought up to required clock yet" do
122
+ slave_should_report_clock(0)
123
+ master_connection.should_receive(method).with('testing').and_return(true)
124
+ old_clock = master_position(1)
125
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
126
+ adapter_connection.send(method, 'testing' )
127
+ end
128
+ new_clock.should be_a(Clock)
129
+ new_clock.should equal(old_clock)
130
+ end
131
+
132
+ it "should send the method '#{method}' to the master connection if there are open transactions" do
133
+ master_connection.stub!(:open_transactions).and_return(1)
134
+ master_connection.should_receive(method).with('testing').and_return(true)
135
+ old_clock = zero
136
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
137
+ adapter_connection.send(method, 'testing')
138
+ end
139
+ new_clock.should be_a(Clock)
140
+ new_clock.should equal(zero)
141
+ end
142
+
143
+ it "should send the method '#{method}' to the master after a write operation" do
144
+ slave_should_report_clock(0)
145
+ master_should_report_clock(2)
146
+ slave_connection.should_receive(method).with('testing').and_return(true)
147
+ master_connection.should_receive(:update).with('testing').and_return(true)
148
+ master_connection.should_receive(method).with('testing').and_return(true)
149
+ old_clock = zero
150
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
151
+ adapter_connection.send(method, 'testing') # slave
152
+ adapter_connection.send(:update, 'testing') # master
153
+ adapter_connection.send(method, 'testing') # master
154
+ end
155
+ new_clock.should be_a(Clock)
156
+ new_clock.should > old_clock
157
+ end
158
+ end
159
+
160
+ it "should update the clock after a transaction" do
161
+ slave_should_report_clock(0)
162
+ master_should_report_clock([0, 1, 1])
163
+
164
+ slave_connection.
165
+ should_receive(:select_all).exactly(1).times.with('testing').
166
+ and_return(true)
167
+
168
+ master_connection.
169
+ should_receive(:update).exactly(3).times.with('testing').
170
+ and_return(true)
171
+ master_connection.
172
+ should_receive(:select_all).exactly(5).times.with('testing').
173
+ and_return(true)
174
+ %w(begin_db_transaction
175
+ commit_db_transaction
176
+ increment_open_transactions
177
+ decrement_open_transactions
178
+ outside_transaction?).each do |txstmt|
179
+ master_connection.should_receive(txstmt).exactly(1).times
180
+ end
181
+
182
+ master_connection.
183
+ should_receive('open_transactions').exactly(13).times.
184
+ and_return(
185
+ # adapter: with_consistency, select_all, update, select_all
186
+ 0, 0, 0, 0,
187
+ # connection: transaction
188
+ 0,
189
+ # adapter: select_all, update, select_all, commit_db_transaction
190
+ 1, 1, 1, 0,
191
+ # connection: transaction (ensure)
192
+ 0,
193
+ # adapter: select_all, update, select_all
194
+ 0, 0, 0
195
+ )
196
+
197
+ old_clock = zero
198
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
199
+ adapter_connection.send(:select_all, 'testing') # slave s=0 m=0
200
+ adapter_connection.send(:update, 'testing') # master s=0 m=1
201
+ adapter_connection.send(:select_all, 'testing') # master s=0 m=1
202
+
203
+ ActiveRecord::Base.transaction do
204
+ adapter_connection.send(:select_all, 'testing') # master s=0 m=1
205
+ adapter_connection.send(:update, 'testing') # master s=0 m=1
206
+ adapter_connection.send(:select_all, 'testing') # master s=0 m=1
207
+ end
208
+
209
+ adapter_connection.send(:select_all, 'testing') # master s=0 m=2
210
+ adapter_connection.send(:update, 'testing') # master s=0 m=3
211
+ adapter_connection.send(:select_all, 'testing') # master s=0 m=3
212
+ end
213
+
214
+ new_clock.should > old_clock
215
+ end
216
+
217
+ context "with nested with_consistency" do
218
+ it "should return the same clock if not writing and no lag" do
219
+ slave_should_report_clock(0)
220
+ slave_connection.
221
+ should_receive(:select_one).exactly(3).times.with('testing').
222
+ and_return(true)
223
+
224
+ old_clock = zero
225
+ new_clock = ActiveRecord::Base.with_consistency(old_clock) do
226
+ adapter_connection.send(:select_one, 'testing')
227
+ ActiveRecord::Base.with_consistency(old_clock) do
228
+ adapter_connection.send(:select_one, 'testing')
229
+ end
230
+ adapter_connection.send(:select_one, 'testing')
231
+ end
232
+ new_clock.should equal(old_clock)
233
+ end
234
+
235
+ it "requesting a newer clock should return a new clock" do
236
+ adapter_connection.
237
+ should_receive('slave_consistent?').exactly(2).times.
238
+ and_return(true, false)
239
+ slave_connection.
240
+ should_receive(:select_all).exactly(2).times.with('testing').
241
+ and_return(true)
242
+ master_connection.
243
+ should_receive(:select_all).exactly(1).times.with('testing').
244
+ and_return(true)
245
+
246
+ start_clock = zero
247
+ inner_clock = zero
248
+ outer_clock = ActiveRecord::Base.with_consistency(start_clock) do
249
+ adapter_connection.send(:select_all, 'testing') # slave
250
+ inner_clock = ActiveRecord::Base.with_consistency(master_position(1)) do
251
+ adapter_connection.send(:select_all, 'testing') # master
252
+ end
253
+ adapter_connection.send(:select_all, 'testing') # slave
254
+ end
255
+
256
+ start_clock.should equal(outer_clock)
257
+ inner_clock.should > start_clock
258
+ end
259
+ end
260
+
261
+ it "should do the right thing when nested inside with_master" do
262
+ slave_should_report_clock(0)
263
+ slave_connection.should_receive(:select_all).exactly(1).times.with('testing').and_return(true)
264
+ master_connection.should_receive(:select_all).exactly(2).times.with('testing').and_return(true)
265
+ ActiveRecord::Base.with_master do
266
+ adapter_connection.send(:select_all, 'testing') # master
267
+ ActiveRecord::Base.with_consistency(zero) do
268
+ adapter_connection.send(:select_all, 'testing') # slave
269
+ end
270
+ adapter_connection.send(:select_all, 'testing') # master
271
+ end
272
+ end
273
+
274
+ it "should do the right thing when nested inside with_slave" do
275
+ slave_should_report_clock(0)
276
+ slave_connection.should_receive(:select_all).exactly(3).times.with('testing').and_return(true)
277
+ ActiveRecord::Base.with_slave do
278
+ adapter_connection.send(:select_all, 'testing') # slave
279
+ ActiveRecord::Base.with_consistency(zero) do
280
+ adapter_connection.send(:select_all, 'testing') # slave
281
+ end
282
+ adapter_connection.send(:select_all, 'testing') # slave
283
+ end
284
+ end
285
+
286
+ it "should do the right thing when wrapping with_master" do
287
+ slave_should_report_clock(0)
288
+ slave_connection.should_receive(:select_all).exactly(2).times.with('testing').and_return(true)
289
+ master_connection.should_receive(:select_all).exactly(1).times.with('testing').and_return(true)
290
+ ActiveRecord::Base.with_consistency(zero) do
291
+ adapter_connection.send(:select_all, 'testing') # slave
292
+ ActiveRecord::Base.with_master do
293
+ adapter_connection.send(:select_all, 'testing') # master
294
+ end
295
+ adapter_connection.send(:select_all, 'testing') # slave
296
+ end
297
+ end
298
+
299
+ it "should do the right thing when wrapping with_slave" do
300
+ slave_should_report_clock(0)
301
+ slave_connection.should_receive(:select_all).exactly(1).times.with('testing').and_return(true)
302
+ master_connection.should_receive(:select_all).exactly(2).times.with('testing').and_return(true)
303
+ ActiveRecord::Base.with_consistency(master_position(1)) do
304
+ adapter_connection.send(:select_all, 'testing') # master
305
+ ActiveRecord::Base.with_slave do
306
+ adapter_connection.send(:select_all, 'testing') # slave
307
+ end
308
+ adapter_connection.send(:select_all, 'testing') # master
309
+ end
310
+ end
311
+
312
+ it "should accept clock as string" do
313
+ slave_should_report_clock(0)
314
+ slave_connection.should_receive(:select_all).with('testing')
315
+
316
+ ActiveRecord::Base.with_consistency("@0") do
317
+ adapter_connection.send(:select_all, 'testing')
318
+ end
319
+ end
320
+ end # /with_consistency
321
+
322
+ describe "connection error detection" do
323
+ {
324
+ 2002 => "query: not connected",
325
+ 2003 => "Can't connect to MySQL server on 'localhost' (3306)",
326
+ 2006 => "MySQL server has gone away",
327
+ 2013 => "Lost connection to MySQL server during query",
328
+ }.each do |errno, description|
329
+ it "raises MasterUnavailable for '#{description}' during query execution" do
330
+ master_connection.stub_chain(:raw_connection, :errno).and_return(errno)
331
+ master_connection.should_receive(:insert).and_raise(ActiveRecord::StatementInvalid.new("Mysql2::Error: #{description}: INSERT 42"))
332
+
333
+ expect do
334
+ adapter_connection.insert("INSERT 42")
335
+ end.to raise_error(ActiveRecord::MasterUnavailable)
336
+ end
337
+
338
+ it "doesn't raise anything for '#{description}' during connection" do
339
+ error = Mysql2::Error.new(description)
340
+ error.stub(:errno).and_return(errno)
341
+ ActiveRecord::Base.should_receive(:mysql2_connection).twice do |config|
342
+ if config[:name] == :master
343
+ raise error
344
+ else
345
+ slave_connection
346
+ end
347
+ end
348
+
349
+ expect do
350
+ ActiveRecord::Base.connection_handler.clear_all_connections!
351
+ ActiveRecord::Base.connection
352
+ end.to_not raise_error
353
+ end
354
+ end
355
+
356
+ it "raises MasterUnavailable for 'closed MySQL connection' during query execution" do
357
+ master_connection.should_receive(:insert).and_raise(ActiveRecord::StatementInvalid.new("Mysql2::Error: closed MySQL connection: INSERT 42"))
358
+
359
+ expect do
360
+ adapter_connection.insert("INSERT 42")
361
+ end.to raise_error(ActiveRecord::MasterUnavailable)
362
+ end
363
+
364
+ it "raises StatementInvalid for other errors" do
365
+ master_connection.should_receive(:insert).and_raise(ActiveRecord::StatementInvalid.new("Mysql2::Error: Query execution was interrupted: INSERT 42"))
366
+
367
+ expect do
368
+ adapter_connection.insert("INSERT 42")
369
+ end.to raise_error(ActiveRecord::StatementInvalid)
370
+ end
371
+ end
372
+ end