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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.idea/.gitignore +8 -0
  4. data/.idea/activerecord-mysql-reconnect.iml +22 -0
  5. data/.idea/misc.xml +4 -0
  6. data/.idea/modules.xml +8 -0
  7. data/.idea/vcs.xml +6 -0
  8. data/.rspec +3 -0
  9. data/.travis.yml +45 -0
  10. data/Appraisals +28 -0
  11. data/ChangeLog +35 -0
  12. data/Gemfile +4 -0
  13. data/LICENSE.txt +22 -0
  14. data/README.md +124 -0
  15. data/Rakefile +5 -0
  16. data/activerecord-mysql-reconnect-new.gemspec +28 -0
  17. data/docker-compose.yml +6 -0
  18. data/gemfiles/activerecord_4.2.gemfile +8 -0
  19. data/gemfiles/activerecord_5.0.gemfile +7 -0
  20. data/gemfiles/activerecord_5.1.gemfile +7 -0
  21. data/gemfiles/activerecord_5.2.gemfile +7 -0
  22. data/gemfiles/activerecord_6.0.gemfile +7 -0
  23. data/gemfiles/activerecord_6.1.gemfile +7 -0
  24. data/gemfiles/activerecord_master.gemfile +7 -0
  25. data/lib/activerecord/mysql/reconnect/abstract_mysql_adapter_ext.rb +46 -0
  26. data/lib/activerecord/mysql/reconnect/base_ext.rb +32 -0
  27. data/lib/activerecord/mysql/reconnect/connection_pool_ext.rb +18 -0
  28. data/lib/activerecord/mysql/reconnect/mysql2_adapter_ext.rb +13 -0
  29. data/lib/activerecord/mysql/reconnect/null_transaction_ext.rb +9 -0
  30. data/lib/activerecord/mysql/reconnect/version.rb +7 -0
  31. data/lib/activerecord/mysql/reconnect.rb +268 -0
  32. data/lib/activerecord-mysql-reconnect.rb +5 -0
  33. data/spec/activerecord-mysql-reconnect_spec.rb +545 -0
  34. data/spec/data.sql +1001 -0
  35. data/spec/employee_model.rb +1 -0
  36. data/spec/mysql2_ext.rb +5 -0
  37. data/spec/mysql_helper.rb +84 -0
  38. data/spec/spec_helper.rb +54 -0
  39. 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
@@ -0,0 +1,5 @@
1
+ require 'active_support'
2
+
3
+ ActiveSupport.on_load :active_record do
4
+ require_relative 'activerecord/mysql/reconnect'
5
+ end