active_record_proxy_adapters 0.4.3 → 0.4.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 +4 -4
- data/README.md +0 -3
- data/lib/active_record/connection_adapters/mysql2_proxy_adapter.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql_proxy_adapter.rb +1 -1
- data/lib/active_record/connection_adapters/trilogy_proxy_adapter.rb +1 -1
- data/lib/active_record_proxy_adapters/active_record_context.rb +18 -0
- data/lib/active_record_proxy_adapters/configuration.rb +44 -8
- data/lib/active_record_proxy_adapters/primary_replica_proxy.rb +23 -7
- data/lib/active_record_proxy_adapters/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c60e5b3ab2242f3c816eeefd49bbb4db18d039b9dd958cc6f356820434f9625
|
4
|
+
data.tar.gz: 995f0698c350be766b1b6ae83061187a4e2a76a5cd1f6a6bb3b76f4df06a0fd5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 438d205bc160dfa52773b8048c9057dbf7b564d26927cb25a25ef470ed9a3cd5fe579acb067aa64cc12eeb7a02fa003ce0b75f2e7dd8093349d4925dac970cd0
|
7
|
+
data.tar.gz: dc0fb03f548118d1475f9fa5a94187b1d31dbaec66ebafef5d906d59597d116cceb4ec31ee25be61f2499cbefb2b6ba46a67c81d0a59f0e0a399f5cb26550932
|
data/README.md
CHANGED
@@ -246,9 +246,6 @@ Since Rails already leases exactly one connection per thread from the pool and t
|
|
246
246
|
|
247
247
|
As long as you're not writing thread unsafe code that handles connections from the pool directly, or you don't have any other gem depenencies that write thread unsafe pool operations, you're all set.
|
248
248
|
|
249
|
-
There is, however, an open bug in `ActiveRecord::ConnectionAdapters::PostgreSQLAdapter` for Rails versions 7.1 and greater that can cause random race conditions, but it's not caused by this gem (More info [here](https://github.com/rails/rails/issues/51780)).
|
250
|
-
Rails 7.0 works as expected.
|
251
|
-
|
252
249
|
Multi-threaded queries example:
|
253
250
|
```ruby
|
254
251
|
# app/models/application_record.rb
|
@@ -15,7 +15,7 @@ module ActiveRecord
|
|
15
15
|
|
16
16
|
ADAPTER_NAME = "Mysql2Proxy"
|
17
17
|
|
18
|
-
delegate_to_proxy
|
18
|
+
delegate_to_proxy(*ActiveRecordProxyAdapters::ActiveRecordContext.hijackable_methods)
|
19
19
|
|
20
20
|
def initialize(...)
|
21
21
|
@proxy = ActiveRecordProxyAdapters::Mysql2Proxy.new(self)
|
@@ -15,7 +15,7 @@ module ActiveRecord
|
|
15
15
|
|
16
16
|
ADAPTER_NAME = "PostgreSQLProxy"
|
17
17
|
|
18
|
-
delegate_to_proxy
|
18
|
+
delegate_to_proxy(*ActiveRecordProxyAdapters::ActiveRecordContext.hijackable_methods)
|
19
19
|
|
20
20
|
unless ActiveRecordProxyAdapters::ActiveRecordContext.active_record_v8_0_or_greater?
|
21
21
|
delegate_to_proxy :exec_no_cache, :exec_cache
|
@@ -15,7 +15,7 @@ module ActiveRecord
|
|
15
15
|
|
16
16
|
ADAPTER_NAME = "TrilogyProxy"
|
17
17
|
|
18
|
-
delegate_to_proxy
|
18
|
+
delegate_to_proxy(*ActiveRecordProxyAdapters::ActiveRecordContext.hijackable_methods)
|
19
19
|
|
20
20
|
def initialize(...)
|
21
21
|
@proxy = ActiveRecordProxyAdapters::TrilogyProxy.new(self)
|
@@ -23,6 +23,8 @@ module ActiveRecordProxyAdapters
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def connection_class_for(connection)
|
26
|
+
return connection.connection_descriptor.name.constantize if active_record_v8_0_2_or_greater?
|
27
|
+
|
26
28
|
connection.connection_class || ActiveRecord::Base
|
27
29
|
end
|
28
30
|
|
@@ -33,6 +35,18 @@ module ActiveRecordProxyAdapters
|
|
33
35
|
ActiveRecord
|
34
36
|
end
|
35
37
|
|
38
|
+
def hijackable_methods
|
39
|
+
hijackable = %i[execute exec_query]
|
40
|
+
|
41
|
+
hijackable << :internal_exec_query if active_record_v8_0_or_greater?
|
42
|
+
|
43
|
+
hijackable
|
44
|
+
end
|
45
|
+
|
46
|
+
def active_record_v7?
|
47
|
+
active_record_version >= Gem::Version.new("7.0") && active_record_version < Gem::Version.new("8.0")
|
48
|
+
end
|
49
|
+
|
36
50
|
def active_record_v7_1_or_greater?
|
37
51
|
active_record_version >= Gem::Version.new("7.1")
|
38
52
|
end
|
@@ -44,5 +58,9 @@ module ActiveRecordProxyAdapters
|
|
44
58
|
def active_record_v8_0_or_greater?
|
45
59
|
active_record_version >= Gem::Version.new("8.0")
|
46
60
|
end
|
61
|
+
|
62
|
+
def active_record_v8_0_2_or_greater?
|
63
|
+
active_record_version >= Gem::Version.new("8.0.2")
|
64
|
+
end
|
47
65
|
end
|
48
66
|
end
|
@@ -11,19 +11,21 @@ module ActiveRecordProxyAdapters
|
|
11
11
|
LOG_SUBSCRIBER_REPLICA_PREFIX = proc { |event| "#{event.payload[:connection].class::ADAPTER_NAME} Replica" }.freeze
|
12
12
|
|
13
13
|
# @return [ActiveSupport::Duration] How long the proxy should reroute all read requests to the primary database
|
14
|
-
# since the latest write. Defaults to PROXY_DELAY.
|
15
|
-
|
14
|
+
# since the latest write. Defaults to PROXY_DELAY. Thread safe.
|
15
|
+
attr_reader :proxy_delay
|
16
16
|
# @return [ActiveSupport::Duration] How long the proxy should wait for a connection from the replica pool.
|
17
|
-
# Defaults to CHECKOUT_TIMEOUT.
|
18
|
-
|
17
|
+
# Defaults to CHECKOUT_TIMEOUT. Thread safe.
|
18
|
+
attr_reader :checkout_timeout
|
19
19
|
|
20
|
-
# @return [Proc] Prefix for the log subscriber when the primary database is used.
|
20
|
+
# @return [Proc] Prefix for the log subscriber when the primary database is used. Thread safe.
|
21
21
|
attr_reader :log_subscriber_primary_prefix
|
22
22
|
|
23
|
-
# @return [Proc] Prefix for the log subscriber when the replica database is used.
|
23
|
+
# @return [Proc] Prefix for the log subscriber when the replica database is used. Thread safe.
|
24
24
|
attr_reader :log_subscriber_replica_prefix
|
25
25
|
|
26
26
|
def initialize
|
27
|
+
@lock = Monitor.new
|
28
|
+
|
27
29
|
self.proxy_delay = PROXY_DELAY
|
28
30
|
self.checkout_timeout = CHECKOUT_TIMEOUT
|
29
31
|
self.log_subscriber_primary_prefix = LOG_SUBSCRIBER_PRIMARY_PREFIX
|
@@ -31,11 +33,45 @@ module ActiveRecordProxyAdapters
|
|
31
33
|
end
|
32
34
|
|
33
35
|
def log_subscriber_primary_prefix=(prefix)
|
34
|
-
|
36
|
+
prefix_proc = prefix.is_a?(Proc) ? prefix : proc { prefix.to_s }
|
37
|
+
|
38
|
+
synchronize_update(:log_subscriber_primary_prefix, from: @log_subscriber_primary_prefix, to: prefix_proc) do
|
39
|
+
@log_subscriber_primary_prefix = prefix_proc
|
40
|
+
end
|
35
41
|
end
|
36
42
|
|
37
43
|
def log_subscriber_replica_prefix=(prefix)
|
38
|
-
|
44
|
+
prefix_proc = prefix.is_a?(Proc) ? prefix : proc { prefix.to_s }
|
45
|
+
|
46
|
+
synchronize_update(:log_subscriber_replica_prefix, from: @log_subscriber_replica_prefix, to: prefix_proc) do
|
47
|
+
@log_subscriber_replica_prefix = prefix_proc
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def proxy_delay=(proxy_delay)
|
52
|
+
synchronize_update(:proxy_delay, from: @proxy_delay, to: proxy_delay) do
|
53
|
+
@proxy_delay = proxy_delay
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def checkout_timeout=(checkout_timeout)
|
58
|
+
synchronize_update(:checkout_timeout, from: @checkout_timeout, to: checkout_timeout) do
|
59
|
+
@checkout_timeout = checkout_timeout
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def synchronize_update(attribute, from:, to:, &block)
|
66
|
+
ActiveSupport::Notifications.instrument(
|
67
|
+
"active_record_proxy_adapters.configuration_update",
|
68
|
+
attribute:,
|
69
|
+
who: Thread.current,
|
70
|
+
from:,
|
71
|
+
to:
|
72
|
+
) do
|
73
|
+
@lock.synchronize(&block)
|
74
|
+
end
|
39
75
|
end
|
40
76
|
end
|
41
77
|
end
|
@@ -18,19 +18,30 @@ module ActiveRecordProxyAdapters
|
|
18
18
|
/\A\s*select.+for update\Z/i, /select.+lock in share mode\Z/i,
|
19
19
|
/\A\s*select.+(nextval|currval|lastval|get_lock|release_lock|pg_advisory_lock|pg_advisory_unlock)\(/i
|
20
20
|
].map(&:freeze).freeze
|
21
|
+
|
22
|
+
CTE_MATCHER = /\A\s*WITH\s+(?<CTE>\S+\s+AS\s+\(\s?[\s\S]*\))/i
|
21
23
|
# All queries that match these patterns should be sent to the replica database
|
22
|
-
SQL_REPLICA_MATCHERS
|
24
|
+
SQL_REPLICA_MATCHERS = [
|
25
|
+
/\A\s*(select)\s/i,
|
26
|
+
/#{CTE_MATCHER.source}\s*select/i
|
27
|
+
].map(&:freeze).freeze
|
23
28
|
# All queries that match these patterns should be sent to all databases
|
24
29
|
SQL_ALL_MATCHERS = [/\A\s*set\s/i].map(&:freeze).freeze
|
25
30
|
# Local sets queries should not be sent to all datbases
|
26
31
|
SQL_SKIP_ALL_MATCHERS = [/\A\s*set\s+local\s/i].map(&:freeze).freeze
|
27
32
|
# These patterns define which database statments are considered write statments, so we can shortly re-route all
|
28
33
|
# requests to the primary database so the replica has time to replicate
|
29
|
-
WRITE_STATEMENT_MATCHERS = [
|
30
|
-
|
34
|
+
WRITE_STATEMENT_MATCHERS = [
|
35
|
+
/\ABEGIN/i,
|
36
|
+
/\ACOMMIT/i,
|
37
|
+
/INSERT\s[\s\S]*INTO\s[\s\S]*/i,
|
38
|
+
/UPDATE\s[\s\S]*/i,
|
39
|
+
/DELETE\s[\s\S]*FROM\s[\s\S]*/i,
|
40
|
+
/DROP\s/i
|
41
|
+
].map(&:freeze).freeze
|
31
42
|
|
32
43
|
# Abstract adapter methods that should be proxied.
|
33
|
-
hijack_method
|
44
|
+
hijack_method(*ActiveRecordContext.hijackable_methods)
|
34
45
|
|
35
46
|
def self.hijacked_methods
|
36
47
|
@hijacked_methods.to_a
|
@@ -171,16 +182,21 @@ module ActiveRecordProxyAdapters
|
|
171
182
|
# @return [TrueClass] if there has been a write within the last {#proxy_delay} seconds
|
172
183
|
# @return [TrueClass] if sql_string matches a write statement (i.e. INSERT, UPDATE, DELETE, SELECT FOR UPDATE)
|
173
184
|
# @return [FalseClass] if sql_string matches a read statement (i.e. SELECT)
|
174
|
-
def need_primary?(sql_string)
|
175
|
-
return true
|
176
|
-
|
185
|
+
def need_primary?(sql_string) # rubocop:disable Metrics/CyclomaticComplexity
|
186
|
+
return true if recent_write_to_primary?
|
177
187
|
return true if in_transaction?
|
188
|
+
return true if cte_for_write?(sql_string)
|
178
189
|
return true if SQL_PRIMARY_MATCHERS.any?(&match_sql?(sql_string))
|
179
190
|
return false if SQL_REPLICA_MATCHERS.any?(&match_sql?(sql_string))
|
180
191
|
|
181
192
|
true
|
182
193
|
end
|
183
194
|
|
195
|
+
def cte_for_write?(sql_string)
|
196
|
+
CTE_MATCHER.match?(sql_string) &&
|
197
|
+
WRITE_STATEMENT_MATCHERS.any?(&match_sql?(sql_string))
|
198
|
+
end
|
199
|
+
|
184
200
|
def need_all?(sql_string)
|
185
201
|
return false if SQL_SKIP_ALL_MATCHERS.any?(&match_sql?(sql_string))
|
186
202
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record_proxy_adapters
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt Cruz
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-03-
|
10
|
+
date: 2025-03-31 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: activerecord
|