rails-pg-adapter 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 648d36ac2c7b9990f850dc54483824206aba63db49dc17d9379c49ce5ec88e1d
4
- data.tar.gz: bd0dd4cfd41cc749eb797d95da5d1f8261c586f04da5608068220694cbfdbbc6
3
+ metadata.gz: 432740419d2edda4a33caf34ba5efa2af778456763db8864c632b3d813f1953e
4
+ data.tar.gz: 60a026760c20847ace00bf181878c0039eface04fa30ff51227e677be88f95c0
5
5
  SHA512:
6
- metadata.gz: 41c0930afcf109ae0402e36ade421227f3b245358a06ccb79a46bf5da0c5749623b996d401fcf60fe6b00d1ad79a3ed72ffdfc21aaa30f9a5b994536884aef2a
7
- data.tar.gz: c96c0592c584ce6c3555bf87a880dcea495830f2b3deb1fcf03bc87f036a5b3b46afe00a8395c8b39757b0f7db159975562cd586cbe60c8ec02635103d4e03c4
6
+ metadata.gz: 5fb1aac2ec7330a599cc9005d339fab13613b409cf37152a003826b760c02b8b4060b0134d50799be4db7d77e6db4165b6ef99d0b1df0399133749f4746c5a25
7
+ data.tar.gz: 0450d73b3e3330da51c01d64a922828da6b32fe1d500d2c719ee40bd5dbbc2ac2c40d87cc93bc3a8a53676c3476f61180684a6bf05cf301d876f393eab6b5bee
data/CHANGELOG.md CHANGED
@@ -1,4 +1,8 @@
1
- ## [0.1.3] - 2023-04-04
1
+ ## [0.1.5] - 2023-04-19
2
+
3
+ - Retry queries when not in transaction
4
+
5
+ ## [0.1.4] - 2023-04-04
2
6
 
3
7
  - Rescue and recover from `ActiveRecord::ConnectionNotEstablished`
4
8
 
data/CODE_OF_CONDUCT.md CHANGED
@@ -10,21 +10,21 @@ We pledge to act and interact in ways that contribute to an open, welcoming, div
10
10
 
11
11
  Examples of behavior that contributes to a positive environment for our community include:
12
12
 
13
- * Demonstrating empathy and kindness toward other people
14
- * Being respectful of differing opinions, viewpoints, and experiences
15
- * Giving and gracefully accepting constructive feedback
16
- * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
- * Focusing on what is best not just for us as individuals, but for the overall community
13
+ - Demonstrating empathy and kindness toward other people
14
+ - Being respectful of differing opinions, viewpoints, and experiences
15
+ - Giving and gracefully accepting constructive feedback
16
+ - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ - Focusing on what is best not just for us as individuals, but for the overall community
18
18
 
19
19
  Examples of unacceptable behavior include:
20
20
 
21
- * The use of sexualized language or imagery, and sexual attention or
21
+ - The use of sexualized language or imagery, and sexual attention or
22
22
  advances of any kind
23
- * Trolling, insulting or derogatory comments, and personal or political attacks
24
- * Public or private harassment
25
- * Publishing others' private information, such as a physical or email
23
+ - Trolling, insulting or derogatory comments, and personal or political attacks
24
+ - Public or private harassment
25
+ - Publishing others' private information, such as a physical or email
26
26
  address, without their explicit permission
27
- * Other conduct which could reasonably be considered inappropriate in a
27
+ - Other conduct which could reasonably be considered inappropriate in a
28
28
  professional setting
29
29
 
30
30
  ## Enforcement Responsibilities
@@ -67,7 +67,7 @@ Community leaders will follow these Community Impact Guidelines in determining t
67
67
 
68
68
  ### 4. Permanent Ban
69
69
 
70
- **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
71
 
72
72
  **Consequence**: A permanent ban from any sort of public interaction within the community.
73
73
 
data/README.md CHANGED
@@ -2,16 +2,6 @@
2
2
 
3
3
  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.
4
4
 
5
- ## How does it work
6
-
7
- During a database failover in production, the `ActiveRecord` connection pool can become exhausted as queries are made against the database during the failover process. This can leave the `ActiveRecord` connection pools with stale or bad connections, even after the database has successfully recovered. Recovering from this issue usually requires a rolling restart of the application processes or containers.
8
-
9
- `RailsPgAdapter` addresses this problem by resetting the connection pool and re-raises the original exception from an `ActiveRecord` monkey patch. This allows the application to auto-heal from stale connections on its own (after database recovery) when performing queries for a new request, without requiring manual intervention.
10
-
11
- Another issue with `ActiveRecord` queries is `PG::UndefinedColumn`, which occurs when an `ActiveRecord` model includes a `SELECT` query with the name of a column that has been dropped from a Rails migration. This can happen even if the column isn't being referenced anywhere in the code. It occurs when a model is using `ignored_columns`, which prompts `ActiveRecord` to perform a dedicated lookup of the allowed columns in a select, such as `SELECT "users".name, "users".template_id...."`, instead of `SELECT "users".*`. When a column like `template_id` is dropped, PostgreSQL throws an undefined column error, which is bubbled up by `ActiveRecord` into `PG::UndefinedColumn`. Recovering from this issue also usually requires a rolling restart of the application processes or containers.
12
-
13
- `RailsPgAdapter` solves this second issue by resetting the `ActiveRecord` schema cache and memoized model column information when it detects a `PG::UndefinedColumn` raised from a monkey patch. Resetting the column information forces `ActiveRecord` to refresh its schema cache by loading the table information from the database and no longer reference the dropped column for new queries, without requiring manual intervention.
14
-
15
5
  ## Installation
16
6
 
17
7
  Install the gem and add to the application's Gemfile by executing:
@@ -27,7 +17,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
27
17
  ### Auto healing connections when PostgreSQL database fails over
28
18
 
29
19
  ```ruby
30
- # config/initializer/rails_pg_adapter.rb
20
+ # config/initializers/rails_pg_adapter.rb
31
21
 
32
22
  RailsPgAdapter.configure do |c|
33
23
  c.add_failover_patch = true
@@ -36,10 +26,28 @@ end
36
26
 
37
27
  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.
38
28
 
29
+ ### Retrying queries
30
+
31
+ When the database is failing you can retry queries that are not in a transaction. The gem will perform a back off retry in establishing the connection.
32
+ Once the back off is reached and no connection is found, it will bubble up the exception. Otherwise, the query will be retried with a new connection.
33
+
34
+ It is an opt-in functionality. You can supply your own back off figures for retries (in seconds) as following:
35
+
36
+ ```ruby
37
+ # config/initializers/rails_pg_adapter.rb
38
+
39
+ RailsPgAdapter.configure do |c|
40
+ c.add_failover_patch = true
41
+ c.reconnect_with_backoff = [0.5, 1, 2, 4, 8, 16] # seconds
42
+ ...
43
+ end
44
+
45
+ ```
46
+
39
47
  ### Refresh model column information on the fly after an existing column is dropped
40
48
 
41
49
  ```ruby
42
- # config/initializer/rails_pg_adapter.rb
50
+ # config/initializers/rails_pg_adapter.rb
43
51
 
44
52
  RailsPgAdapter.configure do |c|
45
53
  c.add_reset_column_information_patch = true
@@ -48,6 +56,16 @@ end
48
56
 
49
57
  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.
50
58
 
59
+ ## How does it work
60
+
61
+ During a database failover in production, the `ActiveRecord` connection pool can become exhausted as queries are made against the database during the failover process. This can leave the `ActiveRecord` connection pools with stale or bad connections, even after the database has successfully recovered. Recovering from this issue usually requires a rolling restart of the application processes or containers.
62
+
63
+ `RailsPgAdapter` addresses this problem by resetting the connection pool and re-raises the original exception from an `ActiveRecord` monkey patch. This allows the application to auto-heal from stale connections on its own (after database recovery) when performing queries for a new request, without requiring manual intervention.
64
+
65
+ Another issue with `ActiveRecord` queries is `PG::UndefinedColumn`, which occurs when an `ActiveRecord` model includes a `SELECT` query with the name of a column that has been dropped from a Rails migration. This can happen even if the column isn't being referenced anywhere in the code. It occurs when a model is using `ignored_columns`, which prompts `ActiveRecord` to perform a dedicated lookup of the allowed columns in a select, such as `SELECT "users".name, "users".template_id...."`, instead of `SELECT "users".*`. When a column like `template_id` is dropped, PostgreSQL throws an undefined column error, which is bubbled up by `ActiveRecord` into `PG::UndefinedColumn`. Recovering from this issue also usually requires a rolling restart of the application processes or containers.
66
+
67
+ `RailsPgAdapter` solves this second issue by resetting the `ActiveRecord` schema cache and memoized model column information when it detects a `PG::UndefinedColumn` raised from a monkey patch. Resetting the column information forces `ActiveRecord` to refresh its schema cache by loading the table information from the database and no longer reference the dropped column for new queries, without requiring manual intervention.
68
+
51
69
  ## Development
52
70
 
53
71
  - Install ruby 3.0
@@ -2,11 +2,12 @@
2
2
 
3
3
  module RailsPgAdapter
4
4
  class Configuration
5
- attr_accessor :add_failover_patch, :add_reset_column_information_patch
5
+ attr_accessor :add_failover_patch, :add_reset_column_information_patch, :reconnect_with_backoff
6
6
 
7
7
  def initialize(attrs)
8
8
  self.add_failover_patch = attrs[:add_failover_patch]
9
9
  self.add_reset_column_information_patch = attrs[:add_reset_column_information_patch]
10
+ self.reconnect_with_backoff = attrs[:reconnect_with_backoff]
10
11
  end
11
12
  end
12
13
 
@@ -14,6 +15,7 @@ module RailsPgAdapter
14
15
  @configuration ||= Configuration.new({
15
16
  add_failover_patch: false,
16
17
  add_reset_column_information_patch: false,
18
+ reconnect_with_backoff: [],
17
19
  })
18
20
  end
19
21
 
@@ -25,6 +27,10 @@ module RailsPgAdapter
25
27
  RailsPgAdapter.configuration.add_failover_patch || false
26
28
  end
27
29
 
30
+ def self.reconnect_with_backoff?
31
+ !RailsPgAdapter.configuration.reconnect_with_backoff.empty?
32
+ end
33
+
28
34
  def self.reset_column_information_patch?
29
35
  RailsPgAdapter.configuration.add_reset_column_information_patch || false
30
36
  end
@@ -33,6 +39,7 @@ module RailsPgAdapter
33
39
  @configuration = Configuration.new({
34
40
  add_failover_patch: false,
35
41
  add_reset_column_information_patch: false,
42
+ reconnect_with_backoff: [],
36
43
  })
37
44
  end
38
45
  end
@@ -12,6 +12,7 @@ module RailsPgAdapter
12
12
  "PG::ConnectionBad",
13
13
  "the database system is starting up",
14
14
  "connection is closed",
15
+ "could not connect",
15
16
  ].freeze
16
17
  CONNECTION_ERROR_RE = /#{CONNECTION_ERROR.map { |w| Regexp.escape(w) }.join("|")}/.freeze
17
18
 
@@ -23,20 +24,37 @@ module RailsPgAdapter
23
24
  def exec_cache(*args)
24
25
  super(*args)
25
26
  rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::ConnectionNotEstablished => e
26
- handle_error(e) || raise
27
+ raise unless supported_errors?(e)
28
+
29
+ try_reconnect?(e) ? retry : handle_error(e)
27
30
  end
28
31
 
29
32
  def exec_no_cache(*args)
30
33
  super(*args)
31
34
  rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::ConnectionNotEstablished => e
32
- handle_error(e) || raise
35
+ raise unless supported_errors?(e)
36
+
37
+ try_reconnect?(e) ? retry : handle_error(e)
38
+ end
39
+
40
+ def try_reconnect?(e)
41
+ return false if in_transaction?
42
+ return false unless failover_error?(e.message)
43
+ return false unless RailsPgAdapter.reconnect_with_backoff?
44
+
45
+ begin
46
+ reconnect!
47
+ true
48
+ rescue ::ActiveRecord::ConnectionNotEstablished
49
+ false
50
+ end
33
51
  end
34
52
 
35
53
  def handle_error(e)
36
54
  if failover_error?(e.message) && RailsPgAdapter.failover_patch?
37
55
  warn("clearing connections due to #{e} - #{e.message}")
38
56
  disconnect_and_remove_conn!
39
- raise
57
+ raise(e)
40
58
  end
41
59
 
42
60
  return unless missing_column_error?(e.message) && RailsPgAdapter.reset_column_information_patch?
@@ -70,7 +88,41 @@ module RailsPgAdapter
70
88
  return if Rails.logger.nil?
71
89
  ::Rails.logger.warn("[RailsPgAdapter::Patch] #{msg}")
72
90
  end
91
+
92
+ def supported_errors?(e)
93
+ return true if failover_error?(e.message) && RailsPgAdapter.failover_patch?
94
+ if missing_column_error?(e.message) && RailsPgAdapter.reset_column_information_patch?
95
+ return true
96
+ end
97
+ false
98
+ end
73
99
  end
74
100
  end
75
101
 
76
102
  ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(RailsPgAdapter::Patch)
103
+
104
+ # Override new client connection to bake in retries
105
+ module ActiveRecord
106
+ module ConnectionAdapters
107
+ class PostgreSQLAdapter
108
+ class << self
109
+ old_new_client_method = instance_method(:new_client)
110
+
111
+ define_method(:new_client) do |args|
112
+ sleep_times = RailsPgAdapter.configuration.reconnect_with_backoff.dup
113
+ begin
114
+ old_new_client_method.bind(self).call(args)
115
+ rescue ::ActiveRecord::ConnectionNotEstablished => e
116
+ raise(e) unless RailsPgAdapter.failover_patch? && RailsPgAdapter.reconnect_with_backoff?
117
+
118
+ sleep_time = sleep_times.shift
119
+ raise unless sleep_time
120
+ warn( "Could not establish a connection from new_client, retrying again in #{sleep_time} sec.")
121
+ sleep(sleep_time)
122
+ retry
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsPgAdapter
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.5"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-pg-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tines Engineering
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-14 00:00:00.000000000 Z
11
+ date: 2023-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails