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 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