activerecord-mysql-reconnect 0.0.1 → 0.1.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.
- 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
|
+
[](http://badge.fury.io/rb/activerecord-mysql-reconnect)
|
6
|
+
[](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
|