active_record_proxy_adapters 0.6.0 → 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: 7884786c370072543b8702a379b372ffa878e281e88e8e96ba603cec78a01523
4
- data.tar.gz: 80141eaa2a1e1bbd9c4eb985136140acf006c16e5749497a114574f809325e98
3
+ metadata.gz: ea3aa59337aa2ed793bfb2e51666d08ef6b40ad9ccd2008453807baeac862933
4
+ data.tar.gz: 1e899907a68ecf3c6ca5a51380e2b7be9859d40c0b033f23b872037c6d1fd5a8
5
5
  SHA512:
6
- metadata.gz: 7f89bd7a1edf1e8813a9b70610c0153603958b64dbcecd5a00cc506301819e774b45ff291b06cfe01d8f25d2db6763853cb2bac4462d98836c33fbada30231b5
7
- data.tar.gz: b7db70493adab591fd31c4294a630ff976e7cefac58a2e466f2e5cb6511102b1900054fbbc6d632a59cac8875f1f047d4ad0c78a5385f669987ec48bc8420651
6
+ metadata.gz: 3b9018059039d65f435a8d4571f5eea8a477e4388728a675b2176252959e5fb6afd0833cb21e65ea5bbd04e77c484f1d2ec3974d698b057c72440f7a9349150a
7
+ data.tar.gz: 7bf7662851cc2d6b1bcd7fcb59df5216fb87f28c368910da05476b6b734f692f2182de15bb731276f5952690b9ded4840ed2ddd2e35653fb2ff17c9c6e0a7bc4
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_record/tasks/mysql2_proxy_database_tasks"
4
- require "active_record/connection_adapters/mysql2_adapter"
5
3
  require "active_record_proxy_adapters/active_record_context"
6
4
  require "active_record_proxy_adapters/hijackable"
7
5
  require "active_record_proxy_adapters/mysql2_proxy"
6
+ require "active_record/connection_adapters/mysql2_adapter"
7
+ require "active_record/tasks/mysql2_proxy_database_tasks"
8
8
 
9
9
  module ActiveRecord
10
10
  module ConnectionAdapters
@@ -13,6 +13,12 @@ module ActiveRecord
13
13
  class Mysql2ProxyAdapter < Mysql2Adapter
14
14
  include ActiveRecordProxyAdapters::Hijackable
15
15
 
16
+ if ActiveRecordProxyAdapters::ActiveRecordContext.active_record_v7_0?
17
+ require "active_record_proxy_adapters/transactionable_proxy_a_r_70"
18
+
19
+ include ActiveRecordProxyAdapters::TransactionableProxyAR70
20
+ end
21
+
16
22
  ADAPTER_NAME = "Mysql2Proxy"
17
23
 
18
24
  delegate_to_proxy(*ActiveRecordProxyAdapters::ActiveRecordContext.hijackable_methods)
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_record/tasks/postgresql_proxy_database_tasks"
4
- require "active_record/connection_adapters/postgresql_adapter"
5
3
  require "active_record_proxy_adapters/active_record_context"
6
4
  require "active_record_proxy_adapters/hijackable"
7
5
  require "active_record_proxy_adapters/postgresql_proxy"
6
+ require "active_record/connection_adapters/postgresql_adapter"
7
+ require "active_record/tasks/postgresql_proxy_database_tasks"
8
8
 
9
9
  module ActiveRecord
10
10
  module ConnectionAdapters
@@ -13,6 +13,12 @@ module ActiveRecord
13
13
  class PostgreSQLProxyAdapter < PostgreSQLAdapter
14
14
  include ActiveRecordProxyAdapters::Hijackable
15
15
 
16
+ if ActiveRecordProxyAdapters::ActiveRecordContext.active_record_v7_0?
17
+ require "active_record_proxy_adapters/transactionable_proxy_a_r_70"
18
+
19
+ include ActiveRecordProxyAdapters::TransactionableProxyAR70
20
+ end
21
+
16
22
  ADAPTER_NAME = "PostgreSQLProxy"
17
23
 
18
24
  delegate_to_proxy(*ActiveRecordProxyAdapters::ActiveRecordContext.hijackable_methods)
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_record/tasks/trilogy_proxy_database_tasks"
4
- require "active_record/connection_adapters/trilogy_adapter"
5
3
  require "active_record_proxy_adapters/active_record_context"
6
4
  require "active_record_proxy_adapters/hijackable"
7
5
  require "active_record_proxy_adapters/trilogy_proxy"
6
+ require "active_record/connection_adapters/trilogy_adapter"
7
+ require "active_record/tasks/trilogy_proxy_database_tasks"
8
8
 
9
9
  module ActiveRecord
10
10
  module ConnectionAdapters
@@ -13,6 +13,12 @@ module ActiveRecord
13
13
  class TrilogyProxyAdapter < TrilogyAdapter
14
14
  include ActiveRecordProxyAdapters::Hijackable
15
15
 
16
+ if ActiveRecordProxyAdapters::ActiveRecordContext.active_record_v7_0?
17
+ require "active_record_proxy_adapters/transactionable_proxy_a_r_70"
18
+
19
+ include ActiveRecordProxyAdapters::TransactionableProxyAR70
20
+ end
21
+
16
22
  ADAPTER_NAME = "TrilogyProxy"
17
23
 
18
24
  delegate_to_proxy(*ActiveRecordProxyAdapters::ActiveRecordContext.hijackable_methods)
@@ -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
@@ -49,6 +49,10 @@ module ActiveRecordProxyAdapters
49
49
  active_record_version >= Gem::Version.new("7.0") && active_record_version < Gem::Version.new("8.0")
50
50
  end
51
51
 
52
+ def active_record_v7_0?
53
+ active_record_version >= Gem::Version.new("7.0") && active_record_version < Gem::Version.new("7.1")
54
+ end
55
+
52
56
  def active_record_v7_1_or_greater?
53
57
  active_record_version >= Gem::Version.new("7.1")
54
58
  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
@@ -52,8 +52,6 @@ module ActiveRecordProxyAdapters
52
52
  "_unproxied"
53
53
  end
54
54
 
55
- private
56
-
57
55
  def proxy_method_name_for(method_name)
58
56
  :"#{method_name}#{unproxied_method_suffix}"
59
57
  end
@@ -63,6 +61,10 @@ module ActiveRecordProxyAdapters
63
61
  def unproxied_method_suffix
64
62
  self.class.unproxied_method_suffix
65
63
  end
64
+
65
+ def proxy_method_name_for(method_name)
66
+ self.class.proxy_method_name_for(method_name)
67
+ end
66
68
  end
67
69
  end
68
70
  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 = [
@@ -34,6 +37,7 @@ module ActiveRecordProxyAdapters
34
37
  WRITE_STATEMENT_MATCHERS = [
35
38
  /\ABEGIN/i,
36
39
  /\ACOMMIT/i,
40
+ /\AROLLBACK/i,
37
41
  /INSERT\s[\s\S]*INTO\s[\s\S]*/i,
38
42
  /UPDATE\s[\s\S]*/i,
39
43
  /DELETE\s[\s\S]*FROM\s[\s\S]*/i,
@@ -50,13 +54,12 @@ module ActiveRecordProxyAdapters
50
54
  # @param primary_connection [ActiveRecord::ConnectionAdatpers::AbstractAdapter]
51
55
  def initialize(primary_connection)
52
56
  @primary_connection = primary_connection
53
- @last_write_at = 0
54
57
  @active_record_context = ActiveRecordContext.new
55
58
  end
56
59
 
57
60
  private
58
61
 
59
- attr_reader :primary_connection, :last_write_at, :active_record_context
62
+ attr_reader :primary_connection, :active_record_context
60
63
 
61
64
  delegate :connection_handler, to: :connection_class
62
65
  delegate :reading_role, :writing_role, to: :active_record_context
@@ -139,10 +142,6 @@ module ActiveRecordProxyAdapters
139
142
  [reading_role, writing_role].include?(role) ? role : nil
140
143
  end
141
144
 
142
- def cache_key_for(sql_string)
143
- cache_config.key_builder.call(sql_string).prepend(cache_config.key_prefix)
144
- end
145
-
146
145
  def connected_to_stack
147
146
  return connection_class.connected_to_stack if connection_class.respond_to?(:connected_to_stack)
148
147
 
@@ -220,11 +219,9 @@ module ActiveRecordProxyAdapters
220
219
  proc { |matcher| matcher.match?(sql_string) }
221
220
  end
222
221
 
223
- # TODO: implement a context API to re-route requests to the primary database if a recent query was sent to it to
224
- # avoid replication delay issues
225
222
  # @return Boolean
226
223
  def recent_write_to_primary?
227
- Concurrent.monotonic_time - last_write_at < proxy_delay
224
+ proxy_context.recent_write_to_primary?(primary_connection_name)
228
225
  end
229
226
 
230
227
  def in_transaction?
@@ -232,27 +229,15 @@ module ActiveRecordProxyAdapters
232
229
  end
233
230
 
234
231
  def update_primary_latest_write_timestamp
235
- @last_write_at = Concurrent.monotonic_time
236
- end
237
-
238
- def cache_store
239
- cache_config.store
240
- end
241
-
242
- def cache_config
243
- proxy_config.cache
244
- end
245
-
246
- def proxy_delay
247
- proxy_config.proxy_delay
232
+ proxy_context.update_for(primary_connection_name)
248
233
  end
249
234
 
250
- def checkout_timeout
251
- proxy_config.checkout_timeout
235
+ def primary_connection_name
236
+ @primary_connection_name ||= primary_connection.pool.try(:db_config).try(:name).try(:to_sym)
252
237
  end
253
238
 
254
- def proxy_config
255
- ActiveRecordProxyAdapters.config
239
+ def proxy_context
240
+ self.current_context ||= context_store.new({})
256
241
  end
257
242
  end
258
243
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordProxyAdapters
4
+ module TransactionableProxyAR70 # rubocop:disable Style/Documentation
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ def begin_db_transaction # :nodoc:
9
+ bypass_proxy_or_else("BEGIN", "TRANSACTION") { super }
10
+ end
11
+
12
+ def commit_db_transaction # :nodoc:
13
+ bypass_proxy_or_else("COMMIT", "TRANSACTION") { super }
14
+ end
15
+
16
+ def exec_rollback_db_transaction # :nodoc:
17
+ bypass_proxy_or_else("ROLLBACK", "TRANSACTION") { super }
18
+ end
19
+
20
+ private
21
+
22
+ def bypass_proxy_or_else(*args)
23
+ method_name = proxy_method_name_for(:execute)
24
+
25
+ return public_send(method_name, *args) if respond_to?(method_name)
26
+
27
+ yield
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordProxyAdapters
4
- VERSION = "0.6.0"
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.0
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Cruz
@@ -94,15 +94,19 @@ 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
103
106
  - lib/active_record_proxy_adapters/railtie.rb
104
107
  - lib/active_record_proxy_adapters/sqlite3_proxy.rb
105
108
  - lib/active_record_proxy_adapters/synchronizable_configuration.rb
109
+ - lib/active_record_proxy_adapters/transactionable_proxy_a_r_70.rb
106
110
  - lib/active_record_proxy_adapters/trilogy_proxy.rb
107
111
  - lib/active_record_proxy_adapters/version.rb
108
112
  homepage: https://github.com/Nasdaq/active_record_proxy_adapters