diff --git a/.github/workflows/specs.yml b/.github/workflows/specs.yml new file mode 100644 index 0000000..fca85d2 --- /dev/null +++ b/.github/workflows/specs.yml @@ -0,0 +1,21 @@ +name: Specs +on: + push: + branches: [main] + pull_request: + branches: [main] +jobs: + run-rspec: + runs-on: ubuntu-22.04 + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Setup Ruby and install gems + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.1 + bundler-cache: true + - name: Install gems with Appraisal + run: bundle exec appraisal install + - name: Run RSpec + run: bundle exec appraisal rspec diff --git a/Appraisals b/Appraisals index c8f84b7..ac2bda5 100644 --- a/Appraisals +++ b/Appraisals @@ -1,24 +1,3 @@ -appraise "activerecord-4.2" do - gem "activerecord", "~> 4.2.0" - gem "mysql2", "< 0.5" -end - -appraise "activerecord-5.0" do - gem "activerecord", "~> 5.0.0" -end - -appraise "activerecord-5.1" do - gem "activerecord", "~> 5.1.0" -end - -appraise "activerecord-5.2" do - gem "activerecord", "~> 5.2.0" -end - -appraise "activerecord-6.0" do - gem "activerecord", "~> 6.0.2" -end - -appraise "activerecord-master" do - gem "activerecord", git: "https://github.com/rails/rails.git" +appraise "activerecord-7.0" do + gem "activerecord", "~> 7.0.0" end diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1eae719 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,52 @@ +## 1.0.0 (unreleased) + +- Feature: Add support for Rails 7 +- Change: Drop support for older versions +- Chore: Test against MySQL 8 +- Fix: Handle ActiveRecord::ConnectionNotEstablished errors too +- Fix: Do not retry on ActiveRecord::StatementTimeout errors + +## 0.5.0 (Jan 29, 2020) + +- Change error message for Rails 6.0 +- Load database config from db_config instead +- Add Active Record 6.0 and master to build matrix + +## 0.4.2 (Aug 15, 2018) + +- Add activerecord-5.2 test +- Add error messages accessor +- Fix mysql2 dependency +- Added "Connection was killed" as an error string to reconnect on. + +## 0.4.1 (Aug 8, 2016) + +- Support AR 5.0 (RP#5 @ssig33) +- Fix test (use docker-compose) + +## 0.4.0 (Mar 22, 2016) + +- Remove `retryable_transaction` +- Disable support AR 3.x 4.0 + +## 0.3.3 (Jan 18, 2014) + +- use BigDecimal for sleep +- add handling error ('The MySQL server is running with the --read-only...') + +## 0.3.1 (Jan 9, 2014) + +- Retry mode is added + +## 0.3.0 (Jan 9, 2014) + +- Retry is disabled by default +- Read-only mode is added + +## 0.2.0 (Jan 4, 2014) + +- Retry transaction is supported + +## 0.1.0 (Oct 11, 2013) + +- activerecord-mysql-reconnect is released diff --git a/ChangeLog b/ChangeLog deleted file mode 100644 index cbb8d89..0000000 --- a/ChangeLog +++ /dev/null @@ -1,31 +0,0 @@ -activerecord-mysql-reconnect 0.4.1 (Aug 8, 2016) - - * Support AR 5.0 (RP#5 @ssig33) - * Fix test (use docker-compose) - -activerecord-mysql-reconnect 0.4.0 (Mar 22, 2016) - - * Remove `retryable_transaction` - * Disable support AR 3.x 4.0 - -activerecord-mysql-reconnect 0.3.3 (Jan 18, 2014) - - * use BigDecimal for sleep - * add handling error ('The MySQL server is running with the --read-only...') - -activerecord-mysql-reconnect 0.3.1 (Jan 9, 2014) - - * Retry mode is added - -activerecord-mysql-reconnect 0.3.0 (Jan 9, 2014) - - * Retry is disabled by default - * Read-only mode is added - -activerecord-mysql-reconnect 0.2.0 (Jan 4, 2014) - - * Retry transaction is supported - -activerecord-mysql-reconnect 0.1.0 (Oct 11, 2013) - - * activerecord-mysql-reconnect is released diff --git a/README.md b/README.md index 3b141ee..d978dd2 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # activerecord-mysql-reconnect -It is the library to reconnect automatically when ActiveRecord is disconnected from MySQL. +Reconnect automatically when ActiveRecord is disconnected from MySQL. Supports Rails 7+ and MySQL 8. -[![Gem Version](https://badge.fury.io/rb/activerecord-mysql-reconnect.svg)](http://badge.fury.io/rb/activerecord-mysql-reconnect) -[![Build Status](https://travis-ci.org/winebarrel/activerecord-mysql-reconnect.svg?branch=master)](https://travis-ci.org/winebarrel/activerecord-mysql-reconnect) +[![Specs](https://github.com/planningcenter/activerecord-mysql-reconnect/actions/workflows/specs.yml/badge.svg)](https://github.com/planningcenter/activerecord-mysql-reconnect/actions/workflows/specs.yml) ## Installation diff --git a/activerecord-mysql-reconnect.gemspec b/activerecord-mysql-reconnect.gemspec index 49769e6..e30ee46 100644 --- a/activerecord-mysql-reconnect.gemspec +++ b/activerecord-mysql-reconnect.gemspec @@ -18,8 +18,10 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ['lib'] - # '~> 4.2.6' - spec.add_dependency 'activerecord' + # We are going to stop using this in Rails 7.1 and beyond in favor of newly + # added reconnect / retry functionality. The Platform team will provide + # instructions for replacing this gem once 7.1 is released. + spec.add_dependency 'activerecord', '~> 7.0.0' spec.add_dependency 'mysql2' spec.add_development_dependency 'bundler' spec.add_development_dependency 'rake' diff --git a/docker-compose.yml b/docker-compose.yml index 0b89c0c..cc42b19 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ -mysql_for_ar_mysql_reconn: - image: "mysql:5.6" - ports: - - "14407:3306" - environment: - MYSQL_ALLOW_EMPTY_PASSWORD: "yes" +services: + mysql: + image: "mysql:8" + ports: + - "14407:3306" + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" diff --git a/gemfiles/activerecord_4.2.gemfile b/gemfiles/activerecord_4.2.gemfile deleted file mode 100644 index 193449b..0000000 --- a/gemfiles/activerecord_4.2.gemfile +++ /dev/null @@ -1,8 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "activerecord", "~> 4.2.0" -gem "mysql2", "< 0.5" - -gemspec path: "../" diff --git a/gemfiles/activerecord_5.0.gemfile b/gemfiles/activerecord_5.0.gemfile deleted file mode 100644 index a7a9bb0..0000000 --- a/gemfiles/activerecord_5.0.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "activerecord", "~> 5.0.0" - -gemspec path: "../" diff --git a/gemfiles/activerecord_5.1.gemfile b/gemfiles/activerecord_5.1.gemfile deleted file mode 100644 index e2f8f85..0000000 --- a/gemfiles/activerecord_5.1.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "activerecord", "~> 5.1.0" - -gemspec path: "../" diff --git a/gemfiles/activerecord_6.0.gemfile b/gemfiles/activerecord_6.0.gemfile deleted file mode 100644 index fe660bc..0000000 --- a/gemfiles/activerecord_6.0.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "activerecord", "~> 6.0.2" - -gemspec path: "../" diff --git a/gemfiles/activerecord_5.2.gemfile b/gemfiles/activerecord_7.0.gemfile similarity index 74% rename from gemfiles/activerecord_5.2.gemfile rename to gemfiles/activerecord_7.0.gemfile index 027888d..bc1dfc9 100644 --- a/gemfiles/activerecord_5.2.gemfile +++ b/gemfiles/activerecord_7.0.gemfile @@ -2,6 +2,6 @@ source "https://rubygems.org" -gem "activerecord", "~> 5.2.0" +gem "activerecord", "~> 7.0.0" gemspec path: "../" diff --git a/gemfiles/activerecord_master.gemfile b/gemfiles/activerecord_master.gemfile deleted file mode 100644 index 8980228..0000000 --- a/gemfiles/activerecord_master.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "activerecord", git: "https://github.com/rails/rails.git" - -gemspec path: "../" diff --git a/lib/activerecord/mysql/reconnect.rb b/lib/activerecord/mysql/reconnect.rb index 2a3322f..eee1769 100644 --- a/lib/activerecord/mysql/reconnect.rb +++ b/lib/activerecord/mysql/reconnect.rb @@ -26,10 +26,15 @@ module Activerecord::Mysql::Reconnect HANDLE_ERROR = [ ActiveRecord::ConnectionNotEstablished, + ActiveRecord::DatabaseConnectionError, ActiveRecord::StatementInvalid, Mysql2::Error, ] + DO_NOT_HANDLE_ERROR = [ + ActiveRecord::StatementTimeout + ] + @@handle_r_error_messages = { lost_connection: 'Lost connection to MySQL server during query', } @@ -47,6 +52,7 @@ module Activerecord::Mysql::Reconnect lost_connection: "Lost connection to MySQL server at 'reading initial communication packet'", not_connected: "MySQL client is not connected", killed: 'Connection was killed', + issue_connecting: 'There is an issue connecting with your hostname', } READ_SQL_REGEXP = /\A\s*(?:SELECT|SHOW|SET)\b/i @@ -203,6 +209,10 @@ def should_handle?(e, opts = {}) return false end + if DO_NOT_HANDLE_ERROR.any? { |i| e.kind_of?(i) } + return false + end + unless Regexp.union(@@handle_r_error_messages.values + @@handle_rw_error_messages.values) =~ e.message return false end diff --git a/lib/activerecord/mysql/reconnect/abstract_mysql_adapter_ext.rb b/lib/activerecord/mysql/reconnect/abstract_mysql_adapter_ext.rb index cc190ac..0f39fd1 100644 --- a/lib/activerecord/mysql/reconnect/abstract_mysql_adapter_ext.rb +++ b/lib/activerecord/mysql/reconnect/abstract_mysql_adapter_ext.rb @@ -1,10 +1,10 @@ module Activerecord::Mysql::Reconnect::ExecuteWithReconnect - def execute(sql, name = nil) + def raw_execute(sql, name, async: false) retryable(sql, name) do |sql_names| retval = nil sql_names.each do |s, n| - retval = super(s, n) + retval = super(s, n, async:) end retval diff --git a/lib/activerecord/mysql/reconnect/version.rb b/lib/activerecord/mysql/reconnect/version.rb index 697f6ae..d451b1a 100644 --- a/lib/activerecord/mysql/reconnect/version.rb +++ b/lib/activerecord/mysql/reconnect/version.rb @@ -1,7 +1,7 @@ module Activerecord module Mysql module Reconnect - VERSION = '0.5.0' + VERSION = '1.0.0' end end end diff --git a/spec/activerecord-mysql-reconnect_spec.rb b/spec/activerecord-mysql-reconnect_spec.rb index 9087c51..50d4ad0 100644 --- a/spec/activerecord-mysql-reconnect_spec.rb +++ b/spec/activerecord-mysql-reconnect_spec.rb @@ -49,7 +49,7 @@ end end - context 'when count on same thead' do + context 'when count on same thread' do specify do expect(Employee.count).to eq 1000 MysqlServer.restart @@ -57,10 +57,10 @@ end end - context 'wehn select on other thread' do + context 'when select on other thread' do specify do th = thread_start { - expect(Employee.where(:id => 1).pluck('sleep(10) * 0 + 3')).to eq [3] + expect(Employee.where(:id => 1).pluck(Arel.sql('sleep(10) * 0 + 3'))).to eq [3] } MysqlServer.restart @@ -113,14 +113,8 @@ specify do th = thread_start { - emp = Employee.create( - :emp_no => 9999, - :birth_date => Time.now, - # wait 10 sec - :first_name => "' + sleep(10) + '", - :last_name => 'Tiger', - :hire_date => Time.now - ) + id = ActiveRecord::Base.connection.insert("insert into employees (emp_no, birth_date, hire_date, first_name, last_name) values (sleep(10) + 9998, '2000-01-01', '2023-08-16', 'Daniel', 'Tiger')", returning: 'id') + emp = Employee.find(id) expect(emp.id).to eq 1001 expect(emp.emp_no).to eq 9999 @@ -214,6 +208,9 @@ MysqlServer.restart + # record is lost + expect { emp.reload }.to raise_error(ActiveRecord::RecordNotFound) + emp = Employee.create( :emp_no => 9998, :birth_date => Time.now, @@ -223,7 +220,8 @@ ) # NOTE: Ignore the transaction on :rw mode - expect(emp.id).to eq 1001 + + expect(emp.id).to eq 1002 # auto_increment still goes up expect(emp.emp_no).to eq 9998 end @@ -451,39 +449,29 @@ "%s (cause: %s, sql: SELECT `employees`.* FROM `employees`, connection: host=127.0.0.1;database=employees;username=root)" end - let(:mysql_error) do - Mysql2::Error.const_defined?(:ConnectionError) ? Mysql2::Error::ConnectionError : Mysql2::Error - end - before do - allow_any_instance_of(mysql_error).to receive(:message).and_return('Lost connection to MySQL server during query') + allow_any_instance_of(Mysql2::Error::ConnectionError).to receive(:message).and_return('Lost connection to MySQL server during query') + allow_any_instance_of(ActiveRecord::StatementInvalid).to receive(:message).and_return('Lost connection to MySQL server during query') + allow_any_instance_of(ActiveRecord::DatabaseConnectionError).to receive(:message).and_return('Lost connection to MySQL server during query') end context "when retry failed " do specify do - if ActiveRecord::VERSION::MAJOR < 6 - expect(ActiveRecord::Base.logger).to receive(:warn).with(warning_template % [ - "MySQL server has gone away. Trying to reconnect in 0.5 seconds.", - "#{mysql_error}: Lost connection to MySQL server during query: SELECT `employees`.* FROM `employees` [ActiveRecord::StatementInvalid]", - ]) - else - expect(ActiveRecord::Base.logger).to receive(:warn).with(warning_template % [ - "MySQL server has gone away. Trying to reconnect in 0.5 seconds.", - "#{mysql_error}: Lost connection to MySQL server during query [ActiveRecord::StatementInvalid]", - ]) - end - + expect(ActiveRecord::Base.logger).to receive(:warn).with(warning_template % [ + "MySQL server has gone away. Trying to reconnect in 0.5 seconds.", + "Lost connection to MySQL server during query [ActiveRecord::StatementInvalid]", + ]) (1.0..4.5).step(0.5).each do |sec| expect(ActiveRecord::Base.logger).to receive(:warn).with(warning_template % [ "MySQL server has gone away. Trying to reconnect in #{sec} seconds.", - "Lost connection to MySQL server during query [#{mysql_error}]", + "Lost connection to MySQL server during query [ActiveRecord::DatabaseConnectionError]", ]) end expect(ActiveRecord::Base.logger).to receive(:warn).with(warning_template % [ "Query retry failed.", - "Lost connection to MySQL server during query [#{mysql_error}]", + "Lost connection to MySQL server during query [ActiveRecord::DatabaseConnectionError]", ]) expect(Employee.all.length).to eq 1000 @@ -491,23 +479,16 @@ expect { Employee.all.length - }.to raise_error(mysql_error) + }.to raise_error(ActiveRecord::DatabaseConnectionError) end end context "when retry succeeded" do specify do - if ActiveRecord::VERSION::MAJOR < 6 - expect(ActiveRecord::Base.logger).to receive(:warn).with(warning_template % [ - "MySQL server has gone away. Trying to reconnect in 0.5 seconds.", - "#{mysql_error}: Lost connection to MySQL server during query: SELECT `employees`.* FROM `employees` [ActiveRecord::StatementInvalid]", - ]) - else - expect(ActiveRecord::Base.logger).to receive(:warn).with(warning_template % [ - "MySQL server has gone away. Trying to reconnect in 0.5 seconds.", - "#{mysql_error}: Lost connection to MySQL server during query [ActiveRecord::StatementInvalid]", - ]) - end + expect(ActiveRecord::Base.logger).to receive(:warn).with(warning_template % [ + "MySQL server has gone away. Trying to reconnect in 0.5 seconds.", + "Lost connection to MySQL server during query [ActiveRecord::StatementInvalid]", + ]) expect(Employee.all.length).to eq 1000 MysqlServer.restart @@ -516,6 +497,15 @@ end end + context "when statement execution time is exceeded" do + it "does not retry" do + expect(ActiveRecord::Base.logger).not_to receive(:warn) + expect { + Employee.connection.select_all("SELECT /*+ MAX_EXECUTION_TIME(1) */ * FROM employees where emp_no > sleep(1)") + }.to raise_error(ActiveRecord::StatementTimeout) + end + end + # NOTE: The following test need to execute at the last context "when the custom error is happened" do before do