Skip to content
This repository was archived by the owner on Nov 19, 2024. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ Gemfile.lock

# rspec failure tracking
.rspec_status

node_modules/
19 changes: 19 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -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
}
}
]
}
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ require "rubocop/rake_task"

RuboCop::RakeTask.new

task default: [:spec, :rubocop]
task default: %i[spec rubocop]
26 changes: 16 additions & 10 deletions lib/rails_pg_adapter/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
11 changes: 8 additions & 3 deletions lib/rails_pg_adapter/patch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}")

Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]:tines/rails-pg-adapter.git",
"license": "MIT"
}
14 changes: 9 additions & 5 deletions rails-pg-adapter.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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")
Expand Down
14 changes: 4 additions & 10 deletions spec/rails_pg_adapter/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
66 changes: 25 additions & 41 deletions spec/rails_pg_adapter/patch_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -46,36 +47,28 @@ 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
allow_any_instance_of(Dummy).to receive(:exec_cache).and_raise(
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",
)
Expand All @@ -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",
)
Expand All @@ -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
Expand All @@ -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

Expand All @@ -146,27 +136,21 @@ 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
allow_any_instance_of(Dummy).to receive(:exec_no_cache).and_raise(
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",
)
Expand All @@ -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
Expand Down
4 changes: 1 addition & 3 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
15 changes: 15 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -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==