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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9ac7822c0f663ad7cf90036be1093e185b6a7510
4
- data.tar.gz: 0e47d5cf1f48399f9f120d7596206521ba91be5d
3
+ metadata.gz: 22681715c79fdec28b4dcffd8a5f1e73a9c45f71
4
+ data.tar.gz: 977d88bd69f012e89270b17730da3312031966eb
5
5
  SHA512:
6
- metadata.gz: 3e02a754713686f67ef46dddb0091c34caf7b43b596048ed5f83543f13093d61dfd5774dad7010a163a9e48474760736a4bec063b1424f6b13b2a95beb50c93b
7
- data.tar.gz: 62edd5b7910a1723d334e91bab60e89ffb3f70c2c2886ffae06cee0857c123efb66786652ad73e2d0f67f0dd403acd87c50352b5c7b5cc677c720161d9276366
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
- ## Running tests
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,3 @@
1
+ class ActiveRecord::ConnectionAdapters::AbstractAdapter
2
+ # XXX:
3
+ end
@@ -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
@@ -0,0 +1,11 @@
1
+ class ActiveRecord::ConnectionAdapters::ConnectionPool
2
+ def new_connection_with_retry
3
+ Activerecord::Mysql::Reconnect.retryable(
4
+ :proc => proc {
5
+ new_connection_without_retry
6
+ }
7
+ )
8
+ end
9
+
10
+ alias_method_chain :new_connection, :retry
11
+ end
@@ -0,0 +1,11 @@
1
+ class ActiveRecord::ConnectionAdapters::Mysql2Adapter
2
+ def reconnect_with_retry!
3
+ Activerecord::Mysql::Reconnect.retryable(
4
+ :proc => proc {
5
+ reconnect_without_retry!
6
+ }
7
+ )
8
+ end
9
+
10
+ alias_method_chain :reconnect!, :retry
11
+ end
@@ -1,7 +1,7 @@
1
1
  module Activerecord
2
2
  module Mysql
3
3
  module Reconnect
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
  end
6
6
  end
7
7
  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/abstract_mysql_adapter'
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.1.1
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-03 00:00:00.000000000 Z
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/abstract_mysql_adapter.rb
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/mysql/reconnect_spec.rb
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/mysql/reconnect_spec.rb
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