rails-pg-adapter 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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