activerecord-mysql-reconnect-new 0.6.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 +7 -0
- data/.gitignore +21 -0
- data/.idea/.gitignore +8 -0
- data/.idea/activerecord-mysql-reconnect.iml +22 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.rspec +3 -0
- data/.travis.yml +45 -0
- data/Appraisals +28 -0
- data/ChangeLog +35 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +124 -0
- data/Rakefile +5 -0
- data/activerecord-mysql-reconnect-new.gemspec +28 -0
- data/docker-compose.yml +6 -0
- data/gemfiles/activerecord_4.2.gemfile +8 -0
- data/gemfiles/activerecord_5.0.gemfile +7 -0
- data/gemfiles/activerecord_5.1.gemfile +7 -0
- data/gemfiles/activerecord_5.2.gemfile +7 -0
- data/gemfiles/activerecord_6.0.gemfile +7 -0
- data/gemfiles/activerecord_6.1.gemfile +7 -0
- data/gemfiles/activerecord_master.gemfile +7 -0
- data/lib/activerecord/mysql/reconnect/abstract_mysql_adapter_ext.rb +46 -0
- data/lib/activerecord/mysql/reconnect/base_ext.rb +32 -0
- data/lib/activerecord/mysql/reconnect/connection_pool_ext.rb +18 -0
- data/lib/activerecord/mysql/reconnect/mysql2_adapter_ext.rb +13 -0
- data/lib/activerecord/mysql/reconnect/null_transaction_ext.rb +9 -0
- data/lib/activerecord/mysql/reconnect/version.rb +7 -0
- data/lib/activerecord/mysql/reconnect.rb +268 -0
- data/lib/activerecord-mysql-reconnect.rb +5 -0
- data/spec/activerecord-mysql-reconnect_spec.rb +545 -0
- data/spec/data.sql +1001 -0
- data/spec/employee_model.rb +1 -0
- data/spec/mysql2_ext.rb +5 -0
- data/spec/mysql_helper.rb +84 -0
- data/spec/spec_helper.rb +54 -0
- metadata +174 -0
@@ -0,0 +1,268 @@
|
|
1
|
+
require 'mysql2'
|
2
|
+
require 'logger'
|
3
|
+
require 'bigdecimal'
|
4
|
+
require 'strscan'
|
5
|
+
|
6
|
+
require 'active_record'
|
7
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
8
|
+
require 'active_record/connection_adapters/abstract_mysql_adapter'
|
9
|
+
require 'active_record/connection_adapters/mysql2_adapter'
|
10
|
+
require 'active_record/connection_adapters/abstract/connection_pool'
|
11
|
+
require 'active_record/connection_adapters/abstract/transaction'
|
12
|
+
|
13
|
+
require 'activerecord/mysql/reconnect/version'
|
14
|
+
require 'activerecord/mysql/reconnect/base_ext'
|
15
|
+
|
16
|
+
require 'activerecord/mysql/reconnect/abstract_mysql_adapter_ext'
|
17
|
+
require 'activerecord/mysql/reconnect/mysql2_adapter_ext'
|
18
|
+
require 'activerecord/mysql/reconnect/connection_pool_ext'
|
19
|
+
require 'activerecord/mysql/reconnect/null_transaction_ext'
|
20
|
+
|
21
|
+
module Activerecord::Mysql::Reconnect
|
22
|
+
DEFAULT_EXECUTION_TRIES = 3
|
23
|
+
DEFAULT_EXECUTION_RETRY_WAIT = 0.5
|
24
|
+
|
25
|
+
WITHOUT_RETRY_KEY = 'activerecord-mysql-reconnect-without-retry'
|
26
|
+
|
27
|
+
HANDLE_ERROR = [
|
28
|
+
ActiveRecord::StatementInvalid,
|
29
|
+
Mysql2::Error,
|
30
|
+
ActiveRecord::ConnectionNotEstablished
|
31
|
+
]
|
32
|
+
|
33
|
+
@@handle_r_error_messages = {
|
34
|
+
lost_connection: 'Lost connection to MySQL server during query',
|
35
|
+
}
|
36
|
+
|
37
|
+
@@handle_rw_error_messages = {
|
38
|
+
gone_away: 'MySQL server has gone away',
|
39
|
+
server_shutdown: 'Server shutdown in progress',
|
40
|
+
closed_connection: 'closed MySQL connection',
|
41
|
+
cannot_connect: "Can't connect to MySQL server",
|
42
|
+
interrupted: 'Query execution was interrupted',
|
43
|
+
access_denied: 'Access denied for user',
|
44
|
+
read_only: 'The MySQL server is running with the --read-only option',
|
45
|
+
cannot_connect_to_local: "Can't connect to local MySQL server", # When running in local sandbox, or using a socket file
|
46
|
+
unknown_host: 'Unknown MySQL server host', # For DNS blips
|
47
|
+
lost_connection: "Lost connection to MySQL server at 'reading initial communication packet'",
|
48
|
+
not_connected: "MySQL client is not connected",
|
49
|
+
killed: 'Connection was killed',
|
50
|
+
}
|
51
|
+
|
52
|
+
READ_SQL_REGEXP = /\A\s*(?:SELECT|SHOW|SET)\b/i
|
53
|
+
|
54
|
+
RETRY_MODES = [:r, :rw, :force]
|
55
|
+
DEFAULT_RETRY_MODE = :r
|
56
|
+
|
57
|
+
class << self
|
58
|
+
def handle_r_error_messages
|
59
|
+
@@handle_r_error_messages
|
60
|
+
end
|
61
|
+
|
62
|
+
def handle_rw_error_messages
|
63
|
+
@@handle_rw_error_messages
|
64
|
+
end
|
65
|
+
|
66
|
+
def execution_tries
|
67
|
+
ActiveRecord::Base.execution_tries || DEFAULT_EXECUTION_TRIES
|
68
|
+
end
|
69
|
+
|
70
|
+
def execution_retry_wait
|
71
|
+
wait = ActiveRecord::Base.execution_retry_wait || DEFAULT_EXECUTION_RETRY_WAIT
|
72
|
+
wait.kind_of?(BigDecimal) ? wait : BigDecimal(wait.to_s)
|
73
|
+
end
|
74
|
+
|
75
|
+
def enable_retry
|
76
|
+
!!ActiveRecord::Base.enable_retry
|
77
|
+
end
|
78
|
+
|
79
|
+
def retry_mode=(v)
|
80
|
+
unless RETRY_MODES.include?(v)
|
81
|
+
raise "Invalid retry_mode. Please set one of the following: #{RETRY_MODES.map {|i| i.inspect }.join(', ')}"
|
82
|
+
end
|
83
|
+
|
84
|
+
@activerecord_mysql_reconnect_retry_mode = v
|
85
|
+
end
|
86
|
+
|
87
|
+
def retry_mode
|
88
|
+
@activerecord_mysql_reconnect_retry_mode || DEFAULT_RETRY_MODE
|
89
|
+
end
|
90
|
+
|
91
|
+
def retry_databases=(v)
|
92
|
+
v ||= []
|
93
|
+
|
94
|
+
unless v.kind_of?(Array)
|
95
|
+
v = [v]
|
96
|
+
end
|
97
|
+
|
98
|
+
@activerecord_mysql_reconnect_retry_databases = v.map do |database|
|
99
|
+
if database.instance_of?(Symbol)
|
100
|
+
database = Regexp.escape(database.to_s)
|
101
|
+
[/.*/, /\A#{database}\z/]
|
102
|
+
else
|
103
|
+
host = '%'
|
104
|
+
database = database.to_s
|
105
|
+
|
106
|
+
if database =~ /:/
|
107
|
+
host, database = database.split(':', 2)
|
108
|
+
end
|
109
|
+
|
110
|
+
[create_pattern_match_regex(host), create_pattern_match_regex(database)]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def retry_databases
|
116
|
+
@activerecord_mysql_reconnect_retry_databases || []
|
117
|
+
end
|
118
|
+
|
119
|
+
def retryable(opts)
|
120
|
+
block = opts.fetch(:proc)
|
121
|
+
on_error = opts[:on_error]
|
122
|
+
conn = opts[:connection]
|
123
|
+
sql = opts[:sql]
|
124
|
+
tries = self.execution_tries
|
125
|
+
retval = nil
|
126
|
+
|
127
|
+
retryable_loop(tries) do |n|
|
128
|
+
begin
|
129
|
+
retval = block.call
|
130
|
+
break
|
131
|
+
rescue => e
|
132
|
+
if enable_retry and (tries.zero? or n < tries) and should_handle?(e, opts)
|
133
|
+
on_error.call if on_error
|
134
|
+
wait = self.execution_retry_wait * n
|
135
|
+
|
136
|
+
logger.warn("MySQL server has gone away. Trying to reconnect in #{wait.to_f} seconds. (#{build_error_message(e, sql, conn)})")
|
137
|
+
sleep(wait)
|
138
|
+
next
|
139
|
+
else
|
140
|
+
if enable_retry and n > 1
|
141
|
+
logger.warn("Query retry failed. (#{build_error_message(e, sql, conn)})")
|
142
|
+
end
|
143
|
+
|
144
|
+
raise e
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
return retval
|
150
|
+
end
|
151
|
+
|
152
|
+
def logger
|
153
|
+
if defined?(Rails)
|
154
|
+
Rails.logger || ActiveRecord::Base.logger || Logger.new($stderr)
|
155
|
+
else
|
156
|
+
ActiveRecord::Base.logger || Logger.new($stderr)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def without_retry
|
161
|
+
begin
|
162
|
+
Thread.current[WITHOUT_RETRY_KEY] = true
|
163
|
+
yield
|
164
|
+
ensure
|
165
|
+
Thread.current[WITHOUT_RETRY_KEY] = nil
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def without_retry?
|
170
|
+
!!Thread.current[WITHOUT_RETRY_KEY]
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
def retryable_loop(n)
|
176
|
+
if n.zero?
|
177
|
+
loop { n += 1 ; yield(n) }
|
178
|
+
else
|
179
|
+
n.times {|i| yield(i + 1) }
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def should_handle?(e, opts = {})
|
184
|
+
sql = opts[:sql]
|
185
|
+
retry_mode = opts[:retry_mode]
|
186
|
+
conn = opts[:connection]
|
187
|
+
|
188
|
+
if without_retry?
|
189
|
+
return false
|
190
|
+
end
|
191
|
+
|
192
|
+
if conn and not retry_databases.empty?
|
193
|
+
conn_info = connection_info(conn)
|
194
|
+
|
195
|
+
included = retry_databases.any? do |host, database|
|
196
|
+
host =~ conn_info[:host] and database =~ conn_info[:database]
|
197
|
+
end
|
198
|
+
|
199
|
+
return false unless included
|
200
|
+
end
|
201
|
+
|
202
|
+
unless HANDLE_ERROR.any? {|i| e.kind_of?(i) }
|
203
|
+
return false
|
204
|
+
end
|
205
|
+
|
206
|
+
unless Regexp.union(@@handle_r_error_messages.values + @@handle_rw_error_messages.values) =~ e.message
|
207
|
+
return false
|
208
|
+
end
|
209
|
+
|
210
|
+
if sql and READ_SQL_REGEXP !~ sql
|
211
|
+
if retry_mode == :r
|
212
|
+
return false
|
213
|
+
end
|
214
|
+
|
215
|
+
if retry_mode != :force and Regexp.union(@@handle_r_error_messages.values) =~ e.message
|
216
|
+
return false
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
return true
|
221
|
+
end
|
222
|
+
|
223
|
+
def connection_info(conn)
|
224
|
+
conn_info = {}
|
225
|
+
|
226
|
+
if conn.kind_of?(Mysql2::Client)
|
227
|
+
[:host, :database, :username].each {|k| conn_info[k] = conn.query_options[k] }
|
228
|
+
elsif conn.kind_of?(Hash)
|
229
|
+
conn_info = conn.dup
|
230
|
+
end
|
231
|
+
|
232
|
+
return conn_info
|
233
|
+
end
|
234
|
+
|
235
|
+
def create_pattern_match_regex(str)
|
236
|
+
ss = StringScanner.new(str)
|
237
|
+
buf = []
|
238
|
+
|
239
|
+
until ss.eos?
|
240
|
+
if (tok = ss.scan(/[^\\%_]+/))
|
241
|
+
buf << Regexp.escape(tok)
|
242
|
+
elsif (tok = ss.scan(/\\/))
|
243
|
+
buf << Regexp.escape(ss.getch)
|
244
|
+
elsif (tok = ss.scan(/%/))
|
245
|
+
buf << '.*'
|
246
|
+
elsif (tok = ss.scan(/_/))
|
247
|
+
buf << '.'
|
248
|
+
else
|
249
|
+
raise 'must not happen'
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
/\A#{buf.join}\z/
|
254
|
+
end
|
255
|
+
|
256
|
+
def build_error_message(e, sql, conn)
|
257
|
+
msgs = {cause: "#{e.message} [#{e.class}]"}
|
258
|
+
msgs[:sql] = sql if sql
|
259
|
+
|
260
|
+
if conn
|
261
|
+
conn_info = connection_info(conn)
|
262
|
+
msgs[:connection] = [:host, :database, :username].map {|k| "#{k}=#{conn_info[k]}" }.join(";")
|
263
|
+
end
|
264
|
+
|
265
|
+
msgs.map {|k, v| "#{k}: #{v}" }.join(", ")
|
266
|
+
end
|
267
|
+
end # end of class methods
|
268
|
+
end
|