diff --git a/.gitignore b/.gitignore index 3223069..53be4a0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ Gemfile.lock # rspec failure tracking .rspec_status + +node_modules/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ce00843 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,19 @@ +{ + "trailingComma": "es5", + "overrides": [ + { + "files": [ + "Gemfile", + "*.{rb,ru,rake,gemspec}", + ".active_record_doctor", + ".irbrc", + ".pryrc" + ], + "options": { + "printWidth": 100, + "rubySingleQuote": false, + "rubyArrayLiteral": false + } + } + ] +} diff --git a/README.md b/README.md index cbe9232..d2a17f4 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,7 @@ If bundler is not being used to manage dependencies, install the gem by executin ```ruby # config/initializers/rails_pg_adapter.rb -RailsPgAdapter.configure do |c| - c.add_failover_patch = true -end +RailsPgAdapter.configure { |c| c.add_failover_patch = true } ``` This will add the monkey patch which resets the `ActiveRecord` connections in the connection pool when the database fails over. The patch will reset the connection and re-raise the error each time it detects that an exception related to a database failover is detected. @@ -49,9 +47,7 @@ end ```ruby # config/initializers/rails_pg_adapter.rb -RailsPgAdapter.configure do |c| - c.add_reset_column_information_patch = true -end +RailsPgAdapter.configure { |c| c.add_reset_column_information_patch = true } ``` This will clear the `ActiveRecord` schema cache and reset the `ActiveRecord` column information memoized on the model. The patch will reset the relevant information and re-raise the error each time it detects that an exception related to a dropped column is raised. diff --git a/Rakefile b/Rakefile index 23e0614..cca7175 100644 --- a/Rakefile +++ b/Rakefile @@ -9,4 +9,4 @@ require "rubocop/rake_task" RuboCop::RakeTask.new -task default: [:spec, :rubocop] +task default: %i[spec rubocop] diff --git a/lib/rails_pg_adapter/configuration.rb b/lib/rails_pg_adapter/configuration.rb index 6243bb2..3c46fee 100644 --- a/lib/rails_pg_adapter/configuration.rb +++ b/lib/rails_pg_adapter/configuration.rb @@ -12,11 +12,14 @@ def initialize(attrs) end def self.configuration - @configuration ||= Configuration.new({ - add_failover_patch: false, - add_reset_column_information_patch: false, - reconnect_with_backoff: [], - }) + @configuration ||= + Configuration.new( + { + add_failover_patch: false, + add_reset_column_information_patch: false, + reconnect_with_backoff: [], + }, + ) end def self.configure @@ -36,10 +39,13 @@ def self.reset_column_information_patch? end def self.reset_configuration - @configuration = Configuration.new({ - add_failover_patch: false, - add_reset_column_information_patch: false, - reconnect_with_backoff: [], - }) + @configuration = + Configuration.new( + { + add_failover_patch: false, + add_reset_column_information_patch: false, + reconnect_with_backoff: [], + }, + ) end end diff --git a/lib/rails_pg_adapter/patch.rb b/lib/rails_pg_adapter/patch.rb index d1a6538..37df347 100644 --- a/lib/rails_pg_adapter/patch.rb +++ b/lib/rails_pg_adapter/patch.rb @@ -17,7 +17,8 @@ module Patch CONNECTION_ERROR_RE = /#{CONNECTION_ERROR.map { |w| Regexp.escape(w) }.join("|")}/.freeze CONNECTION_SCHEMA_ERROR = ["PG::UndefinedColumn"].freeze - CONNECTION_SCHEMA_RE = /#{CONNECTION_SCHEMA_ERROR.map { |w| Regexp.escape(w) }.join("|")}/.freeze + CONNECTION_SCHEMA_RE = + /#{CONNECTION_SCHEMA_ERROR.map { |w| Regexp.escape(w) }.join("|")}/.freeze private @@ -57,7 +58,9 @@ def handle_error(e) raise(e) end - return unless missing_column_error?(e.message) && RailsPgAdapter.reset_column_information_patch? + unless missing_column_error?(e.message) && RailsPgAdapter.reset_column_information_patch? + return + end warn("clearing column information due to #{e} - #{e.message}") @@ -117,7 +120,9 @@ class << self sleep_time = sleep_times.shift raise unless sleep_time - warn( "Could not establish a connection from new_client, retrying again in #{sleep_time} sec.") + warn( + "Could not establish a connection from new_client, retrying again in #{sleep_time} sec.", + ) sleep(sleep_time) retry end diff --git a/package.json b/package.json new file mode 100644 index 0000000..ef9ed55 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "devDependencies": { + "@prettier/plugin-ruby": "^3.2.2", + "prettier": "^2.8.7" + }, + "scripts": { + "format": "prettier --write '{**/,}{Gemfile,*.{md,yml,css,scss,json,js,rake,rb,ru,gemspec,ts,tsx}}'" + }, + "name": "rails-pg-adapter", + "version": "1.0.0", + "main": "index.js", + "repository": "git@github.com:tines/rails-pg-adapter.git", + "license": "MIT" +} diff --git a/rails-pg-adapter.gemspec b/rails-pg-adapter.gemspec index f716e69..820c50f 100644 --- a/rails-pg-adapter.gemspec +++ b/rails-pg-adapter.gemspec @@ -9,7 +9,8 @@ Gem::Specification.new do |spec| spec.name = "rails-pg-adapter" spec.version = RailsPgAdapter::VERSION spec.summary = "Rails Postgres ActiveRecord patches for common production workloads" - spec.description = "This project allows you to monkey patch ActiveRecord (PostgreSQL) and auto-heal applications in production when PostgreSQL database fails over or when a cached column (in ActiveRecord schema cache) is removed from the database from a migration in another process." + spec.description = + "This project allows you to monkey patch ActiveRecord (PostgreSQL) and auto-heal applications in production when PostgreSQL database fails over or when a cached column (in ActiveRecord schema cache) is removed from the database from a migration in another process." spec.license = "MIT" spec.required_ruby_version = ">= 2.6.0" spec.authors = ["Tines Engineering"] @@ -21,11 +22,14 @@ Gem::Specification.new do |spec| # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - spec.files = Dir.chdir(__dir__) do - `git ls-files -z`.split("\x0").reject do |f| - (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) + spec.files = + Dir.chdir(__dir__) do + `git ls-files -z`.split("\x0") + .reject do |f| + (f == __FILE__) || + f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) + end end - end spec.require_paths = ["lib"] spec.add_dependency("rails", "~> 6") diff --git a/spec/rails_pg_adapter/configuration_spec.rb b/spec/rails_pg_adapter/configuration_spec.rb index b154552..c47d937 100644 --- a/spec/rails_pg_adapter/configuration_spec.rb +++ b/spec/rails_pg_adapter/configuration_spec.rb @@ -7,10 +7,8 @@ describe "new" do it "initializes with the passed attributes" do - c = described_class.new({ - add_failover_patch: true, - add_reset_column_information_patch: true, - }) + c = + described_class.new({ add_failover_patch: true, add_reset_column_information_patch: true }) expect(c.add_failover_patch).to be(true) expect(c.add_reset_column_information_patch).to be(true) @@ -44,9 +42,7 @@ end it "returns true" do - RailsPgAdapter.configure do |c| - c.add_failover_patch = true - end + RailsPgAdapter.configure { |c| c.add_failover_patch = true } expect(RailsPgAdapter.failover_patch?).to be(true) end end @@ -57,9 +53,7 @@ end it "returns true" do - RailsPgAdapter.configure do |c| - c.add_reset_column_information_patch = true - end + RailsPgAdapter.configure { |c| c.add_reset_column_information_patch = true } expect(RailsPgAdapter.reset_column_information_patch?).to be(true) end end diff --git a/spec/rails_pg_adapter/patch_spec.rb b/spec/rails_pg_adapter/patch_spec.rb index 35b8533..fd24b56 100644 --- a/spec/rails_pg_adapter/patch_spec.rb +++ b/spec/rails_pg_adapter/patch_spec.rb @@ -10,9 +10,11 @@ def initialize private - def exec_no_cache; end + def exec_no_cache + end - def disconnect!; end + def disconnect! + end def in_transaction? false @@ -26,8 +28,7 @@ def reconnect! EXCEPTION_MESSAGE = "PG::ReadOnlySqlTransaction: ERROR: cannot execute UPDATE in a read-only transaction" -COLUMN_EXCEPTION_MESSAGE = - "PG::UndefinedColumn: ERROR: column users.template_id does not exist" +COLUMN_EXCEPTION_MESSAGE = "PG::UndefinedColumn: ERROR: column users.template_id does not exist" RSpec.describe(RailsPgAdapter::Patch) do before do @@ -46,22 +47,18 @@ def reconnect! allow_any_instance_of(Object).to receive(:sleep) expect(ActiveRecord::Base.connection_pool).to receive(:remove) expect_any_instance_of(Dummy).to receive(:disconnect!) - expect { - Dummy.new.extend(RailsPgAdapter::Patch).send(:exec_cache) - }.to raise_error( + expect { Dummy.new.extend(RailsPgAdapter::Patch).send(:exec_cache) }.to raise_error( ActiveRecord::StatementInvalid, "PG::ReadOnlySqlTransaction: ERROR: cannot execute UPDATE in a read-only transaction", ) end it "does not call clear_all_connections when a general exception is raised" do - allow_any_instance_of(Dummy).to receive(:exec_cache).and_raise( + allow_any_instance_of(Dummy).to receive(:exec_cache).and_raise("Exception") + expect(ActiveRecord::Base).not_to receive(:clear_all_connections!) + expect { Dummy.new.extend(RailsPgAdapter::Patch).send(:exec_cache) }.to raise_error( "Exception", ) - expect(ActiveRecord::Base).not_to receive(:clear_all_connections!) - expect { - Dummy.new.extend(RailsPgAdapter::Patch).send(:exec_cache) - }.to raise_error("Exception") end it "clears schema cache when a PG::UndefinedColumn exception is raised" do @@ -69,13 +66,9 @@ def reconnect! ActiveRecord::StatementInvalid.new(COLUMN_EXCEPTION_MESSAGE), ) - expect(ActiveRecord::Base).to receive(:descendants).at_least( - :once, - ).and_call_original + expect(ActiveRecord::Base).to receive(:descendants).at_least(:once).and_call_original - expect { - Dummy.new.extend(RailsPgAdapter::Patch).send(:exec_cache) - }.to raise_error( + expect { Dummy.new.extend(RailsPgAdapter::Patch).send(:exec_cache) }.to raise_error( ActiveRecord::StatementInvalid, "PG::UndefinedColumn: ERROR: column users.template_id does not exist", ) @@ -92,9 +85,7 @@ def reconnect! expect(ActiveRecord::Base.connection_pool).to receive(:remove) expect_any_instance_of(Dummy).to receive(:disconnect!) - expect do - Dummy.new.extend(RailsPgAdapter::Patch).send(:exec_no_cache) - end.to raise_error( + expect do Dummy.new.extend(RailsPgAdapter::Patch).send(:exec_no_cache) end.to raise_error( ActiveRecord::StatementInvalid, "PG::ReadOnlySqlTransaction: ERROR: cannot execute UPDATE in a read-only transaction", ) @@ -110,9 +101,10 @@ def reconnect! expect(ActiveRecord::Base.connection_pool).to receive(:remove) expect_any_instance_of(Dummy).to receive(:disconnect!) - expect do - Dummy.new.extend(RailsPgAdapter::Patch).send(:exec_no_cache) - end.to raise_error(ActiveRecord::ConnectionNotEstablished, msg) + expect do Dummy.new.extend(RailsPgAdapter::Patch).send(:exec_no_cache) end.to raise_error( + ActiveRecord::ConnectionNotEstablished, + msg, + ) end it "calls clear_all_connections when a ActiveRecord::ConnectionNotEstablished retries once and fails" do @@ -123,9 +115,7 @@ def reconnect! end values = [proc { raise ActiveRecord::ConnectionNotEstablished }] # raise error once - allow_any_instance_of(Dummy).to receive( - :reconnect!, - ).and_wrap_original do |original, *args| + allow_any_instance_of(Dummy).to receive(:reconnect!).and_wrap_original do |original, *args| values.empty? ? original.call(*args) : values.shift.call end @@ -146,13 +136,11 @@ def reconnect! end it "does not call clear_all_connections when a general exception is raised" do - allow_any_instance_of(Dummy).to receive(:exec_no_cache).and_raise( + allow_any_instance_of(Dummy).to receive(:exec_no_cache).and_raise("Exception") + expect(ActiveRecord::Base).not_to receive(:clear_all_connections!) + expect do Dummy.new.extend(RailsPgAdapter::Patch).send(:exec_no_cache) end.to raise_error( "Exception", ) - expect(ActiveRecord::Base).not_to receive(:clear_all_connections!) - expect do - Dummy.new.extend(RailsPgAdapter::Patch).send(:exec_no_cache) - end.to raise_error("Exception") end it "clears schema cache when a PG::UndefinedColumn exception is raised" do @@ -160,13 +148,9 @@ def reconnect! ActiveRecord::StatementInvalid.new(COLUMN_EXCEPTION_MESSAGE), ) - expect(ActiveRecord::Base).to receive(:descendants).at_least( - :once, - ).and_call_original + expect(ActiveRecord::Base).to receive(:descendants).at_least(:once).and_call_original - expect do - Dummy.new.extend(RailsPgAdapter::Patch).send(:exec_no_cache) - end.to raise_error( + expect do Dummy.new.extend(RailsPgAdapter::Patch).send(:exec_no_cache) end.to raise_error( ActiveRecord::StatementInvalid, "PG::UndefinedColumn: ERROR: column users.template_id does not exist", ) @@ -181,9 +165,9 @@ def reconnect! c.reconnect_with_backoff = [0.5] end - expect(PG).to receive(:connect).and_raise( - ActiveRecord::ConnectionNotEstablished, - ).at_most(:twice) + expect(PG).to receive(:connect).and_raise(ActiveRecord::ConnectionNotEstablished).at_most( + :twice, + ) expect(Object).to receive(:sleep).at_most(:once) expect do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6142074..4ab5703 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -11,9 +11,7 @@ # Disable RSpec exposing methods globally on `Module` and `main` config.disable_monkey_patching! - config.expect_with(:rspec) do |c| - c.syntax = :expect - end + config.expect_with(:rspec) { |c| c.syntax = :expect } config.before(:suite) do ActiveRecord::Base.establish_connection( diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..da95f05 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,15 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@prettier/plugin-ruby@^3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@prettier/plugin-ruby/-/plugin-ruby-3.2.2.tgz#43c9d85349032f74d34c4f57e6a77487d5c5bdc1" + integrity sha512-Vc7jVE39Fgswl517ET4kPtpnoRWE6XTi1Sivd84rZyomYnHYUmvUsEeoOf6tVhzTuIXE5XVQB1YCG2hulrwR3Q== + dependencies: + prettier ">=2.3.0" + +prettier@>=2.3.0, prettier@^2.8.7: + version "2.8.7" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.7.tgz#bb79fc8729308549d28fe3a98fce73d2c0656450" + integrity sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==