activerecord-mysql-reconnect 0.1.1 → 0.2.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 +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
|