master_slave_adapter 1.0.0.beta1 → 1.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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