activerecord-mysql-reconnect 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +22 -1
- data/lib/activerecord/mysql/reconnect/abstract_adapter_ext.rb +3 -0
- data/lib/activerecord/mysql/reconnect/abstract_mysql_adapter_ext.rb +57 -0
- data/lib/activerecord/mysql/reconnect/base_ext.rb +18 -0
- data/lib/activerecord/mysql/reconnect/connection_pool_ext.rb +11 -0
- data/lib/activerecord/mysql/reconnect/mysql2_adapter_ext.rb +11 -0
- data/lib/activerecord/mysql/reconnect/version.rb +1 -1
- data/lib/activerecord/mysql/reconnect.rb +128 -1
- data/spec/{activerecord/mysql/reconnect_spec.rb → activerecord-mysql-reconnect_spec.rb} +48 -0
- data/spec/spec_helper.rb +10 -0
- metadata +9 -5
- data/lib/activerecord/mysql/reconnect/abstract_mysql_adapter.rb +0 -131
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 22681715c79fdec28b4dcffd8a5f1e73a9c45f71
|
4
|
+
data.tar.gz: 977d88bd69f012e89270b17730da3312031966eb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: febba5eaf59e2e0704c1b5dbaf47d4b5ab43eafd93de02eaafb6d5bec8edb1b805666cff5524c70851d6905bdfc2bafb0d9598e0c9a9bf3233f2a50bd4e56145
|
7
|
+
data.tar.gz: 104afd1d2871e16ce7885aa9b236af13906a40bd7b833737d7d24c37731d844b7df9b06ff2773683e0c78841cece01e8e907d5300444f7f9cc3fe3906c88b355
|
data/README.md
CHANGED
@@ -74,10 +74,31 @@ ActiveRecord::Base.retryable_transaction do
|
|
74
74
|
end
|
75
75
|
```
|
76
76
|
|
77
|
-
##
|
77
|
+
## Use on rails
|
78
|
+
|
79
|
+
### Gemfile
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
gem 'activerecord-mysql-reconnect'
|
83
|
+
```
|
84
|
+
|
85
|
+
### environment file
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
MyApp::Application.configure do
|
89
|
+
...
|
90
|
+
config.active_record.execution_tries = 10 # times
|
91
|
+
config.active_record.execution_retry_wait = 1.5 # sec
|
92
|
+
...
|
93
|
+
ene
|
94
|
+
```
|
95
|
+
|
96
|
+
## Running tests on local
|
78
97
|
|
79
98
|
```sh
|
80
99
|
mysql.server start
|
100
|
+
export ACTIVERECORD_MYSQL_RECONNECT_MYSQL_START='mysql.server start'
|
101
|
+
export ACTIVERECORD_MYSQL_RECONNECT_MYSQL_STOP='mysql.server stop'
|
81
102
|
export ACTIVERECORD_MYSQL_RECONNECT_MYSQL_RESTART='mysql.server restart'
|
82
103
|
bundle install
|
83
104
|
bundle exec rake
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
|
2
|
+
def execute_with_reconnect(sql, name = nil)
|
3
|
+
retryable(sql, name) do |sql_names|
|
4
|
+
retval = nil
|
5
|
+
|
6
|
+
sql_names.each do |s, n|
|
7
|
+
retval = execute_without_reconnect(s, n)
|
8
|
+
end
|
9
|
+
|
10
|
+
add_sql_to_transaction(sql, name)
|
11
|
+
retval
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
alias_method_chain :execute, :reconnect
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def retryable(sql, name, &block)
|
20
|
+
block_with_reconnect = nil
|
21
|
+
sql_names = [[sql, name]]
|
22
|
+
orig_transaction = @transaction
|
23
|
+
|
24
|
+
Activerecord::Mysql::Reconnect.retryable(
|
25
|
+
:proc => proc {
|
26
|
+
(block_with_reconnect || block).call(sql_names)
|
27
|
+
},
|
28
|
+
:on_error => proc {
|
29
|
+
unless block_with_reconnect
|
30
|
+
block_with_reconnect = proc do |i|
|
31
|
+
reconnect_without_retry!
|
32
|
+
@transaction = orig_transaction if orig_transaction
|
33
|
+
block.call(i)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
sql_names = merge_transaction(sql, name)
|
38
|
+
}
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_sql_to_transaction(sql, name)
|
43
|
+
if (buf = Activerecord::Mysql::Reconnect.retryable_transaction_buffer)
|
44
|
+
buf << [sql, name]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def merge_transaction(sql, name)
|
49
|
+
sql_name = [sql, name]
|
50
|
+
|
51
|
+
if (buf = Activerecord::Mysql::Reconnect.retryable_transaction_buffer)
|
52
|
+
buf + [sql_name]
|
53
|
+
else
|
54
|
+
[sql_name]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class ActiveRecord::Base
|
2
|
+
class_attribute :execution_tries, :instance_writer => false
|
3
|
+
class_attribute :execution_retry_wait, :instance_writer => false
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def without_retry
|
7
|
+
Activerecord::Mysql::Reconnect.without_retry do
|
8
|
+
yield
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def retryable_transaction
|
13
|
+
Activerecord::Mysql::Reconnect.retryable_transaction do
|
14
|
+
yield
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,2 +1,129 @@
|
|
1
|
+
require 'mysql2'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
require 'active_record'
|
5
|
+
require 'active_record/base'
|
6
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
7
|
+
require 'active_record/connection_adapters/abstract_mysql_adapter'
|
8
|
+
require 'active_record/connection_adapters/mysql2_adapter'
|
9
|
+
require 'active_record/connection_adapters/abstract/connection_pool'
|
10
|
+
require 'active_support'
|
11
|
+
|
1
12
|
require 'activerecord/mysql/reconnect/version'
|
2
|
-
require 'activerecord/mysql/reconnect/
|
13
|
+
require 'activerecord/mysql/reconnect/base_ext'
|
14
|
+
require 'activerecord/mysql/reconnect/abstract_adapter_ext'
|
15
|
+
require 'activerecord/mysql/reconnect/abstract_mysql_adapter_ext'
|
16
|
+
require 'activerecord/mysql/reconnect/mysql2_adapter_ext'
|
17
|
+
require 'activerecord/mysql/reconnect/connection_pool_ext'
|
18
|
+
|
19
|
+
module Activerecord::Mysql::Reconnect
|
20
|
+
DEFAULT_EXECUTION_TRIES = 3
|
21
|
+
DEFAULT_EXECUTION_RETRY_WAIT = 0.5
|
22
|
+
|
23
|
+
WITHOUT_RETRY_KEY = 'activerecord-mysql-reconnect-without-retry'
|
24
|
+
RETRYABLE_TRANSACTION_KEY = 'activerecord-mysql-reconnect-transaction-retry'
|
25
|
+
|
26
|
+
HANDLE_ERROR = [
|
27
|
+
ActiveRecord::StatementInvalid,
|
28
|
+
Mysql2::Error,
|
29
|
+
]
|
30
|
+
|
31
|
+
HANDLE_ERROR_MESSAGES = [
|
32
|
+
'MySQL server has gone away',
|
33
|
+
'Server shutdown in progress',
|
34
|
+
'closed MySQL connection',
|
35
|
+
"Can't connect to MySQL server",
|
36
|
+
'Query execution was interrupted',
|
37
|
+
'Access denied for user',
|
38
|
+
'Lost connection to MySQL server during query',
|
39
|
+
]
|
40
|
+
|
41
|
+
class << self
|
42
|
+
def execution_tries
|
43
|
+
ActiveRecord::Base.execution_tries || DEFAULT_EXECUTION_TRIES
|
44
|
+
end
|
45
|
+
|
46
|
+
def execution_retry_wait
|
47
|
+
ActiveRecord::Base.execution_retry_wait || DEFAULT_EXECUTION_RETRY_WAIT
|
48
|
+
end
|
49
|
+
|
50
|
+
def retryable(opts)
|
51
|
+
block = opts.fetch(:proc)
|
52
|
+
on_error = opts[:on_error]
|
53
|
+
tries = self.execution_tries
|
54
|
+
retval = nil
|
55
|
+
|
56
|
+
retryable_loop(tries) do |n|
|
57
|
+
begin
|
58
|
+
retval = block.call
|
59
|
+
break
|
60
|
+
rescue => e
|
61
|
+
if (tries.zero? or n < tries) and should_handle?(e)
|
62
|
+
on_error.call if on_error
|
63
|
+
wait = self.execution_retry_wait * n
|
64
|
+
logger.warn("MySQL server has gone away. Trying to reconnect in #{wait} seconds. (cause: #{e} [#{e.class}])")
|
65
|
+
sleep(wait)
|
66
|
+
next
|
67
|
+
else
|
68
|
+
raise e
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
return retval
|
74
|
+
end
|
75
|
+
|
76
|
+
def logger
|
77
|
+
if defined?(Rails)
|
78
|
+
Rails.logger || ActiveRecord::Base.logger || Logger.new($stderr)
|
79
|
+
else
|
80
|
+
ActiveRecord::Base.logger || Logger.new($stderr)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def without_retry
|
85
|
+
begin
|
86
|
+
Thread.current[WITHOUT_RETRY_KEY] = true
|
87
|
+
yield
|
88
|
+
ensure
|
89
|
+
Thread.current[WITHOUT_RETRY_KEY] = nil
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def without_retry?
|
94
|
+
!!Thread.current[WITHOUT_RETRY_KEY]
|
95
|
+
end
|
96
|
+
|
97
|
+
def retryable_transaction
|
98
|
+
begin
|
99
|
+
Thread.current[RETRYABLE_TRANSACTION_KEY] = []
|
100
|
+
|
101
|
+
ActiveRecord::Base.transaction do
|
102
|
+
yield
|
103
|
+
end
|
104
|
+
ensure
|
105
|
+
Thread.current[RETRYABLE_TRANSACTION_KEY] = nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def retryable_transaction_buffer
|
110
|
+
Thread.current[RETRYABLE_TRANSACTION_KEY]
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def retryable_loop(n)
|
116
|
+
if n.zero?
|
117
|
+
loop { n += 1 ; yield(n) }
|
118
|
+
else
|
119
|
+
n.times {|i| yield(i + 1) }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def should_handle?(e)
|
124
|
+
!without_retry? &&
|
125
|
+
HANDLE_ERROR.any? {|i| e.kind_of?(i) } &&
|
126
|
+
Regexp.union(HANDLE_ERROR_MESSAGES) =~ e.message
|
127
|
+
end
|
128
|
+
end # end of class methods
|
129
|
+
end
|
@@ -165,4 +165,52 @@ describe 'activerecord-mysql-reconnect' do
|
|
165
165
|
expect(Employee.count).to eq(300027)
|
166
166
|
}.to_not raise_error
|
167
167
|
end
|
168
|
+
|
169
|
+
it 'retry new connection' do
|
170
|
+
expect {
|
171
|
+
ActiveRecord::Base.clear_all_connections!
|
172
|
+
mysql_restart
|
173
|
+
expect(Employee.count).to eq(300024)
|
174
|
+
}.to_not raise_error
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'retry verify' do
|
178
|
+
expect {
|
179
|
+
thread_running = false
|
180
|
+
|
181
|
+
th = Thread.start {
|
182
|
+
thread_running = true
|
183
|
+
mysql_stop
|
184
|
+
sleep 15
|
185
|
+
mysql_start
|
186
|
+
thread_running = false
|
187
|
+
}
|
188
|
+
|
189
|
+
th.abort_on_exception = true
|
190
|
+
sleep 3
|
191
|
+
expect(thread_running).to be_true
|
192
|
+
ActiveRecord::Base.connection.verify!
|
193
|
+
th.join
|
194
|
+
}.to_not raise_error
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'retry reconnect' do
|
198
|
+
expect {
|
199
|
+
thread_running = false
|
200
|
+
|
201
|
+
th = Thread.start {
|
202
|
+
thread_running = true
|
203
|
+
mysql_stop
|
204
|
+
sleep 15
|
205
|
+
mysql_start
|
206
|
+
thread_running = false
|
207
|
+
}
|
208
|
+
|
209
|
+
th.abort_on_exception = true
|
210
|
+
sleep 3
|
211
|
+
expect(thread_running).to be_true
|
212
|
+
ActiveRecord::Base.connection.reconnect!
|
213
|
+
th.join
|
214
|
+
}.to_not raise_error
|
215
|
+
end
|
168
216
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -3,6 +3,16 @@ require 'mysql2'
|
|
3
3
|
|
4
4
|
class Employee < ActiveRecord::Base; end
|
5
5
|
|
6
|
+
def mysql_start
|
7
|
+
cmd = ENV['ACTIVERECORD_MYSQL_RECONNECT_MYSQL_START'] || 'sudo /etc/init.d/mysql start'
|
8
|
+
system(cmd)
|
9
|
+
end
|
10
|
+
|
11
|
+
def mysql_stop
|
12
|
+
cmd = ENV['ACTIVERECORD_MYSQL_RECONNECT_MYSQL_STOP'] || 'sudo /etc/init.d/mysql stop'
|
13
|
+
system(cmd)
|
14
|
+
end
|
15
|
+
|
6
16
|
def mysql_restart
|
7
17
|
cmd = ENV['ACTIVERECORD_MYSQL_RECONNECT_MYSQL_RESTART'] || 'sudo /etc/init.d/mysql restart'
|
8
18
|
system(cmd)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-mysql-reconnect
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Genki Sugawara
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-01-
|
11
|
+
date: 2014-01-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -96,9 +96,13 @@ files:
|
|
96
96
|
- Rakefile
|
97
97
|
- activerecord-mysql-reconnect.gemspec
|
98
98
|
- lib/activerecord/mysql/reconnect.rb
|
99
|
-
- lib/activerecord/mysql/reconnect/
|
99
|
+
- lib/activerecord/mysql/reconnect/abstract_adapter_ext.rb
|
100
|
+
- lib/activerecord/mysql/reconnect/abstract_mysql_adapter_ext.rb
|
101
|
+
- lib/activerecord/mysql/reconnect/base_ext.rb
|
102
|
+
- lib/activerecord/mysql/reconnect/connection_pool_ext.rb
|
103
|
+
- lib/activerecord/mysql/reconnect/mysql2_adapter_ext.rb
|
100
104
|
- lib/activerecord/mysql/reconnect/version.rb
|
101
|
-
- spec/activerecord
|
105
|
+
- spec/activerecord-mysql-reconnect_spec.rb
|
102
106
|
- spec/employees.sql
|
103
107
|
- spec/spec_helper.rb
|
104
108
|
homepage: https://bitbucket.org/winebarrel/activerecord-mysql-reconnect
|
@@ -127,6 +131,6 @@ specification_version: 4
|
|
127
131
|
summary: It is the library to reconnect automatically when ActiveRecord is disconnected
|
128
132
|
from MySQL.
|
129
133
|
test_files:
|
130
|
-
- spec/activerecord
|
134
|
+
- spec/activerecord-mysql-reconnect_spec.rb
|
131
135
|
- spec/employees.sql
|
132
136
|
- spec/spec_helper.rb
|
@@ -1,131 +0,0 @@
|
|
1
|
-
require 'active_record'
|
2
|
-
require 'active_record/connection_adapters/abstract_mysql_adapter'
|
3
|
-
require 'active_support'
|
4
|
-
require 'mysql2'
|
5
|
-
require 'logger'
|
6
|
-
|
7
|
-
class ActiveRecord::Base
|
8
|
-
class_attribute :execution_tries
|
9
|
-
class_attribute :execution_retry_wait
|
10
|
-
|
11
|
-
class << self
|
12
|
-
def without_retry
|
13
|
-
begin
|
14
|
-
Thread.current[ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::WITHOUT_RETRY_KEY] = true
|
15
|
-
yield
|
16
|
-
ensure
|
17
|
-
Thread.current[ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::WITHOUT_RETRY_KEY] = nil
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def retryable_transaction
|
22
|
-
begin
|
23
|
-
Thread.current[ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::RETRYABLE_TRANSACTION_KEY] = []
|
24
|
-
|
25
|
-
ActiveRecord::Base.transaction do
|
26
|
-
yield
|
27
|
-
end
|
28
|
-
ensure
|
29
|
-
Thread.current[ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::RETRYABLE_TRANSACTION_KEY] = nil
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
class ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
|
36
|
-
DEFAULT_EXECUTION_TRIES = 3
|
37
|
-
DEFAULT_EXECUTION_RETRY_WAIT = 0.5
|
38
|
-
|
39
|
-
ERROR_MESSAGES = [
|
40
|
-
'MySQL server has gone away',
|
41
|
-
'Server shutdown in progress',
|
42
|
-
'closed MySQL connection',
|
43
|
-
"Can't connect to MySQL server",
|
44
|
-
'Query execution was interrupted',
|
45
|
-
]
|
46
|
-
|
47
|
-
WITHOUT_RETRY_KEY = 'activerecord-mysql-reconnect-without-retry'
|
48
|
-
RETRYABLE_TRANSACTION_KEY = 'activerecord-mysql-reconnect-transaction-retry'
|
49
|
-
|
50
|
-
def execute_with_reconnect(sql, name = nil)
|
51
|
-
retryable(sql, name) do |sql_names|
|
52
|
-
retval = nil
|
53
|
-
|
54
|
-
sql_names.each do |s, n|
|
55
|
-
retval = execute_without_reconnect(s, n)
|
56
|
-
end
|
57
|
-
|
58
|
-
add_sql_to_transaction(sql, name)
|
59
|
-
retval
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
alias_method_chain :execute, :reconnect
|
64
|
-
|
65
|
-
private
|
66
|
-
|
67
|
-
def retryable(sql, name, &block)
|
68
|
-
tries = ActiveRecord::Base.execution_tries || DEFAULT_EXECUTION_TRIES
|
69
|
-
logger = ActiveRecord::Base.logger || Logger.new($stderr)
|
70
|
-
block_with_reconnect = nil
|
71
|
-
retval = nil
|
72
|
-
sql_names = [[sql, name]]
|
73
|
-
orig_transaction = @transaction
|
74
|
-
|
75
|
-
retryable_loop(tries) do |n|
|
76
|
-
begin
|
77
|
-
retval = (block_with_reconnect || block).call(sql_names)
|
78
|
-
break
|
79
|
-
rescue ActiveRecord::StatementInvalid, Mysql2::Error => e
|
80
|
-
if not without_retry? and (tries.zero? or n < tries) and e.message =~ Regexp.union(ERROR_MESSAGES)
|
81
|
-
unless block_with_reconnect
|
82
|
-
block_with_reconnect = proc do |i|
|
83
|
-
reconnect!
|
84
|
-
@transaction = orig_transaction if orig_transaction
|
85
|
-
block.call(i)
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
sql_names = merge_transaction(sql, name)
|
90
|
-
wait = (ActiveRecord::Base.execution_retry_wait || DEFAULT_EXECUTION_RETRY_WAIT) * n
|
91
|
-
logger.warn("MySQL server has gone away. Trying to reconnect in #{wait} seconds. (cause: #{e} [#{e.class}])")
|
92
|
-
sleep(wait)
|
93
|
-
|
94
|
-
next
|
95
|
-
else
|
96
|
-
raise e
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
return retval
|
102
|
-
end
|
103
|
-
|
104
|
-
def retryable_loop(n)
|
105
|
-
if n.zero?
|
106
|
-
loop { n += 1 ; yield(n) }
|
107
|
-
else
|
108
|
-
n.times {|i| yield(i + 1) }
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
def without_retry?
|
113
|
-
!!Thread.current[WITHOUT_RETRY_KEY]
|
114
|
-
end
|
115
|
-
|
116
|
-
def add_sql_to_transaction(sql, name)
|
117
|
-
if (buf = Thread.current[RETRYABLE_TRANSACTION_KEY])
|
118
|
-
buf << [sql, name]
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
def merge_transaction(sql, name)
|
123
|
-
sql_name = [sql, name]
|
124
|
-
|
125
|
-
if (buf = Thread.current[RETRYABLE_TRANSACTION_KEY])
|
126
|
-
buf + [sql_name]
|
127
|
-
else
|
128
|
-
[sql_name]
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|