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 +4 -4
- data/lib/active_record_proxy_adapters/configuration.rb +50 -3
- data/lib/active_record_proxy_adapters/errors.rb +7 -0
- data/lib/active_record_proxy_adapters/mixin/configuration.rb +14 -0
- data/lib/active_record_proxy_adapters/primary_replica_proxy.rb +26 -7
- data/lib/active_record_proxy_adapters/version.rb +1 -1
- metadata +30 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8e87a47a033b2402f2fdd0dbae2aec360ff04afbe4639f83615030da82d80cfc
|
4
|
+
data.tar.gz: 7caec5f8d663d2b67482f28f30b669e36a00fedb8d9eea6b06404ad3df9ce594
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
24
|
-
self.context_store
|
25
|
-
|
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
|
@@ -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
|
117
|
-
|
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
|
141
|
+
role, klasses = top.values_at(:role, :klasses)
|
140
142
|
return unless role.present?
|
141
143
|
|
142
|
-
|
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
|
-
|
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(
|
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
|
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
|
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.
|
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
|