active_record_proxy_adapters 0.4.2 → 0.4.4

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: 1dbad1d7e12818f5671c53475572a48f6ad2645ac528410d96dd851760cce2f5
4
- data.tar.gz: 51d8be8b3b36f5c7590d7d804427fdad95d4753c66cee3c3453fafb17d68c36c
3
+ metadata.gz: b3d41a4a59d8c5917bdbc07a02c626f6e0f028b1fac534a436ec114816902c4a
4
+ data.tar.gz: c0a14e90a707f6ffdaf2e0f67443a880de96c6bb3278795677dd47258c07bf26
5
5
  SHA512:
6
- metadata.gz: ccb3f7e1347e7d21a79c36fbf39740f58188d215b7929b2e1926cbe7142bd5f97afebf2ddbc0cfbdf0a8dd4536d18f1586f3f63464b8f3fde096545eb0696431
7
- data.tar.gz: 3e9646ced949036ef60f27e0dd2dbf973e75f81efc1be3be90cbb6713b7647b74b1ba2d993d8edd448a0709fae7c70ed7c6d9f68041f2ed34d7b53d54ccbb759
6
+ metadata.gz: a89e0589e582bc2ed1da956640bf3c1455e14ae4d712684adad898c7c2eb00c6b297cf5f0dd0184a1fc70c9642814d130749a3c2352480790fba4f0574c89ffe
7
+ data.tar.gz: 2f9dc79dad11c1469ff3fb04e1a201f6f746206714a49a03d9e15e501cade1878b0959ddeb80f02b6538d340a8af911f20588a93d251e87ee77ca120e9f97186
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 :execute, :exec_query
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 :execute, :exec_query
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 :execute, :exec_query
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
- attr_accessor :proxy_delay
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
- attr_accessor :checkout_timeout
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
- @log_subscriber_primary_prefix = prefix.is_a?(Proc) ? prefix : proc { prefix.to_s }
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
- @log_subscriber_replica_prefix = prefix.is_a?(Proc) ? prefix : proc { prefix.to_s }
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
@@ -30,7 +30,7 @@ module ActiveRecordProxyAdapters
30
30
  /DROP\s/i].map(&:freeze).freeze
31
31
 
32
32
  # Abstract adapter methods that should be proxied.
33
- hijack_method :execute, :exec_query
33
+ hijack_method(*ActiveRecordContext.hijackable_methods)
34
34
 
35
35
  def self.hijacked_methods
36
36
  @hijacked_methods.to_a
@@ -50,6 +50,22 @@ module ActiveRecordProxyAdapters
50
50
  delegate :connection_handler, to: :connection_class
51
51
  delegate :reading_role, :writing_role, to: :active_record_context
52
52
 
53
+ # We need to call .verify! to ensure `configure_connection` is called on the instance before attempting to use it.
54
+ # This is necessary because the connection may have been lazily initialized and is an unintended side effect from a
55
+ # change in Rails to defer connection verification: https://github.com/rails/rails/pull/44576
56
+ # verify! cannot be called before the object is initialized and because of how the proxy hooks into the connection
57
+ # instance, it has to be done lazily (hence the memoization). Ideally, we shouldn't have to worry about this at all
58
+ # But there is tight coupling between methods in ActiveRecord::ConnectionAdapters::AbstractAdapter and
59
+ # its descendants which will require significant refactoring to be decoupled.
60
+ # See https://github.com/rails/rails/issues/51780
61
+ def verified_primary_connection
62
+ @verified_primary_connection ||= begin
63
+ connected_to(role: writing_role) { primary_connection.verify! }
64
+
65
+ primary_connection
66
+ end
67
+ end
68
+
53
69
  def replica_pool_unavailable?
54
70
  !replica_pool
55
71
  end
@@ -120,7 +136,8 @@ module ActiveRecordProxyAdapters
120
136
  end
121
137
 
122
138
  def connection_for(role, sql_string)
123
- connection = primary_connection if role == writing_role || replica_pool_unavailable?
139
+ connection = verified_primary_connection if role == writing_role || replica_pool_unavailable?
140
+
124
141
  connection ||= checkout_replica_connection
125
142
 
126
143
  result = connected_to(role:) { yield connection }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordProxyAdapters
4
- VERSION = "0.4.2"
4
+ VERSION = "0.4.4"
5
5
  end
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.2
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Cruz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-02-25 00:00:00.000000000 Z
10
+ date: 2025-03-19 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord