activerecord-mysql-reconnect-new 0.6.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.
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