activerecord-mysql-reconnect 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/.gitignore +1 -0
- data/.rspec +2 -0
- data/README.md +30 -6
- data/Rakefile +5 -1
- data/activerecord-mysql-reconnect.gemspec +12 -11
- data/lib/activerecord/mysql/reconnect/abstract_mysql_adapter.rb +108 -18
- data/lib/activerecord/mysql/reconnect/version.rb +1 -1
- data/spec/activerecord/mysql/reconnect_spec.rb +168 -0
- data/spec/employees.sql +36 -0
- data/spec/spec_helper.rb +31 -0
- metadata +39 -18
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
ZGJkZjJmZDJkNDA4OWVhY2EzOTZjOGEzYzVlMDkyNzJjZDY5MzZiOA==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4dc078edb058b76e0a2608757ee28874723e17b0
|
4
|
+
data.tar.gz: 3ce76b372e6dcc6c0ea9a1d3c92852376671aa86
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
NzNlMGU2MGZjYjVlZDU1MmFhYjFiNTQ5ZmI1OGZlZmRhZjRmMjI4ZDg5M2Jm
|
11
|
-
YmZiYzMwMzcwNmEyZmY2Y2M0NTkwN2IxOWY5NWE1MGNiNjQ1Mzk=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
M2ZmNjNmNjdlMTg2YzcwZTU0OWJiMWU2ZThkMWUwYThiNzNmMmI5ZGZkNzIy
|
14
|
-
MzNjNjc3NTZkOTg2MjY2M2RjZmFmZjAyYmE0Y2IxMDNjYzE2OWE3ZjYxNDZk
|
15
|
-
MzUzNWE3YzE3NDQyNmFmNjY1OWI1ODhmMTE2MWQzMThmOGQyYTA=
|
6
|
+
metadata.gz: f026083fd7126a3910045110d0fb697b004667eef51ad4bbaa201256ecdd8b216b1ba11401b19faa730e4a0f7bc2207ff11d62e25eb0c5c0c0508f942d2ee1d9
|
7
|
+
data.tar.gz: b0a162b084442e8fc6990f736f55cfedd5c36fb620403fbe3b1ec8351b8a5bf36b69058dc1564051474f14aec4da8667341e45ee8c0cc7acbe5295f1cfe9ab52
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/README.md
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
-
#
|
1
|
+
# activerecord-mysql-reconnect
|
2
2
|
|
3
3
|
It is the library to reconnect automatically when ActiveRecord is disconnected from MySQL.
|
4
4
|
|
5
|
+
[![Gem Version](https://badge.fury.io/rb/activerecord-mysql-reconnect.png)](http://badge.fury.io/rb/activerecord-mysql-reconnect)
|
6
|
+
[![Build Status](https://drone.io/bitbucket.org/winebarrel/activerecord-mysql-reconnect/status.png)](https://drone.io/bitbucket.org/winebarrel/activerecord-mysql-reconnect/latest)
|
7
|
+
|
5
8
|
## Installation
|
6
9
|
|
7
10
|
Add this line to your application's Gemfile:
|
@@ -19,8 +22,6 @@ Or install it yourself as:
|
|
19
22
|
## Usage
|
20
23
|
|
21
24
|
```ruby
|
22
|
-
#!/usr/bin/env ruby
|
23
|
-
require 'active_record'
|
24
25
|
require 'activerecord/mysql/reconnect'
|
25
26
|
require 'logger'
|
26
27
|
|
@@ -28,7 +29,7 @@ ActiveRecord::Base.establish_connection(
|
|
28
29
|
adapter: 'mysql2',
|
29
30
|
host: '127.0.0.1',
|
30
31
|
username: 'root',
|
31
|
-
database: '
|
32
|
+
database: 'employees',
|
32
33
|
)
|
33
34
|
|
34
35
|
ActiveRecord::Base.logger = Logger.new($stdout)
|
@@ -55,6 +56,29 @@ MySQL server has gone away. Trying to reconnect in 0.5 seconds.
|
|
55
56
|
300024
|
56
57
|
```
|
57
58
|
|
58
|
-
|
59
|
+
### without_retry
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
ActiveRecord::Base.without_retry do
|
63
|
+
Employee.count
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
### retryable_transaction
|
59
68
|
|
60
|
-
|
69
|
+
```ruby
|
70
|
+
ActiveRecord::Base.retryable_transaction do
|
71
|
+
Employee.create(emp_no: 1, birth_date: Time.now, first_name: 'Scott', last_name: 'Tiger', hire_date: Time.now)
|
72
|
+
Employee.create(emp_no: 2, birth_date: Time.now, first_name: 'Scott', last_name: 'Tiger', hire_date: Time.now)
|
73
|
+
Employee.create(emp_no: 3, birth_date: Time.now, first_name: 'Scott', last_name: 'Tiger', hire_date: Time.now)
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
## Running tests
|
78
|
+
|
79
|
+
```sh
|
80
|
+
mysql.server start
|
81
|
+
export ACTIVERECORD_MYSQL_RECONNECT_MYSQL_RESTART='mysql.server restart'
|
82
|
+
bundle install
|
83
|
+
bundle exec rake
|
84
|
+
```
|
data/Rakefile
CHANGED
@@ -4,22 +4,23 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'activerecord/mysql/reconnect/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'activerecord-mysql-reconnect'
|
8
8
|
spec.version = Activerecord::Mysql::Reconnect::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
9
|
+
spec.authors = ['Genki Sugawara']
|
10
|
+
spec.email = ['sugawara@cookpad.com']
|
11
11
|
spec.description = %q{It is the library to reconnect automatically when ActiveRecord is disconnected from MySQL.}
|
12
|
-
spec.summary =
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
12
|
+
spec.summary = spec.description
|
13
|
+
spec.homepage = 'https://bitbucket.org/winebarrel/activerecord-mysql-reconnect'
|
14
|
+
spec.license = 'MIT'
|
15
15
|
|
16
16
|
spec.files = `git ls-files`.split($/)
|
17
17
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
-
spec.require_paths = [
|
19
|
+
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
spec.add_dependency
|
22
|
-
spec.
|
23
|
-
spec.add_development_dependency
|
24
|
-
spec.add_development_dependency
|
21
|
+
spec.add_dependency 'activerecord', '~> 3.2.14'
|
22
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
23
|
+
spec.add_development_dependency 'rake'
|
24
|
+
spec.add_development_dependency 'rspec', '>= 2.14.1'
|
25
|
+
spec.add_development_dependency 'mysql2'
|
25
26
|
end
|
@@ -1,35 +1,125 @@
|
|
1
1
|
require 'active_record'
|
2
2
|
require 'active_record/connection_adapters/abstract_mysql_adapter'
|
3
3
|
require 'active_support'
|
4
|
-
require '
|
4
|
+
require 'logger'
|
5
5
|
|
6
6
|
class ActiveRecord::Base
|
7
7
|
class_attribute :execution_tries
|
8
8
|
class_attribute :execution_retry_wait
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def without_retry
|
12
|
+
begin
|
13
|
+
Thread.current[ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::WITHOUT_RETRY_KEY] = true
|
14
|
+
yield
|
15
|
+
ensure
|
16
|
+
Thread.current[ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::WITHOUT_RETRY_KEY] = nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def retryable_transaction
|
21
|
+
begin
|
22
|
+
Thread.current[ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::RETRYABLE_TRANSACTION_KEY] = []
|
23
|
+
|
24
|
+
ActiveRecord::Base.transaction do
|
25
|
+
yield
|
26
|
+
end
|
27
|
+
ensure
|
28
|
+
Thread.current[ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::RETRYABLE_TRANSACTION_KEY] = nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
9
32
|
end
|
10
33
|
|
11
34
|
class ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
|
12
|
-
|
13
|
-
DEFAULT_EXECUTION_TRIES = 1
|
35
|
+
DEFAULT_EXECUTION_TRIES = 3
|
14
36
|
DEFAULT_EXECUTION_RETRY_WAIT = 0.5
|
15
37
|
|
38
|
+
ERROR_MESSAGES = [
|
39
|
+
'MySQL server has gone away',
|
40
|
+
'Server shutdown in progress',
|
41
|
+
'closed MySQL connection',
|
42
|
+
"Can't connect to MySQL server",
|
43
|
+
'Query execution was interrupted',
|
44
|
+
]
|
45
|
+
|
46
|
+
WITHOUT_RETRY_KEY = 'activerecord-mysql-reconnect-without-retry'
|
47
|
+
RETRYABLE_TRANSACTION_KEY = 'activerecord-mysql-reconnect-transaction-retry'
|
48
|
+
|
16
49
|
def execute_with_reconnect(sql, name = nil)
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
}
|
27
|
-
}
|
28
|
-
|
29
|
-
retryable(retryable_options) do
|
30
|
-
execute_without_reconnect(sql, name)
|
50
|
+
retryable(sql, name) do |sql_names|
|
51
|
+
retval = nil
|
52
|
+
|
53
|
+
sql_names.each do |s, n|
|
54
|
+
retval = execute_without_reconnect(s, n)
|
55
|
+
end
|
56
|
+
|
57
|
+
add_sql_to_transaction(sql, name)
|
58
|
+
retval
|
31
59
|
end
|
32
60
|
end
|
33
|
-
|
61
|
+
|
34
62
|
alias_method_chain :execute, :reconnect
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def retryable(sql, name, &block)
|
67
|
+
tries = ActiveRecord::Base.execution_tries || DEFAULT_EXECUTION_TRIES
|
68
|
+
logger = ActiveRecord::Base.logger || Logger.new($stderr)
|
69
|
+
block_with_reconnect = nil
|
70
|
+
retval = nil
|
71
|
+
sql_names = [[sql, name]]
|
72
|
+
|
73
|
+
retryable_loop(tries) do |n|
|
74
|
+
begin
|
75
|
+
retval = (block_with_reconnect || block).call(sql_names)
|
76
|
+
break
|
77
|
+
rescue => e
|
78
|
+
if not without_retry? and (tries.zero? or n < tries) and e.message =~ Regexp.union(ERROR_MESSAGES)
|
79
|
+
unless block_with_reconnect
|
80
|
+
block_with_reconnect = proc {|i| reconnect! ; block.call(i) }
|
81
|
+
end
|
82
|
+
|
83
|
+
sql_names = merge_transaction(sql, name)
|
84
|
+
wait = (ActiveRecord::Base.execution_retry_wait || DEFAULT_EXECUTION_RETRY_WAIT) * n
|
85
|
+
logger.warn("MySQL server has gone away. Trying to reconnect in #{wait} seconds. (cause: #{e} [#{e.class}])")
|
86
|
+
sleep(wait)
|
87
|
+
|
88
|
+
next
|
89
|
+
else
|
90
|
+
raise e
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
return retval
|
96
|
+
end
|
97
|
+
|
98
|
+
def retryable_loop(n)
|
99
|
+
if n.zero?
|
100
|
+
loop { n += 1 ; yield(n) }
|
101
|
+
else
|
102
|
+
n.times {|i| yield(i + 1) }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def without_retry?
|
107
|
+
!!Thread.current[WITHOUT_RETRY_KEY]
|
108
|
+
end
|
109
|
+
|
110
|
+
def add_sql_to_transaction(sql, name)
|
111
|
+
if (buf = Thread.current[RETRYABLE_TRANSACTION_KEY])
|
112
|
+
buf << [sql, name]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def merge_transaction(sql, name)
|
117
|
+
sql_name = [sql, name]
|
118
|
+
|
119
|
+
if (buf = Thread.current[RETRYABLE_TRANSACTION_KEY])
|
120
|
+
buf + [sql_name]
|
121
|
+
else
|
122
|
+
[sql_name]
|
123
|
+
end
|
124
|
+
end
|
35
125
|
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
describe Hash do
|
2
|
+
it 'select all' do
|
3
|
+
expect {
|
4
|
+
expect(Employee.all.length).to eq(300024)
|
5
|
+
mysql_restart
|
6
|
+
expect(Employee.all.length).to eq(300024)
|
7
|
+
}.to_not raise_error
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'count' do
|
11
|
+
expect {
|
12
|
+
expect(Employee.count).to eq(300024)
|
13
|
+
mysql_restart
|
14
|
+
expect(Employee.count).to eq(300024)
|
15
|
+
}.to_not raise_error
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'on select' do
|
19
|
+
expect {
|
20
|
+
thread_running = false
|
21
|
+
|
22
|
+
th = Thread.start {
|
23
|
+
thread_running = true
|
24
|
+
expect(Employee.where(:id => 1).pluck('sleep(15)')).to eq([1])
|
25
|
+
thread_running = false
|
26
|
+
}
|
27
|
+
|
28
|
+
th.abort_on_exception = true
|
29
|
+
sleep 3
|
30
|
+
expect(thread_running).to be_true
|
31
|
+
mysql_restart
|
32
|
+
expect(Employee.count).to be >= 300024
|
33
|
+
th.join
|
34
|
+
}.to_not raise_error
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'on insert' do
|
38
|
+
expect {
|
39
|
+
thread_running = false
|
40
|
+
|
41
|
+
th = Thread.start {
|
42
|
+
thread_running = true
|
43
|
+
emp = Employee.create(
|
44
|
+
:emp_no => 1,
|
45
|
+
:birth_date => Time.now,
|
46
|
+
:first_name => "' + sleep(15) + '",
|
47
|
+
:last_name => 'Tiger',
|
48
|
+
:hire_date => Time.now
|
49
|
+
)
|
50
|
+
thread_running = false
|
51
|
+
expect(emp.id).to eq(300025)
|
52
|
+
expect(emp.emp_no).to eq(1)
|
53
|
+
}
|
54
|
+
|
55
|
+
th.abort_on_exception = true
|
56
|
+
sleep 3
|
57
|
+
expect(thread_running).to be_true
|
58
|
+
mysql_restart
|
59
|
+
expect(Employee.count).to be >= 300024
|
60
|
+
th.join
|
61
|
+
}.to_not raise_error
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'op update' do
|
65
|
+
expect {
|
66
|
+
thread_running = false
|
67
|
+
|
68
|
+
th = Thread.start {
|
69
|
+
thread_running = true
|
70
|
+
emp = Employee.where(:id => 1).first
|
71
|
+
emp.first_name = "' + sleep(15) + '"
|
72
|
+
emp.last_name = 'ZapZapZap'
|
73
|
+
emp.save!
|
74
|
+
thread_running = false
|
75
|
+
|
76
|
+
emp = Employee.where(:id => 1).first
|
77
|
+
expect(emp.last_name).to eq('ZapZapZap')
|
78
|
+
}
|
79
|
+
|
80
|
+
th.abort_on_exception = true
|
81
|
+
sleep 3
|
82
|
+
expect(thread_running).to be_true
|
83
|
+
mysql_restart
|
84
|
+
expect(Employee.count).to eq(300024)
|
85
|
+
th.join
|
86
|
+
}.to_not raise_error
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'without_retry' do
|
90
|
+
expect {
|
91
|
+
ActiveRecord::Base.without_retry do
|
92
|
+
Employee.count
|
93
|
+
mysql_restart
|
94
|
+
Employee.count
|
95
|
+
end
|
96
|
+
}.to raise_error(ActiveRecord::StatementInvalid)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'transaction' do
|
100
|
+
expect {
|
101
|
+
expect(Employee.count).to eq(300024)
|
102
|
+
|
103
|
+
ActiveRecord::Base.transaction do
|
104
|
+
emp = Employee.create(
|
105
|
+
:emp_no => 1,
|
106
|
+
:birth_date => Time.now,
|
107
|
+
:first_name => 'Scott',
|
108
|
+
:last_name => 'Tiger',
|
109
|
+
:hire_date => Time.now
|
110
|
+
)
|
111
|
+
expect(emp.id).to eq(300025)
|
112
|
+
expect(emp.emp_no).to eq(1)
|
113
|
+
mysql_restart
|
114
|
+
emp = Employee.create(
|
115
|
+
:emp_no => 2,
|
116
|
+
:birth_date => Time.now,
|
117
|
+
:first_name => 'Scott',
|
118
|
+
:last_name => 'Tiger',
|
119
|
+
:hire_date => Time.now
|
120
|
+
)
|
121
|
+
expect(emp.id).to eq(300025)
|
122
|
+
expect(emp.emp_no).to eq(2)
|
123
|
+
end
|
124
|
+
|
125
|
+
expect(Employee.count).to eq(300025)
|
126
|
+
}.to_not raise_error
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'retryable_transaction' do
|
130
|
+
expect {
|
131
|
+
expect(Employee.count).to eq(300024)
|
132
|
+
|
133
|
+
ActiveRecord::Base.retryable_transaction do
|
134
|
+
emp = Employee.create(
|
135
|
+
:emp_no => 1,
|
136
|
+
:birth_date => Time.now,
|
137
|
+
:first_name => 'Scott',
|
138
|
+
:last_name => 'Tiger',
|
139
|
+
:hire_date => Time.now
|
140
|
+
)
|
141
|
+
expect(emp.id).to eq(300025)
|
142
|
+
expect(emp.emp_no).to eq(1)
|
143
|
+
mysql_restart
|
144
|
+
emp = Employee.create(
|
145
|
+
:emp_no => 2,
|
146
|
+
:birth_date => Time.now,
|
147
|
+
:first_name => 'Scott',
|
148
|
+
:last_name => 'Tiger',
|
149
|
+
:hire_date => Time.now
|
150
|
+
)
|
151
|
+
expect(emp.id).to eq(300026)
|
152
|
+
expect(emp.emp_no).to eq(2)
|
153
|
+
mysql_restart
|
154
|
+
emp = Employee.create(
|
155
|
+
:emp_no => 3,
|
156
|
+
:birth_date => Time.now,
|
157
|
+
:first_name => 'Scott',
|
158
|
+
:last_name => 'Tiger',
|
159
|
+
:hire_date => Time.now
|
160
|
+
)
|
161
|
+
expect(emp.id).to eq(300027)
|
162
|
+
expect(emp.emp_no).to eq(3)
|
163
|
+
end
|
164
|
+
|
165
|
+
expect(Employee.count).to eq(300027)
|
166
|
+
}.to_not raise_error
|
167
|
+
end
|
168
|
+
end
|