require 'mysql2' require 'logger' require 'active_record' require 'active_record/base' require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/abstract_mysql_adapter' require 'active_record/connection_adapters/mysql2_adapter' require 'active_record/connection_adapters/abstract/connection_pool' require 'active_support' require 'activerecord/mysql/reconnect/version' require 'activerecord/mysql/reconnect/base_ext' # XXX: #require 'activerecord/mysql/reconnect/abstract_adapter_ext' require 'activerecord/mysql/reconnect/abstract_mysql_adapter_ext' require 'activerecord/mysql/reconnect/mysql2_adapter_ext' require 'activerecord/mysql/reconnect/connection_pool_ext' module Activerecord::Mysql::Reconnect DEFAULT_EXECUTION_TRIES = 3 DEFAULT_EXECUTION_RETRY_WAIT = 0.5 WITHOUT_RETRY_KEY = 'activerecord-mysql-reconnect-without-retry' RETRYABLE_TRANSACTION_KEY = 'activerecord-mysql-reconnect-transaction-retry' HANDLE_ERROR = [ ActiveRecord::StatementInvalid, Mysql2::Error, ] HANDLE_R_ERROR_MESSAGES = [ 'Lost connection to MySQL server during query', ] HANDLE_RW_ERROR_MESSAGES = [ 'MySQL server has gone away', 'Server shutdown in progress', 'closed MySQL connection', "Can't connect to MySQL server", 'Query execution was interrupted', 'Access denied for user', 'The MySQL server is running with the --read-only option', ] HANDLE_ERROR_MESSAGES = HANDLE_R_ERROR_MESSAGES + HANDLE_RW_ERROR_MESSAGES READ_SQL_REGEXP = /\A\s*(?:SELECT|SHOW|SET)\b/i RETRY_MODES = [:r, :rw, :force] DEFAULT_RETRY_MODE = :r class << self def execution_tries ActiveRecord::Base.execution_tries || DEFAULT_EXECUTION_TRIES end def execution_retry_wait ActiveRecord::Base.execution_retry_wait || DEFAULT_EXECUTION_RETRY_WAIT end def enable_retry !!ActiveRecord::Base.enable_retry end def retry_mode=(v) unless RETRY_MODES.include?(v) raise "Invalid retry_mode. Please set one of the following: #{RETRY_MODES.map {|i| i.inspect }.join(', ')}" end @activerecord_mysql_reconnect_retry_mode = v end def retry_mode @activerecord_mysql_reconnect_retry_mode || DEFAULT_RETRY_MODE end def retryable(opts) block = opts.fetch(:proc) on_error = opts[:on_error] conn = opts[:connection] tries = self.execution_tries retval = nil retryable_loop(tries) do |n| begin retval = block.call break rescue => e if enable_retry and (tries.zero? or n < tries) and should_handle?(e, opts) on_error.call if on_error wait = self.execution_retry_wait * n opt_msgs = ["cause: #{e} [#{e.class}]"] if conn and conn.kind_of?(Mysql2::Client) opt_msgs << 'connection: ' + [:host, :database, :username].map {|k| "#{k}=#{conn.query_options[k]}" }.join(";") end logger.warn("MySQL server has gone away. Trying to reconnect in #{wait} seconds. (#{opt_msgs.join(', ')})") sleep(wait) next else raise e end end end return retval end def logger if defined?(Rails) Rails.logger || ActiveRecord::Base.logger || Logger.new($stderr) else ActiveRecord::Base.logger || Logger.new($stderr) end end def without_retry begin Thread.current[WITHOUT_RETRY_KEY] = true yield ensure Thread.current[WITHOUT_RETRY_KEY] = nil end end def without_retry? !!Thread.current[WITHOUT_RETRY_KEY] end def retryable_transaction begin Thread.current[RETRYABLE_TRANSACTION_KEY] = [] ActiveRecord::Base.transaction do yield end ensure Thread.current[RETRYABLE_TRANSACTION_KEY] = nil end end def retryable_transaction_buffer Thread.current[RETRYABLE_TRANSACTION_KEY] end private def retryable_loop(n) if n.zero? loop { n += 1 ; yield(n) } else n.times {|i| yield(i + 1) } end end def should_handle?(e, opts = {}) sql = opts[:sql] retry_mode = opts[:retry_mode] if without_retry? return false end unless HANDLE_ERROR.any? {|i| e.kind_of?(i) } return false end unless Regexp.union(HANDLE_ERROR_MESSAGES) =~ e.message return false end if sql and READ_SQL_REGEXP !~ sql if retry_mode == :r return false end if retry_mode != :force and Regexp.union(HANDLE_R_ERROR_MESSAGES) =~ e.message return false end end return true end end # end of class methods end