activerecord-mysql-reconnect-new 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.idea/.gitignore +8 -0
  4. data/.idea/activerecord-mysql-reconnect.iml +22 -0
  5. data/.idea/misc.xml +4 -0
  6. data/.idea/modules.xml +8 -0
  7. data/.idea/vcs.xml +6 -0
  8. data/.rspec +3 -0
  9. data/.travis.yml +45 -0
  10. data/Appraisals +28 -0
  11. data/ChangeLog +35 -0
  12. data/Gemfile +4 -0
  13. data/LICENSE.txt +22 -0
  14. data/README.md +124 -0
  15. data/Rakefile +5 -0
  16. data/activerecord-mysql-reconnect-new.gemspec +28 -0
  17. data/docker-compose.yml +6 -0
  18. data/gemfiles/activerecord_4.2.gemfile +8 -0
  19. data/gemfiles/activerecord_5.0.gemfile +7 -0
  20. data/gemfiles/activerecord_5.1.gemfile +7 -0
  21. data/gemfiles/activerecord_5.2.gemfile +7 -0
  22. data/gemfiles/activerecord_6.0.gemfile +7 -0
  23. data/gemfiles/activerecord_6.1.gemfile +7 -0
  24. data/gemfiles/activerecord_master.gemfile +7 -0
  25. data/lib/activerecord/mysql/reconnect/abstract_mysql_adapter_ext.rb +46 -0
  26. data/lib/activerecord/mysql/reconnect/base_ext.rb +32 -0
  27. data/lib/activerecord/mysql/reconnect/connection_pool_ext.rb +18 -0
  28. data/lib/activerecord/mysql/reconnect/mysql2_adapter_ext.rb +13 -0
  29. data/lib/activerecord/mysql/reconnect/null_transaction_ext.rb +9 -0
  30. data/lib/activerecord/mysql/reconnect/version.rb +7 -0
  31. data/lib/activerecord/mysql/reconnect.rb +268 -0
  32. data/lib/activerecord-mysql-reconnect.rb +5 -0
  33. data/spec/activerecord-mysql-reconnect_spec.rb +545 -0
  34. data/spec/data.sql +1001 -0
  35. data/spec/employee_model.rb +1 -0
  36. data/spec/mysql2_ext.rb +5 -0
  37. data/spec/mysql_helper.rb +84 -0
  38. data/spec/spec_helper.rb +54 -0
  39. metadata +174 -0
@@ -0,0 +1,545 @@
1
+ describe 'activerecord-mysql-reconnect' do
2
+ before(:each) do
3
+ ActiveRecord::Base.establish_connection(
4
+ :adapter => 'mysql2',
5
+ :host => '127.0.0.1',
6
+ :username => 'root',
7
+ :database => 'employees',
8
+ :port => 14407,
9
+ )
10
+
11
+ ActiveRecord::Base.logger = Logger.new($stdout)
12
+ ActiveRecord::Base.logger.formatter = proc {|_, _, _, message| "#{message}\n" }
13
+
14
+ if ENV['DEBUG'] == '1'
15
+ ActiveRecord::Base.logger.level = Logger::DEBUG
16
+ else
17
+ ActiveRecord::Base.logger.level = Logger::ERROR
18
+ end
19
+
20
+ ActiveRecord::Base.enable_retry = true
21
+ ActiveRecord::Base.execution_tries = 10
22
+ ActiveRecord::Base.retry_mode = :rw
23
+ ActiveRecord::Base.retry_databases = []
24
+ end
25
+
26
+ let(:insert_with_sleep) do
27
+ <<-SQL
28
+ INSERT INTO `employees` (
29
+ `birth_date`,
30
+ `emp_no`,
31
+ `first_name`,
32
+ `hire_date`,
33
+ `last_name`
34
+ ) VALUES (
35
+ '2014-01-09 03:22:25',
36
+ SLEEP(10),
37
+ 'Scott',
38
+ '2014-01-09 03:22:25',
39
+ 'Tiger'
40
+ )
41
+ SQL
42
+ end
43
+
44
+ context 'when select all on same thread' do
45
+ specify do
46
+ expect(Employee.all.length).to eq 1000
47
+ MysqlServer.restart
48
+ expect(Employee.all.length).to eq 1000
49
+ end
50
+ end
51
+
52
+ context 'when count on same thead' do
53
+ specify do
54
+ expect(Employee.count).to eq 1000
55
+ MysqlServer.restart
56
+ expect(Employee.count).to eq 1000
57
+ end
58
+ end
59
+
60
+ context 'wehn select on other thread' do
61
+ specify do
62
+ th = thread_start {
63
+ expect(Employee.where(:id => 1).pluck('sleep(10) * 0 + 3')).to eq [3]
64
+ }
65
+
66
+ MysqlServer.restart
67
+ expect(Employee.count).to eq 1000
68
+ th.join
69
+ end
70
+ end
71
+
72
+ context 'when insert on other thread' do
73
+ before do
74
+ allow_any_instance_of(Mysql2::Error).to receive(:message).and_return('MySQL server has gone away')
75
+ end
76
+
77
+ specify do
78
+ th = thread_start {
79
+ emp = Employee.create(
80
+ :emp_no => 9999,
81
+ :birth_date => Time.now,
82
+ # wait 10 sec
83
+ :first_name => "' + sleep(10) + '",
84
+ :last_name => 'Tiger',
85
+ :hire_date => Time.now
86
+ )
87
+
88
+ expect(emp.id).to eq 1001
89
+ expect(emp.emp_no).to eq 9999
90
+ }
91
+
92
+ MysqlServer.restart
93
+ th.join
94
+ end
95
+ end
96
+
97
+ [
98
+ 'MySQL server has gone away',
99
+ 'Server shutdown in progress',
100
+ 'closed MySQL connection',
101
+ "Can't connect to MySQL server",
102
+ 'Query execution was interrupted',
103
+ 'Access denied for user',
104
+ 'The MySQL server is running with the --read-only option',
105
+ "Can't connect to local MySQL server", # When running in local sandbox, or using a socket file
106
+ 'Unknown MySQL server host', # For DNS blips
107
+ "Lost connection to MySQL server at 'reading initial communication packet'",
108
+ ].each do |errmsg|
109
+ context "when `#{errmsg}` is happened" do
110
+ before do
111
+ allow_any_instance_of(Mysql2::Error).to receive(:message).and_return(errmsg)
112
+ end
113
+
114
+ specify do
115
+ th = thread_start {
116
+ emp = Employee.create(
117
+ :emp_no => 9999,
118
+ :birth_date => Time.now,
119
+ # wait 10 sec
120
+ :first_name => "' + sleep(10) + '",
121
+ :last_name => 'Tiger',
122
+ :hire_date => Time.now
123
+ )
124
+
125
+ expect(emp.id).to eq 1001
126
+ expect(emp.emp_no).to eq 9999
127
+ }
128
+
129
+ MysqlServer.restart
130
+ th.join
131
+ end
132
+ end
133
+ end
134
+
135
+ context 'when unexpected error is happened' do
136
+ before do
137
+ allow_any_instance_of(Mysql2::Error).to receive(:message).and_return("unexpected error")
138
+ end
139
+
140
+ specify do
141
+ th = thread_start {
142
+ expect {
143
+ emp = Employee.create(
144
+ :emp_no => 9999,
145
+ :birth_date => Time.now,
146
+ # wait 10 sec
147
+ :first_name => "' + sleep(10) + '",
148
+ :last_name => 'Tiger',
149
+ :hire_date => Time.now
150
+ )
151
+ }.to raise_error(/unexpected error/)
152
+ }
153
+
154
+ MysqlServer.restart
155
+ th.join
156
+ end
157
+ end
158
+
159
+ context 'when update on other thread' do
160
+ before do
161
+ allow_any_instance_of(Mysql2::Error).to receive(:message).and_return('MySQL server has gone away')
162
+ end
163
+
164
+ specify do
165
+ th = thread_start {
166
+ emp = Employee.where(:id => 1).first
167
+ # wait 10 sec
168
+ emp.first_name = "' + sleep(10) + '"
169
+ emp.last_name = 'ZapZapZap'
170
+ emp.save!
171
+
172
+ emp = Employee.where(:id => 1).first
173
+ expect(emp.last_name).to eq 'ZapZapZap'
174
+ }
175
+
176
+ MysqlServer.restart
177
+ th.join
178
+ end
179
+ end
180
+
181
+ context 'when use #without_retry' do
182
+ specify do
183
+ expect {
184
+ ActiveRecord::Base.without_retry do
185
+ Employee.count
186
+ MysqlServer.restart
187
+ Employee.count
188
+ end
189
+ }.to raise_error(ActiveRecord::StatementInvalid)
190
+ end
191
+ end
192
+
193
+ context 'with transaction' do
194
+ before do
195
+ allow_any_instance_of(Mysql2::Error).to receive(:message).and_return('MySQL server has gone away')
196
+ end
197
+
198
+ specify do
199
+ skip if myisam?
200
+
201
+ expect(Employee.count).to eq 1000
202
+
203
+ ActiveRecord::Base.transaction do
204
+ emp = Employee.create(
205
+ :emp_no => 9999,
206
+ :birth_date => Time.now,
207
+ :first_name => 'Scott',
208
+ :last_name => 'Tiger',
209
+ :hire_date => Time.now
210
+ )
211
+
212
+ expect(emp.id).to eq 1001
213
+ expect(emp.emp_no).to eq 9999
214
+
215
+ MysqlServer.restart
216
+
217
+ emp = Employee.create(
218
+ :emp_no => 9998,
219
+ :birth_date => Time.now,
220
+ :first_name => 'Scott',
221
+ :last_name => 'Tiger',
222
+ :hire_date => Time.now
223
+ )
224
+
225
+ # NOTE: Ignore the transaction on :rw mode
226
+ expect(emp.id).to eq 1001
227
+ expect(emp.emp_no).to eq 9998
228
+ end
229
+
230
+ expect(Employee.count).to eq 1001
231
+ end
232
+ end
233
+
234
+ context 'when new connection' do
235
+ specify do
236
+ ActiveRecord::Base.clear_all_connections!
237
+ MysqlServer.restart
238
+ expect(Employee.count).to eq 1000
239
+ end
240
+ end
241
+
242
+ context 'when connection verify' do
243
+ specify do
244
+ th = thread_start {
245
+ MysqlServer.stop
246
+ sleep 10
247
+ MysqlServer.start
248
+ }
249
+
250
+ sleep 5
251
+ ActiveRecord::Base.connection.verify!
252
+ th.join
253
+ end
254
+ end
255
+
256
+ context 'when connection reconnect' do
257
+ specify do
258
+ th = thread_start {
259
+ MysqlServer.stop
260
+ sleep 10
261
+ MysqlServer.start
262
+ }
263
+
264
+ sleep 5
265
+ ActiveRecord::Base.connection.reconnect!
266
+ th.join
267
+ end
268
+ end
269
+
270
+ context 'when disable reconnect' do
271
+ specify do
272
+ ActiveRecord::Base.enable_retry = false
273
+
274
+ expect {
275
+ expect(Employee.all.length).to eq 1000
276
+ MysqlServer.restart
277
+ expect(Employee.all.length).to eq 1000
278
+ }.to raise_error(ActiveRecord::StatementInvalid)
279
+
280
+ ActiveRecord::Base.enable_retry = true
281
+
282
+ expect(Employee.all.length).to eq 1000
283
+ MysqlServer.restart
284
+ expect(Employee.all.length).to eq 1000
285
+ end
286
+ end
287
+
288
+ context 'when select on :r mode' do
289
+ before do
290
+ ActiveRecord::Base.retry_mode = :r
291
+ end
292
+
293
+ specify do
294
+ expect(Employee.all.length).to eq 1000
295
+ MysqlServer.restart
296
+ expect(Employee.all.length).to eq 1000
297
+ end
298
+ end
299
+
300
+ context 'when insert on :r mode' do
301
+ before do
302
+ ActiveRecord::Base.retry_mode = :r
303
+ allow_any_instance_of(Mysql2::Error).to receive(:message).and_return('MySQL server has gone away')
304
+ end
305
+
306
+ specify do
307
+ expect(Employee.all.length).to eq 1000
308
+
309
+ MysqlServer.restart
310
+
311
+ expect {
312
+ Employee.create(
313
+ :emp_no => 9999,
314
+ :birth_date => Time.now,
315
+ # wait 10 sec
316
+ :first_name => "' + sleep(10) + '",
317
+ :last_name => 'Tiger',
318
+ :hire_date => Time.now
319
+ )
320
+ }.to raise_error(ActiveRecord::StatementInvalid)
321
+ end
322
+ end
323
+
324
+ context 'when `lost connection` is happened' do
325
+ before do
326
+ allow_any_instance_of(Mysql2::Error).to receive(:message).and_return('Lost connection to MySQL server during query')
327
+ end
328
+
329
+ specify do
330
+ expect(Employee.all.length).to eq 1000
331
+
332
+ MysqlServer.restart
333
+
334
+ expect {
335
+ Employee.create(
336
+ :emp_no => 9999,
337
+ :birth_date => Time.now,
338
+ # wait 10 sec
339
+ :first_name => "' + sleep(10) + '",
340
+ :last_name => 'Tiger',
341
+ :hire_date => Time.now
342
+ )
343
+ }.to raise_error(ActiveRecord::StatementInvalid)
344
+ end
345
+ end
346
+
347
+ context 'when `lost connection` is happened on :force mode' do
348
+ before do
349
+ ActiveRecord::Base.retry_mode = :force
350
+ allow_any_instance_of(Mysql2::Error).to receive(:message).and_return('Lost connection to MySQL server during query')
351
+ end
352
+
353
+ specify do
354
+ expect(Employee.all.length).to eq 1000
355
+
356
+ MysqlServer.restart
357
+
358
+ emp = Employee.create(
359
+ :emp_no => 9999,
360
+ :birth_date => Time.now,
361
+ # wait 10 sec
362
+ :first_name => "' + sleep(10) + '",
363
+ :last_name => 'Tiger',
364
+ :hire_date => Time.now
365
+ )
366
+
367
+ expect(emp.id).to eq 1001
368
+ expect(emp.emp_no).to eq 9999
369
+ end
370
+ end
371
+
372
+ context 'when `lost connection` is happened on :force mode (2)' do
373
+ before do
374
+ ActiveRecord::Base.retry_mode = :force
375
+ allow_any_instance_of(Mysql2::Error).to receive(:message).and_return('Lost connection to MySQL server during query')
376
+
377
+ Thread.start do
378
+ MysqlServer.lock_tables
379
+ end
380
+ end
381
+
382
+ specify do
383
+ th = thread_start {
384
+ ActiveRecord::Base.connection.execute(insert_with_sleep)
385
+ }
386
+
387
+ sleep 3
388
+ MysqlServer.restart
389
+ th.join
390
+ end
391
+ end
392
+
393
+ context 'when read-only=1' do
394
+ before do
395
+ allow_any_instance_of(Mysql2::Error).to receive(:message).and_return('The MySQL server is running with the --read-only option so it cannot execute this statement:')
396
+ end
397
+
398
+ specify do
399
+ expect(Employee.all.length).to eq 1000
400
+ MysqlServer.restart
401
+ expect(Employee.all.length).to eq 1000
402
+ end
403
+ end
404
+
405
+ [
406
+ :employees2,
407
+ '127.0.0.2:employees',
408
+ '127.0.0.\_:employees',
409
+ ].each do |db|
410
+ context "when retry specific database: #{db}" do
411
+ before do
412
+ ActiveRecord::Base.retry_databases = db
413
+ end
414
+
415
+ specify do
416
+ expect {
417
+ expect(Employee.all.length).to eq 1000
418
+ MysqlServer.restart
419
+ expect(Employee.all.length).to eq 1000
420
+ }.to raise_error(ActiveRecord::StatementInvalid)
421
+
422
+ ActiveRecord::Base.retry_databases = []
423
+
424
+ expect(Employee.all.length).to eq 1000
425
+ MysqlServer.restart
426
+ expect(Employee.all.length).to eq 1000
427
+ end
428
+ end
429
+ end
430
+
431
+ [
432
+ :employees,
433
+ '127.0.0.1:employees',
434
+ '127.0.0._:e%',
435
+ ].each do |db|
436
+ context "when retry specific database: #{db}" do
437
+ before do
438
+ ActiveRecord::Base.retry_databases = db
439
+ end
440
+
441
+ specify do
442
+ expect(Employee.all.length).to eq 1000
443
+ MysqlServer.restart
444
+ expect(Employee.all.length).to eq 1000
445
+ end
446
+ end
447
+ end
448
+
449
+ context "when retry with warning" do
450
+ let(:warning_template) do
451
+ "%s (cause: %s, sql: SELECT `employees`.* FROM `employees`, connection: host=127.0.0.1;database=employees;username=root)"
452
+ end
453
+
454
+ let(:mysql_error) do
455
+ Mysql2::Error.const_defined?(:ConnectionError) ? Mysql2::Error::ConnectionError : Mysql2::Error
456
+ end
457
+
458
+ before do
459
+ allow_any_instance_of(mysql_error).to receive(:message).and_return('Lost connection to MySQL server during query')
460
+ end
461
+
462
+ context "when retry failed " do
463
+ specify do
464
+ if ActiveRecord::VERSION::MAJOR < 6
465
+ expect(ActiveRecord::Base.logger).to receive(:warn).with(warning_template % [
466
+ "MySQL server has gone away. Trying to reconnect in 0.5 seconds.",
467
+ "#{mysql_error}: Lost connection to MySQL server during query: SELECT `employees`.* FROM `employees` [ActiveRecord::StatementInvalid]",
468
+ ])
469
+ else
470
+ expect(ActiveRecord::Base.logger).to receive(:warn).with(warning_template % [
471
+ "MySQL server has gone away. Trying to reconnect in 0.5 seconds.",
472
+ "#{mysql_error}: Lost connection to MySQL server during query [ActiveRecord::StatementInvalid]",
473
+ ])
474
+ end
475
+
476
+
477
+ (1.0..4.5).step(0.5).each do |sec|
478
+ expect(ActiveRecord::Base.logger).to receive(:warn).with(warning_template % [
479
+ "MySQL server has gone away. Trying to reconnect in #{sec} seconds.",
480
+ "Lost connection to MySQL server during query [#{mysql_error}]",
481
+ ])
482
+ end
483
+
484
+ expect(ActiveRecord::Base.logger).to receive(:warn).with(warning_template % [
485
+ "Query retry failed.",
486
+ "Lost connection to MySQL server during query [#{mysql_error}]",
487
+ ])
488
+
489
+ expect(Employee.all.length).to eq 1000
490
+ MysqlServer.stop
491
+
492
+ expect {
493
+ Employee.all.length
494
+ }.to raise_error(mysql_error)
495
+ end
496
+ end
497
+
498
+ context "when retry succeeded" do
499
+ specify do
500
+ if ActiveRecord::VERSION::MAJOR < 6
501
+ expect(ActiveRecord::Base.logger).to receive(:warn).with(warning_template % [
502
+ "MySQL server has gone away. Trying to reconnect in 0.5 seconds.",
503
+ "#{mysql_error}: Lost connection to MySQL server during query: SELECT `employees`.* FROM `employees` [ActiveRecord::StatementInvalid]",
504
+ ])
505
+ else
506
+ expect(ActiveRecord::Base.logger).to receive(:warn).with(warning_template % [
507
+ "MySQL server has gone away. Trying to reconnect in 0.5 seconds.",
508
+ "#{mysql_error}: Lost connection to MySQL server during query [ActiveRecord::StatementInvalid]",
509
+ ])
510
+ end
511
+
512
+ expect(Employee.all.length).to eq 1000
513
+ MysqlServer.restart
514
+ expect(Employee.all.length).to eq 1000
515
+ end
516
+ end
517
+ end
518
+
519
+ # NOTE: The following test need to execute at the last
520
+ context "when the custom error is happened" do
521
+ before do
522
+ allow_any_instance_of(Mysql2::Error).to receive(:message).and_return('ZapZapZap')
523
+ Activerecord::Mysql::Reconnect.handle_rw_error_messages.update(zapzapzap: 'ZapZapZap')
524
+ end
525
+
526
+ specify do
527
+ th = thread_start {
528
+ emp = Employee.create(
529
+ :emp_no => 9999,
530
+ :birth_date => Time.now,
531
+ # wait 10 sec
532
+ :first_name => "' + sleep(10) + '",
533
+ :last_name => 'Tiger',
534
+ :hire_date => Time.now
535
+ )
536
+
537
+ expect(emp.id).to eq 1001
538
+ expect(emp.emp_no).to eq 9999
539
+ }
540
+
541
+ MysqlServer.restart
542
+ th.join
543
+ end
544
+ end
545
+ end