active_record_proxy_adapters 0.6.1 → 0.6.2

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: d73a2213222b1689c5ff581f40e561974446e480355d582fca5daa04eccf9996
4
- data.tar.gz: ee667c46d62816df1dafbe583cbe0503448097f30ebf7c6d37565fd6cb335779
3
+ metadata.gz: ea3aa59337aa2ed793bfb2e51666d08ef6b40ad9ccd2008453807baeac862933
4
+ data.tar.gz: 1e899907a68ecf3c6ca5a51380e2b7be9859d40c0b033f23b872037c6d1fd5a8
5
5
  SHA512:
6
- metadata.gz: a0c1db04e45da543ff86f31a37b45a5f3c5a3e31d0c877cd7e0038f46d01207bf90124868dda9a89ce73a0701726e9c539a422ef53a6e3a9cac9f2304e20a4de
7
- data.tar.gz: 1e7eeb8e61c753dd6ad8475a61c4625c90240cdf1d2cf544b40c215a34ec498df806e8403922728c579f5dc727ea5901ff9e81875a57a36fcb2f1fbb17ec4b08
6
+ metadata.gz: 3b9018059039d65f435a8d4571f5eea8a477e4388728a675b2176252959e5fb6afd0833cb21e65ea5bbd04e77c484f1d2ec3974d698b057c72440f7a9349150a
7
+ data.tar.gz: 7bf7662851cc2d6b1bcd7fcb59df5216fb87f28c368910da05476b6b734f692f2182de15bb731276f5952690b9ded4840ed2ddd2e35653fb2ff17c9c6e0a7bc4
@@ -40,7 +40,7 @@ module ActiveRecordProxyAdapters
40
40
  def hijackable_methods
41
41
  hijackable = %i[execute exec_query]
42
42
 
43
- hijackable << :internal_exec_query if active_record_v8_0_or_greater?
43
+ hijackable << :internal_exec_query if active_record_v7_1_or_greater?
44
44
 
45
45
  hijackable
46
46
  end
@@ -3,6 +3,7 @@
3
3
  require "active_support/core_ext/integer/time"
4
4
  require "active_record_proxy_adapters/synchronizable_configuration"
5
5
  require "active_record_proxy_adapters/cache_configuration"
6
+ require "active_record_proxy_adapters/context"
6
7
 
7
8
  module ActiveRecordProxyAdapters
8
9
  # Provides a global configuration object to configure how the proxy should behave.
@@ -27,6 +28,9 @@ module ActiveRecordProxyAdapters
27
28
  # @return [Proc] Prefix for the log subscriber when the replica database is used. Thread safe.
28
29
  attr_reader :log_subscriber_replica_prefix
29
30
 
31
+ # @return [Class] The context that is used to store the current request's state.
32
+ attr_reader :context_store
33
+
30
34
  def initialize
31
35
  @lock = Monitor.new
32
36
 
@@ -35,6 +39,7 @@ module ActiveRecordProxyAdapters
35
39
  self.log_subscriber_primary_prefix = LOG_SUBSCRIBER_PRIMARY_PREFIX
36
40
  self.log_subscriber_replica_prefix = LOG_SUBSCRIBER_REPLICA_PREFIX
37
41
  self.cache_configuration = CacheConfiguration.new(@lock)
42
+ self.context_store = ActiveRecordProxyAdapters::Context
38
43
  end
39
44
 
40
45
  def log_subscriber_primary_prefix=(prefix)
@@ -65,6 +70,12 @@ module ActiveRecordProxyAdapters
65
70
  end
66
71
  end
67
72
 
73
+ def context_store=(context_store)
74
+ synchronize_update(:context_store, from: @context_store, to: context_store) do
75
+ @context_store = context_store
76
+ end
77
+ end
78
+
68
79
  def cache
69
80
  block_given? ? yield(cache_configuration) : cache_configuration
70
81
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record_proxy_adapters/mixin/configuration"
4
+
5
+ module ActiveRecordProxyAdapters
6
+ # Context is a simple class that holds a registry of connection names and their last write timestamps.
7
+ # It is used to track the last time a write operation was performed on each connection.
8
+ # This allows the proxy to determine whether to route read requests to the primary or replica database
9
+ class Context
10
+ include Mixin::Configuration
11
+
12
+ # @param hash [Hash] A hash containing the connection names as keys and the last write timestamps as values.
13
+ # Can be empty.
14
+ def initialize(hash)
15
+ @timestamp_registry = hash.transform_values(&:to_f)
16
+ end
17
+
18
+ def recent_write_to_primary?(connection_name)
19
+ now - self[connection_name] < proxy_delay
20
+ end
21
+
22
+ def update_for(connection_name)
23
+ self[connection_name] = now
24
+ end
25
+
26
+ def [](connection_name)
27
+ timestamp_registry[connection_name] || 0
28
+ end
29
+
30
+ def []=(connection_name, timestamp)
31
+ timestamp_registry[connection_name] = timestamp
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :timestamp_registry
37
+
38
+ def now
39
+ Time.now.utc.to_f
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordProxyAdapters
4
+ # A mixin for managing the context of current database connections.
5
+ module Contextualizer
6
+ module_function
7
+
8
+ # @return [ActiveRecordProxyAdapters::Context]
9
+ # Retrieves the context for the current thread.
10
+ def current_context
11
+ Thread.current.thread_variable_get(:arpa_context)
12
+ end
13
+
14
+ # @param context [ActiveRecordProxyAdapters::Context]
15
+ # Sets the context for the current thread.
16
+ def current_context=(context)
17
+ Thread.current.thread_variable_set(:arpa_context, context)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/integer/time"
4
+ require "active_record_proxy_adapters/synchronizable_configuration"
5
+ require "active_record_proxy_adapters/cache_configuration"
6
+ require "active_record_proxy_adapters/context"
7
+
8
+ module ActiveRecordProxyAdapters
9
+ module Mixin
10
+ # Provides helpers to access to reduce boilerplate while retrieving configuration properties.
11
+ module Configuration
12
+ # Helper to retrieve the proxy delay from the configuration stored in
13
+ # {ActiveRecordProxyAdapters::Configuration#proxy_delay}.
14
+ # @return [ActiveSupport::Duration]
15
+ def proxy_delay
16
+ proxy_config.proxy_delay
17
+ end
18
+
19
+ # Helper to retrieve the checkout timeout from the configuration stored in
20
+ # {ActiveRecordProxyAdapters::Configuration#checkout_timeout}.
21
+ # @return [ActiveSupport::Duration]
22
+ def checkout_timeout
23
+ proxy_config.checkout_timeout
24
+ end
25
+
26
+ # Helper to retrieve the context store class from the configuration stored in
27
+ # {ActiveRecordProxyAdapters::Configuration#context_store}.
28
+ # @return [Class]
29
+ def context_store
30
+ proxy_config.context_store
31
+ end
32
+
33
+ # Helper to retrieve the cache store from the configuration stored in
34
+ # {ActiveRecordProxyAdapters::CacheConfiguration#store}.
35
+ # @return [ActiveSupport::Cache::Store]
36
+ def cache_store
37
+ cache_config.store
38
+ end
39
+
40
+ # Helper to retrieve the cache key prefix from the configuration stored in
41
+ # {ActiveRecordProxyAdapters::CacheConfiguration#key_prefix}.
42
+ # It uses the key builder to generate a cache key for the given SQL string, prepended with the key prefix.
43
+ # @return [String]
44
+ def cache_key_for(sql_string)
45
+ cache_config.key_builder.call(sql_string).prepend(cache_config.key_prefix)
46
+ end
47
+
48
+ # @!visibility private
49
+ def cache_config
50
+ proxy_config.cache
51
+ end
52
+
53
+ # @!visibility private
54
+ def proxy_config
55
+ ActiveRecordProxyAdapters.config
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,17 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_record_proxy_adapters/active_record_context"
3
4
  require "active_record_proxy_adapters/configuration"
5
+ require "active_record_proxy_adapters/contextualizer"
6
+ require "active_record_proxy_adapters/hijackable"
7
+ require "active_record_proxy_adapters/mixin/configuration"
4
8
  require "active_support/core_ext/module/delegation"
5
9
  require "active_support/core_ext/object/blank"
6
- require "concurrent-ruby"
7
- require "active_record_proxy_adapters/active_record_context"
8
- require "active_record_proxy_adapters/hijackable"
9
10
 
10
11
  module ActiveRecordProxyAdapters
11
12
  # This is the base class for all proxies. It defines the methods that should be proxied
12
13
  # and the logic to determine which database to use.
13
14
  class PrimaryReplicaProxy # rubocop:disable Metrics/ClassLength
14
15
  include Hijackable
16
+ include Contextualizer
17
+ include Mixin::Configuration
15
18
 
16
19
  # All queries that match these patterns should be sent to the primary database
17
20
  SQL_PRIMARY_MATCHERS = [
@@ -51,13 +54,12 @@ module ActiveRecordProxyAdapters
51
54
  # @param primary_connection [ActiveRecord::ConnectionAdatpers::AbstractAdapter]
52
55
  def initialize(primary_connection)
53
56
  @primary_connection = primary_connection
54
- @last_write_at = 0
55
57
  @active_record_context = ActiveRecordContext.new
56
58
  end
57
59
 
58
60
  private
59
61
 
60
- attr_reader :primary_connection, :last_write_at, :active_record_context
62
+ attr_reader :primary_connection, :active_record_context
61
63
 
62
64
  delegate :connection_handler, to: :connection_class
63
65
  delegate :reading_role, :writing_role, to: :active_record_context
@@ -140,10 +142,6 @@ module ActiveRecordProxyAdapters
140
142
  [reading_role, writing_role].include?(role) ? role : nil
141
143
  end
142
144
 
143
- def cache_key_for(sql_string)
144
- cache_config.key_builder.call(sql_string).prepend(cache_config.key_prefix)
145
- end
146
-
147
145
  def connected_to_stack
148
146
  return connection_class.connected_to_stack if connection_class.respond_to?(:connected_to_stack)
149
147
 
@@ -221,11 +219,9 @@ module ActiveRecordProxyAdapters
221
219
  proc { |matcher| matcher.match?(sql_string) }
222
220
  end
223
221
 
224
- # TODO: implement a context API to re-route requests to the primary database if a recent query was sent to it to
225
- # avoid replication delay issues
226
222
  # @return Boolean
227
223
  def recent_write_to_primary?
228
- Concurrent.monotonic_time - last_write_at < proxy_delay
224
+ proxy_context.recent_write_to_primary?(primary_connection_name)
229
225
  end
230
226
 
231
227
  def in_transaction?
@@ -233,27 +229,15 @@ module ActiveRecordProxyAdapters
233
229
  end
234
230
 
235
231
  def update_primary_latest_write_timestamp
236
- @last_write_at = Concurrent.monotonic_time
237
- end
238
-
239
- def cache_store
240
- cache_config.store
241
- end
242
-
243
- def cache_config
244
- proxy_config.cache
245
- end
246
-
247
- def proxy_delay
248
- proxy_config.proxy_delay
232
+ proxy_context.update_for(primary_connection_name)
249
233
  end
250
234
 
251
- def checkout_timeout
252
- proxy_config.checkout_timeout
235
+ def primary_connection_name
236
+ @primary_connection_name ||= primary_connection.pool.try(:db_config).try(:name).try(:to_sym)
253
237
  end
254
238
 
255
- def proxy_config
256
- ActiveRecordProxyAdapters.config
239
+ def proxy_context
240
+ self.current_context ||= context_store.new({})
257
241
  end
258
242
  end
259
243
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordProxyAdapters
4
- VERSION = "0.6.1"
4
+ VERSION = "0.6.2"
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.6.1
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Cruz
@@ -94,9 +94,12 @@ files:
94
94
  - lib/active_record_proxy_adapters/connection_handling/postgresql.rb
95
95
  - lib/active_record_proxy_adapters/connection_handling/sqlite3.rb
96
96
  - lib/active_record_proxy_adapters/connection_handling/trilogy.rb
97
+ - lib/active_record_proxy_adapters/context.rb
98
+ - lib/active_record_proxy_adapters/contextualizer.rb
97
99
  - lib/active_record_proxy_adapters/database_tasks.rb
98
100
  - lib/active_record_proxy_adapters/hijackable.rb
99
101
  - lib/active_record_proxy_adapters/log_subscriber.rb
102
+ - lib/active_record_proxy_adapters/mixin/configuration.rb
100
103
  - lib/active_record_proxy_adapters/mysql2_proxy.rb
101
104
  - lib/active_record_proxy_adapters/postgresql_proxy.rb
102
105
  - lib/active_record_proxy_adapters/primary_replica_proxy.rb