active_record_proxy_adapters 0.7.2 → 0.8.0

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: 5942f8ab80752d3abe9ac1015a991cc8bb8cbe7faa3961824e617ec478fd1cf0
4
- data.tar.gz: daaf2fb00ad05b38c0477bb14d5a8f02d405863fd592252c63a887964385cc2f
3
+ metadata.gz: 8e87a47a033b2402f2fdd0dbae2aec360ff04afbe4639f83615030da82d80cfc
4
+ data.tar.gz: 7caec5f8d663d2b67482f28f30b669e36a00fedb8d9eea6b06404ad3df9ce594
5
5
  SHA512:
6
- metadata.gz: 2f61e7d36e3d968c9cf4f873f3cc7d70c5a1054337aa0b773ad59d76ad52b33088dc2c4588cf996725a7fa2efda42de77888654db6b3e53a93026a06dced44e4
7
- data.tar.gz: 022acae0581a7910927a18a7452750e9628b238cc590b05e3b3bf15c0b3d4f8b163fbcc7fcc30071f3673dc7d1c6cd33fa01c2bd1c88d0c1e5611de6f77c060f
6
+ metadata.gz: 65850028f544840668b1aa172a6442665cf9142c1a52934773f433c823fc56365b0251b6188636ccd252f9b33f74c9a40c9578d44953b57e33af842393e92e54
7
+ data.tar.gz: eb1c1b67ba524477b2b792d8a23f003f057cd764c4ee2930e9c5613efee9262ea7ba03c18084004b406b26e9c9e770a905733a2164ce6a84d107b172589860b0
@@ -3,8 +3,10 @@
3
3
  require "active_record_proxy_adapters/cache_configuration"
4
4
  require "active_record_proxy_adapters/context"
5
5
  require "active_record_proxy_adapters/database_configuration"
6
+ require "active_record_proxy_adapters/errors"
6
7
  require "active_record_proxy_adapters/synchronizable_configuration"
7
8
  require "active_support/core_ext/integer/time"
9
+ require "logger"
8
10
 
9
11
  module ActiveRecordProxyAdapters
10
12
  # Provides a global configuration object to configure how the proxy should behave.
@@ -13,16 +15,38 @@ module ActiveRecordProxyAdapters
13
15
 
14
16
  DEFAULT_DATABASE_NAME = :primary
15
17
  DEFAULT_REPLICA_DATABASE_NAME = :primary_replica
18
+ TIMEOUT_MESSAGE_BUILDER = proc { |sql_string, regex = nil|
19
+ [regex, "timed out. Input too big (#{sql_string.size})."].compact_blank.join(" ")
20
+ }.freeze
21
+
22
+ private_constant :TIMEOUT_MESSAGE_BUILDER
23
+
24
+ REGEXP_TIMEOUT_STRATEGY_REGISTRY = {
25
+ log: proc { |sql_string, regex = nil|
26
+ ActiveRecordProxyAdapters.config.logger.error(TIMEOUT_MESSAGE_BUILDER.call(sql_string, regex))
27
+ },
28
+ raise: proc { |sql_string, regex = nil|
29
+ raise ActiveRecordProxyAdapters::RegexpTimeoutError, TIMEOUT_MESSAGE_BUILDER.call(sql_string, regex)
30
+ }
31
+ }.freeze
16
32
 
17
33
  # @return [Class] The context that is used to store the current request's state.
18
34
  attr_reader :context_store
19
35
 
36
+ # @return [Proc] The timeout strategy to use for regex matching.
37
+ attr_reader :regexp_timeout_strategy
38
+
39
+ # @return [Logger] The logger to use for logging messages.
40
+ attr_reader :logger
41
+
20
42
  def initialize
21
43
  @lock = Monitor.new
22
44
 
23
- self.cache_configuration = CacheConfiguration.new(@lock)
24
- self.context_store = ActiveRecordProxyAdapters::Context
25
- @database_configurations = {}
45
+ self.cache_configuration = CacheConfiguration.new(@lock)
46
+ self.context_store = ActiveRecordProxyAdapters::Context
47
+ self.regexp_timeout_strategy = :log
48
+ self.logger = ActiveRecord::Base.logger || Logger.new($stdout)
49
+ @database_configurations = {}
26
50
  end
27
51
 
28
52
  def log_subscriber_primary_prefix=(prefix)
@@ -57,6 +81,25 @@ module ActiveRecordProxyAdapters
57
81
  default_database_config.checkout_timeout = checkout_timeout
58
82
  end
59
83
 
84
+ def logger=(logger)
85
+ synchronize_update(:logger, from: @logger, to: logger) do
86
+ @logger = logger
87
+ end
88
+ end
89
+
90
+ def regexp_timeout_strategy=(strategy)
91
+ synchronize_update(:regexp_timeout_strategy, from: @regexp_timeout_strategy, to: strategy) do
92
+ @regexp_timeout_strategy = if strategy.respond_to?(:call)
93
+ strategy
94
+ else
95
+ REGEXP_TIMEOUT_STRATEGY_REGISTRY.fetch(strategy)
96
+ end
97
+ rescue KeyError
98
+ raise ActiveRecordProxyAdapters::ConfigurationError,
99
+ "Invalid regex timeout strategy: #{strategy.inspect}. Must be one of: #{valid_regexp_timeout_strategies}"
100
+ end
101
+ end
102
+
60
103
  def database(database_name)
61
104
  key = database_name.to_s
62
105
  lock.synchronize { @database_configurations[key] ||= DatabaseConfiguration.new }
@@ -72,6 +115,10 @@ module ActiveRecordProxyAdapters
72
115
 
73
116
  attr_reader :cache_configuration, :database_configurations, :lock
74
117
 
118
+ def valid_regexp_timeout_strategies
119
+ REGEXP_TIMEOUT_STRATEGY_REGISTRY.keys
120
+ end
121
+
75
122
  def default_database_config
76
123
  database(DEFAULT_DATABASE_NAME)
77
124
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordProxyAdapters
4
+ Error = Class.new(StandardError)
5
+ RegexpTimeoutError = Class.new(Error)
6
+ ConfigurationError = Class.new(Error)
7
+ end
@@ -40,6 +40,20 @@ module ActiveRecordProxyAdapters
40
40
  proxy_config.context_store
41
41
  end
42
42
 
43
+ # Helper to retrieve the logger from the configuration stored in
44
+ # {ActiveRecordProxyAdapters::Configuration#logger}.
45
+ # @return [Logger]
46
+ def logger
47
+ proxy_config.logger
48
+ end
49
+
50
+ # Helper to retrieve the timeout strategy from the configuration stored in
51
+ # {ActiveRecordProxyAdapters::Configuration#regexp_timeout_strategy}.
52
+ # @return [Proc]
53
+ def regexp_timeout_strategy
54
+ proxy_config.regexp_timeout_strategy
55
+ end
56
+
43
57
  # Helper to retrieve the cache store from the configuration stored in
44
58
  # {ActiveRecordProxyAdapters::CacheConfiguration#store}.
45
59
  # @return [ActiveSupport::Cache::Store]
@@ -7,6 +7,7 @@ require "active_record_proxy_adapters/hijackable"
7
7
  require "active_record_proxy_adapters/mixin/configuration"
8
8
  require "active_support/core_ext/module/delegation"
9
9
  require "active_support/core_ext/object/blank"
10
+ require "timeout"
10
11
 
11
12
  module ActiveRecordProxyAdapters
12
13
  # This is the base class for all proxies. It defines the methods that should be proxied
@@ -113,8 +114,9 @@ module ActiveRecordProxyAdapters
113
114
  end.last
114
115
  end
115
116
 
116
- def roles_for(sql_string) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
117
- return [top_of_connection_stack_role] if top_of_connection_stack_role.present?
117
+ def roles_for(sql_string) # rubocop:disable Metrics/MethodLength
118
+ top_of_stack_role = top_of_connection_stack_role
119
+ return [top_of_stack_role] if top_of_stack_role.present?
118
120
  return [writing_role] if recent_write_to_primary? || in_transaction?
119
121
 
120
122
  cache_key = cache_key_for(sql_string)
@@ -136,17 +138,23 @@ module ActiveRecordProxyAdapters
136
138
  return if connected_to_stack.empty?
137
139
 
138
140
  top = connected_to_stack.last
139
- role = top[:role]
141
+ role, klasses = top.values_at(:role, :klasses)
140
142
  return unless role.present?
141
143
 
142
- [reading_role, writing_role].include?(role) ? role : nil
144
+ # ActiveRecord::Base is the parent record for all models so,
145
+ # if the top of the stack includes it, we should respect it.
146
+ role_for_current_class = klasses.include?(connection_class) || klasses.include?(ActiveRecord::Base)
147
+
148
+ [reading_role, writing_role].include?(role) && role_for_current_class ? role : nil
143
149
  end
144
150
 
145
151
  def connected_to_stack
146
152
  return connection_class.connected_to_stack if connection_class.respond_to?(:connected_to_stack)
147
153
 
148
154
  # handle Rails 7.2+ pending migrations Connection
149
- return [{ role: writing_role }] if pending_migration_connection?
155
+ if pending_migration_connection?
156
+ return [{ role: writing_role, shard: nil, prevent_writes: false, klasses: [connection_class] }]
157
+ end
150
158
 
151
159
  []
152
160
  end
@@ -181,7 +189,7 @@ module ActiveRecordProxyAdapters
181
189
  end
182
190
 
183
191
  def checkout_replica_connection
184
- replica_pool.checkout(checkout_timeout(primary_connection_name))
192
+ replica_pool.checkout(proxy_checkout_timeout)
185
193
  # rescue NoDatabaseError to avoid crashing when running db:create rake task
186
194
  # rescue ConnectionNotEstablished to handle connectivity issues in the replica
187
195
  # (for example, replication delay)
@@ -215,7 +223,14 @@ module ActiveRecordProxyAdapters
215
223
  end
216
224
 
217
225
  def match_sql?(sql_string)
218
- proc { |matcher| matcher.match?(sql_string) }
226
+ proc do |matcher|
227
+ # TODO: switch to regexp timeout once Ruby 3.1 support is dropped.
228
+ Timeout.timeout(proxy_checkout_timeout.to_f) { matcher.match?(sql_string) }
229
+ rescue Timeout::Error
230
+ regexp_timeout_strategy.call(sql_string, matcher)
231
+
232
+ false
233
+ end
219
234
  end
220
235
 
221
236
  # @return Boolean
@@ -235,6 +250,10 @@ module ActiveRecordProxyAdapters
235
250
  @primary_connection_name ||= primary_connection.pool.try(:db_config).try(:name).try(:to_s)
236
251
  end
237
252
 
253
+ def proxy_checkout_timeout
254
+ checkout_timeout(primary_connection_name)
255
+ end
256
+
238
257
  def proxy_context
239
258
  self.current_context ||= context_store.new({})
240
259
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordProxyAdapters
4
- VERSION = "0.7.2"
4
+ VERSION = "0.8.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_proxy_adapters
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Cruz
@@ -77,6 +77,34 @@ dependencies:
77
77
  - - ">="
78
78
  - !ruby/object:Gem::Version
79
79
  version: '0'
80
+ - !ruby/object:Gem::Dependency
81
+ name: logger
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ type: :runtime
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: timeout
96
+ requirement: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ type: :runtime
102
+ prerelease: false
103
+ version_requirements: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
80
108
  description: |-
81
109
  This gem allows automatic connection switching between a primary and one read replica database in ActiveRecord.
82
110
  It pattern matches the SQL statement being sent to decide whether it should go to the replica (SELECT) or the
@@ -112,6 +140,7 @@ files:
112
140
  - lib/active_record_proxy_adapters/contextualizer.rb
113
141
  - lib/active_record_proxy_adapters/database_configuration.rb
114
142
  - lib/active_record_proxy_adapters/database_tasks.rb
143
+ - lib/active_record_proxy_adapters/errors.rb
115
144
  - lib/active_record_proxy_adapters/hijackable.rb
116
145
  - lib/active_record_proxy_adapters/log_subscriber.rb
117
146
  - lib/active_record_proxy_adapters/middleware.rb