active_record_proxy_adapters 0.6.1 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d73a2213222b1689c5ff581f40e561974446e480355d582fca5daa04eccf9996
4
- data.tar.gz: ee667c46d62816df1dafbe583cbe0503448097f30ebf7c6d37565fd6cb335779
3
+ metadata.gz: eaa34c675630f13046dbc7170773fca75998eecef0b6e4990ce53110bdfdecef
4
+ data.tar.gz: 92c024345625535b4f3bfcb241b898e3a26c09836a5f3c6dac61749c913aef85
5
5
  SHA512:
6
- metadata.gz: a0c1db04e45da543ff86f31a37b45a5f3c5a3e31d0c877cd7e0038f46d01207bf90124868dda9a89ce73a0701726e9c539a422ef53a6e3a9cac9f2304e20a4de
7
- data.tar.gz: 1e7eeb8e61c753dd6ad8475a61c4625c90240cdf1d2cf544b40c215a34ec498df806e8403922728c579f5dc727ea5901ff9e81875a57a36fcb2f1fbb17ec4b08
6
+ metadata.gz: 76493c2922eb8f725410bc69223b9bc9dd121cece620e188a73b941ea7b8ef549108ffea8d3c1c38ddee2ed5686b6160284f87665ea431e41f4a32bccc80472a
7
+ data.tar.gz: 24c3cf701b469e364a5206ace80d3e8a609fddcd1839337069f0b0f7bbbf27f6c35940d7642a5f717b6030bf776afa988912fb8306b0e8b5d4d2e7fe5bde0ff4
data/README.md CHANGED
@@ -150,6 +150,25 @@ ActiveRecordProxyAdapters.configure do |config|
150
150
  end
151
151
  ```
152
152
 
153
+ ### Configuring multiple connections
154
+ General settings are automatically applied to the `primary` database by default.
155
+
156
+ If your application has multiple databases that need separate proxy settings, you can use the `database` block for individual database settings.
157
+
158
+ ```ruby
159
+ ActiveRecordProxyAdapters.configure do |config|
160
+ config.database :primary do |primary_config|
161
+ primary_config.proxy_delay = 2.seconds
162
+ end
163
+
164
+ config.database :secondary do |secondary_config|
165
+ secondary_config.proxy_delay = 5.seconds
166
+ end
167
+ end
168
+ ```
169
+
170
+ With those settings, any model that `connects_to database: { writing: :primary }` will have a 2-second delay, and any model that `connects_to database: { writing: :secondary }` will have a 5-second delay.
171
+
153
172
  ## Logging
154
173
 
155
174
  ```ruby
@@ -157,8 +176,8 @@ end
157
176
  require "active_record_proxy_adapters/log_subscriber"
158
177
 
159
178
  ActiveRecordProxyAdapters.configure do |config|
160
- config.log_subscriber_primary_prefix = "My primary tag" # defaults to "#{adapter_name} Primary", i.e "PostgreSQL Primary"
161
- config.log_subscriber_replica_prefix = "My replica tag" # defaults to "#{adapter_name} Replica", i.e "PostgreSQL Replica"
179
+ config.log_subscriber_primary_prefix = "My primary tag" # defaults to ActiveRecord configuration name", i.e "primary"
180
+ config.log_subscriber_replica_prefix = "My replica tag" # defaults to ActiveRecord configuration name", i.e "primary_replica"
162
181
  end
163
182
 
164
183
  # You may want to remove duplicate logs
@@ -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
@@ -7,12 +7,8 @@ module ActiveRecordProxyAdapters
7
7
  include SynchronizableConfiguration
8
8
 
9
9
  # Sets the cache store to use for caching SQL statements.
10
- #
11
- # @param store [ActiveSupport::Cache::Store] The cache store to use for caching SQL statements.
12
- # Defaults to ActiveSupport::Cache::NullStore, which does not cache anything.
13
- # Thread safe.
10
+ # Defaults to {ActiveSupport::Cache::NullStore}, which does not cache anything.
14
11
  # @return [ActiveSupport::Cache::Store] The cache store to use for caching SQL statements.
15
- # Defaults to ActiveSupport::Cache::NullStore, which does not cache anything.
16
12
  # Thread safe.
17
13
  attr_reader :store
18
14
 
@@ -51,5 +47,9 @@ module ActiveRecordProxyAdapters
51
47
  def bust
52
48
  store.delete_matched("#{key_prefix}*")
53
49
  end
50
+
51
+ private
52
+
53
+ attr_reader :lock
54
54
  end
55
55
  end
@@ -1,68 +1,66 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/integer/time"
4
- require "active_record_proxy_adapters/synchronizable_configuration"
5
3
  require "active_record_proxy_adapters/cache_configuration"
4
+ require "active_record_proxy_adapters/context"
5
+ require "active_record_proxy_adapters/database_configuration"
6
+ require "active_record_proxy_adapters/synchronizable_configuration"
7
+ require "active_support/core_ext/integer/time"
6
8
 
7
9
  module ActiveRecordProxyAdapters
8
10
  # Provides a global configuration object to configure how the proxy should behave.
9
11
  class Configuration
10
12
  include SynchronizableConfiguration
11
13
 
12
- PROXY_DELAY = 2.seconds.freeze
13
- CHECKOUT_TIMEOUT = 2.seconds.freeze
14
- LOG_SUBSCRIBER_PRIMARY_PREFIX = proc { |event| "#{event.payload[:connection].class::ADAPTER_NAME} Primary" }.freeze
15
- LOG_SUBSCRIBER_REPLICA_PREFIX = proc { |event| "#{event.payload[:connection].class::ADAPTER_NAME} Replica" }.freeze
16
-
17
- # @return [ActiveSupport::Duration] How long the proxy should reroute all read requests to the primary database
18
- # since the latest write. Defaults to PROXY_DELAY. Thread safe.
19
- attr_reader :proxy_delay
20
- # @return [ActiveSupport::Duration] How long the proxy should wait for a connection from the replica pool.
21
- # Defaults to CHECKOUT_TIMEOUT. Thread safe.
22
- attr_reader :checkout_timeout
23
-
24
- # @return [Proc] Prefix for the log subscriber when the primary database is used. Thread safe.
25
- attr_reader :log_subscriber_primary_prefix
14
+ DEFAULT_DATABASE_NAME = :primary
26
15
 
27
- # @return [Proc] Prefix for the log subscriber when the replica database is used. Thread safe.
28
- attr_reader :log_subscriber_replica_prefix
16
+ # @return [Class] The context that is used to store the current request's state.
17
+ attr_reader :context_store
29
18
 
30
19
  def initialize
31
20
  @lock = Monitor.new
32
21
 
33
- self.proxy_delay = PROXY_DELAY
34
- self.checkout_timeout = CHECKOUT_TIMEOUT
35
- self.log_subscriber_primary_prefix = LOG_SUBSCRIBER_PRIMARY_PREFIX
36
- self.log_subscriber_replica_prefix = LOG_SUBSCRIBER_REPLICA_PREFIX
37
- self.cache_configuration = CacheConfiguration.new(@lock)
22
+ self.cache_configuration = CacheConfiguration.new(@lock)
23
+ self.context_store = ActiveRecordProxyAdapters::Context
24
+ @database_configurations = {}
38
25
  end
39
26
 
40
27
  def log_subscriber_primary_prefix=(prefix)
41
- prefix_proc = prefix.is_a?(Proc) ? prefix : proc { prefix.to_s }
28
+ default_database_config.log_subscriber_primary_prefix = prefix
29
+ end
42
30
 
43
- synchronize_update(:log_subscriber_primary_prefix, from: @log_subscriber_primary_prefix, to: prefix_proc) do
44
- @log_subscriber_primary_prefix = prefix_proc
45
- end
31
+ def log_subscriber_primary_prefix
32
+ default_database_config.log_subscriber_primary_prefix
46
33
  end
47
34
 
48
35
  def log_subscriber_replica_prefix=(prefix)
49
- prefix_proc = prefix.is_a?(Proc) ? prefix : proc { prefix.to_s }
36
+ default_database_config.log_subscriber_replica_prefix = prefix
37
+ end
50
38
 
51
- synchronize_update(:log_subscriber_replica_prefix, from: @log_subscriber_replica_prefix, to: prefix_proc) do
52
- @log_subscriber_replica_prefix = prefix_proc
53
- end
39
+ def log_subscriber_replica_prefix
40
+ default_database_config.log_subscriber_replica_prefix
41
+ end
42
+
43
+ def proxy_delay
44
+ default_database_config.proxy_delay
54
45
  end
55
46
 
56
47
  def proxy_delay=(proxy_delay)
57
- synchronize_update(:proxy_delay, from: @proxy_delay, to: proxy_delay) do
58
- @proxy_delay = proxy_delay
59
- end
48
+ default_database_config.proxy_delay = proxy_delay
49
+ end
50
+
51
+ def checkout_timeout
52
+ default_database_config.checkout_timeout
60
53
  end
61
54
 
62
55
  def checkout_timeout=(checkout_timeout)
63
- synchronize_update(:checkout_timeout, from: @checkout_timeout, to: checkout_timeout) do
64
- @checkout_timeout = checkout_timeout
65
- end
56
+ default_database_config.checkout_timeout = checkout_timeout
57
+ end
58
+
59
+ def database(database_name)
60
+ key = database_name.to_s
61
+ lock.synchronize { @database_configurations[key] ||= DatabaseConfiguration.new }
62
+
63
+ block_given? ? yield(database_configurations[key]) : database_configurations[key]
66
64
  end
67
65
 
68
66
  def cache
@@ -71,13 +69,22 @@ module ActiveRecordProxyAdapters
71
69
 
72
70
  private
73
71
 
74
- # @return [CacheConfiguration] The cache configuration for the proxy adapters.
75
- attr_reader :cache_configuration
72
+ attr_reader :cache_configuration, :database_configurations, :lock
73
+
74
+ def default_database_config
75
+ database(DEFAULT_DATABASE_NAME)
76
+ end
76
77
 
77
78
  def cache_configuration=(cache_configuration)
78
79
  synchronize_update(:cache_configuration, from: @cache_configuration, to: cache_configuration) do
79
80
  @cache_configuration = cache_configuration
80
81
  end
81
82
  end
83
+
84
+ def context_store=(context_store)
85
+ synchronize_update(:context_store, from: @context_store, to: context_store) do
86
+ @context_store = context_store
87
+ end
88
+ end
82
89
  end
83
90
  end
@@ -0,0 +1,46 @@
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.to_s] < proxy_delay(connection_name)
20
+ end
21
+
22
+ def update_for(connection_name)
23
+ self[connection_name.to_s] = now
24
+ end
25
+
26
+ def [](connection_name)
27
+ timestamp_registry[connection_name.to_s] || 0
28
+ end
29
+
30
+ def []=(connection_name, timestamp)
31
+ timestamp_registry[connection_name.to_s] = timestamp
32
+ end
33
+
34
+ def to_h
35
+ timestamp_registry.dup
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :timestamp_registry
41
+
42
+ def now
43
+ Time.now.utc.to_f
44
+ end
45
+ end
46
+ 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,74 @@
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
+ # Provides a global configuration object to configure how the proxy should behave.
10
+ class DatabaseConfiguration
11
+ include SynchronizableConfiguration
12
+
13
+ PROXY_DELAY = 2.seconds.freeze
14
+ CHECKOUT_TIMEOUT = 2.seconds.freeze
15
+ DEFAULT_PREFIX = proc do |event|
16
+ connection = event.payload[:connection]
17
+
18
+ connection.pool.try(:db_config).try(:name) || connection.class::ADAPTER_NAME
19
+ end
20
+
21
+ # @return [ActiveSupport::Duration] How long the proxy should reroute all read requests to the primary database
22
+ # since the latest write. Defaults to PROXY_DELAY. Thread safe.
23
+ attr_reader :proxy_delay
24
+ # @return [ActiveSupport::Duration] How long the proxy should wait for a connection from the replica pool.
25
+ # Defaults to CHECKOUT_TIMEOUT. Thread safe.
26
+ attr_reader :checkout_timeout
27
+
28
+ # @return [Proc] Prefix for the log subscriber when the primary database is used. Thread safe.
29
+ attr_reader :log_subscriber_primary_prefix
30
+
31
+ # @return [Proc] Prefix for the log subscriber when the replica database is used. Thread safe.
32
+ attr_reader :log_subscriber_replica_prefix
33
+
34
+ def initialize
35
+ @lock = Monitor.new
36
+ self.proxy_delay = PROXY_DELAY
37
+ self.checkout_timeout = CHECKOUT_TIMEOUT
38
+ self.log_subscriber_primary_prefix = DEFAULT_PREFIX
39
+ self.log_subscriber_replica_prefix = DEFAULT_PREFIX
40
+ end
41
+
42
+ def proxy_delay=(proxy_delay)
43
+ synchronize_update(:proxy_delay, from: @proxy_delay, to: proxy_delay) do
44
+ @proxy_delay = proxy_delay
45
+ end
46
+ end
47
+
48
+ def checkout_timeout=(checkout_timeout)
49
+ synchronize_update(:checkout_timeout, from: @checkout_timeout, to: checkout_timeout) do
50
+ @checkout_timeout = checkout_timeout
51
+ end
52
+ end
53
+
54
+ def log_subscriber_primary_prefix=(prefix)
55
+ prefix_proc = prefix.is_a?(Proc) ? prefix : proc { prefix.to_s }
56
+
57
+ synchronize_update(:log_subscriber_primary_prefix, from: @log_subscriber_primary_prefix, to: prefix_proc) do
58
+ @log_subscriber_primary_prefix = prefix_proc
59
+ end
60
+ end
61
+
62
+ def log_subscriber_replica_prefix=(prefix)
63
+ prefix_proc = prefix.is_a?(Proc) ? prefix : proc { prefix.to_s }
64
+
65
+ synchronize_update(:log_subscriber_replica_prefix, from: @log_subscriber_replica_prefix, to: prefix_proc) do
66
+ @log_subscriber_replica_prefix = prefix_proc
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ attr_reader :lock
73
+ end
74
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module ActiveRecordProxyAdapters
4
4
  class LogSubscriber < ActiveRecord::LogSubscriber # rubocop:disable Style/Documentation
5
+ include Mixin::Configuration
6
+
5
7
  attach_to :active_record
6
8
 
7
9
  IGNORE_PAYLOAD_NAMES = %w[SCHEMA EXPLAIN].freeze
@@ -19,23 +21,27 @@ module ActiveRecordProxyAdapters
19
21
  protected
20
22
 
21
23
  def database_instance_prefix_for(event)
22
- connection = event.payload[:connection]
23
- config = connection.instance_variable_get(:@config)
24
- prefix = if config[:replica] || config["replica"]
25
- log_subscriber_replica_prefix
24
+ connection = event.payload[:connection]
25
+ db_config = connection.pool.try(:db_config) || NullConfig.new # AR 7.0.x does not support "NullConfig"
26
+ connection_name = db_config.name
27
+
28
+ prefix = if db_config.replica?
29
+ log_subscriber_replica_prefix(connection_name)
26
30
  else
27
- log_subscriber_primary_prefix
31
+ log_subscriber_primary_prefix(connection_name)
28
32
  end
29
33
 
30
34
  "[#{prefix.call(event)}]"
31
35
  end
32
36
 
33
- private
34
-
35
- delegate :log_subscriber_primary_prefix, :log_subscriber_replica_prefix, to: :config
37
+ class NullConfig # rubocop:disable Style/Documentation
38
+ def method_missing(...)
39
+ nil
40
+ end
36
41
 
37
- def config
38
- ActiveRecordProxyAdapters.config
42
+ def respond_to_missing?(*)
43
+ true
44
+ end
39
45
  end
40
46
  end
41
47
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+ require "json"
5
+ require "active_record_proxy_adapters/context"
6
+ require "active_record_proxy_adapters/contextualizer"
7
+ require "active_record_proxy_adapters/mixin/configuration"
8
+
9
+ module ActiveRecordProxyAdapters
10
+ class Middleware # rubocop:disable Style/Documentation
11
+ include Contextualizer
12
+ include Mixin::Configuration
13
+
14
+ COOKIE_NAME = "arpa_context"
15
+ COOKIE_BUFFER = 5.seconds.freeze
16
+ DEFAULT_COOKIE_OPTIONS = {
17
+ path: "/",
18
+ http_only: true
19
+ }.freeze
20
+
21
+ class << self
22
+ include Mixin::Configuration
23
+ end
24
+
25
+ COOKIE_READER = lambda do |rack_env|
26
+ rack_request = Rack::Request.new(rack_env)
27
+ arpa_cookie = rack_request.cookies[COOKIE_NAME]
28
+ JSON.parse(arpa_cookie || "{}")
29
+ rescue JSON::ParserError
30
+ {}
31
+ end.freeze
32
+
33
+ COOKIE_WRITER = lambda do |headers, cookie_hash, options|
34
+ cookie = DEFAULT_COOKIE_OPTIONS.merge(options)
35
+ max_value = cookie_hash.values.max || 0
36
+ then_time = Time.at(max_value).utc
37
+ expires = then_time + proxy_delay(cookie_hash.key(max_value)) + COOKIE_BUFFER
38
+ max_age = expires - then_time
39
+ cookie[:expires] = expires
40
+ cookie[:max_age] = max_age
41
+ cookie[:value] = cookie_hash.to_json
42
+
43
+ Rack::Utils.set_cookie_header!(headers, COOKIE_NAME, cookie)
44
+ end.freeze
45
+
46
+ def initialize(app, cookie_options = {})
47
+ @app = app
48
+ @cookie_options = cookie_options
49
+ end
50
+
51
+ def call(env)
52
+ return @app.call(env) if ignore_request?(env)
53
+
54
+ self.current_context = context_store.new(COOKIE_READER.call(env))
55
+
56
+ status, headers, body = @app.call(env)
57
+
58
+ update_cookie_from_context(headers)
59
+
60
+ [status, headers, body]
61
+ end
62
+
63
+ private
64
+
65
+ def update_cookie_from_context(headers)
66
+ COOKIE_WRITER.call(headers, current_context.to_h, @cookie_options)
67
+ end
68
+
69
+ def ignore_request?(env)
70
+ return false unless defined?(Rails)
71
+ return false unless asset_prefix
72
+
73
+ /^#{asset_prefix}/.match?(env["PATH_INFO"].to_s)
74
+ end
75
+
76
+ def asset_prefix
77
+ Rails.try(:application).try(:config).try(:assets).try(:prefix)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,82 @@
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::DatabaseConfiguration#log_subscriber_primary_prefix}.
14
+ # @param database_name [Symbol, String] The name of the database to retrieve the prefix.
15
+ # @return [Proc]
16
+ def log_subscriber_primary_prefix(database_name)
17
+ database_config(database_name).log_subscriber_primary_prefix
18
+ end
19
+
20
+ # Helper to retrieve the proxy delay from the configuration stored in
21
+ # {ActiveRecordProxyAdapters::DatabaseConfiguration#log_subscriber_replica_prefix}.
22
+ # @param database_name [Symbol, String] The name of the database to retrieve the prefix.
23
+ # @return [Proc]
24
+ def log_subscriber_replica_prefix(database_name)
25
+ database_config(database_name).log_subscriber_replica_prefix
26
+ end
27
+
28
+ # Helper to retrieve the proxy delay from the configuration stored in
29
+ # {ActiveRecordProxyAdapters::DatabaseConfiguration#proxy_delay}.
30
+ # @param database_name [Symbol, String] The name of the database to retrieve the proxy delay for.
31
+ # @return [ActiveSupport::Duration]
32
+ def proxy_delay(database_name)
33
+ database_config(database_name).proxy_delay
34
+ end
35
+
36
+ # Helper to retrieve the checkout timeout from the configuration stored in
37
+ # {ActiveRecordProxyAdapters::DatabaseConfiguration#checkout_timeout}.
38
+ # @param database_name [Symbol, String] The name of the database to retrieve the checkout timeout for.
39
+ # @return [ActiveSupport::Duration]
40
+ def checkout_timeout(database_name)
41
+ database_config(database_name).checkout_timeout
42
+ end
43
+
44
+ # Helper to retrieve the context store class from the configuration stored in
45
+ # {ActiveRecordProxyAdapters::Configuration#context_store}.
46
+ # @return [Class]
47
+ def context_store
48
+ proxy_config.context_store
49
+ end
50
+
51
+ # Helper to retrieve the cache store from the configuration stored in
52
+ # {ActiveRecordProxyAdapters::CacheConfiguration#store}.
53
+ # @return [ActiveSupport::Cache::Store]
54
+ def cache_store
55
+ cache_config.store
56
+ end
57
+
58
+ # Helper to retrieve the cache key prefix from the configuration stored in
59
+ # {ActiveRecordProxyAdapters::CacheConfiguration#key_prefix}.
60
+ # It uses the key builder to generate a cache key for the given SQL string, prepended with the key prefix.
61
+ # @return [String]
62
+ def cache_key_for(sql_string)
63
+ cache_config.key_builder.call(sql_string).prepend(cache_config.key_prefix)
64
+ end
65
+
66
+ # @!visibility private
67
+ def cache_config
68
+ proxy_config.cache
69
+ end
70
+
71
+ # @!visibility private
72
+ def database_config(database_name)
73
+ proxy_config.database(database_name)
74
+ end
75
+
76
+ # @!visibility private
77
+ def proxy_config
78
+ ActiveRecordProxyAdapters.config
79
+ end
80
+ end
81
+ end
82
+ 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
 
@@ -183,7 +181,7 @@ module ActiveRecordProxyAdapters
183
181
  end
184
182
 
185
183
  def checkout_replica_connection
186
- replica_pool.checkout(checkout_timeout)
184
+ replica_pool.checkout(checkout_timeout(primary_connection_name))
187
185
  # rescue NoDatabaseError to avoid crashing when running db:create rake task
188
186
  # rescue ConnectionNotEstablished to handle connectivity issues in the replica
189
187
  # (for example, replication delay)
@@ -191,7 +189,6 @@ module ActiveRecordProxyAdapters
191
189
  primary_connection
192
190
  end
193
191
 
194
- # @return [TrueClass] if there has been a write within the last {#proxy_delay} seconds
195
192
  # @return [TrueClass] if sql_string matches a write statement (i.e. INSERT, UPDATE, DELETE, SELECT FOR UPDATE)
196
193
  # @return [FalseClass] if sql_string matches a read statement (i.e. SELECT)
197
194
  def need_primary?(sql_string)
@@ -221,11 +218,9 @@ module ActiveRecordProxyAdapters
221
218
  proc { |matcher| matcher.match?(sql_string) }
222
219
  end
223
220
 
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
221
  # @return Boolean
227
222
  def recent_write_to_primary?
228
- Concurrent.monotonic_time - last_write_at < proxy_delay
223
+ proxy_context.recent_write_to_primary?(primary_connection_name)
229
224
  end
230
225
 
231
226
  def in_transaction?
@@ -233,27 +228,15 @@ module ActiveRecordProxyAdapters
233
228
  end
234
229
 
235
230
  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
231
+ proxy_context.update_for(primary_connection_name)
249
232
  end
250
233
 
251
- def checkout_timeout
252
- proxy_config.checkout_timeout
234
+ def primary_connection_name
235
+ @primary_connection_name ||= primary_connection.pool.try(:db_config).try(:name).try(:to_s)
253
236
  end
254
237
 
255
- def proxy_config
256
- ActiveRecordProxyAdapters.config
238
+ def proxy_context
239
+ self.current_context ||= context_store.new({})
257
240
  end
258
241
  end
259
242
  end
@@ -6,6 +6,7 @@ module ActiveRecordProxyAdapters
6
6
  # Hooks into rails boot process to extend ActiveRecord with the proxy adapter.
7
7
  class Railtie < Rails::Railtie
8
8
  require "active_record_proxy_adapters/connection_handling"
9
+ require "active_record_proxy_adapters/middleware"
9
10
 
10
11
  config.to_prepare do
11
12
  Rails.autoloaders.each do |autoloader|
@@ -17,5 +18,9 @@ module ActiveRecordProxyAdapters
17
18
  )
18
19
  end
19
20
  end
21
+
22
+ initializer "active_record_proxy_adapters.configure_rails_initialization" do |app|
23
+ app.middleware.use ActiveRecordProxyAdapters::Middleware
24
+ end
20
25
  end
21
26
  end
@@ -7,8 +7,6 @@ module ActiveRecordProxyAdapters
7
7
  included do
8
8
  private
9
9
 
10
- attr_reader :lock
11
-
12
10
  def synchronize_update(attribute, from:, to:, &block)
13
11
  ActiveSupport::Notifications.instrument(
14
12
  "active_record_proxy_adapters.configuration_update",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordProxyAdapters
4
- VERSION = "0.6.1"
4
+ VERSION = "0.7.0"
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.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Cruz
@@ -63,6 +63,20 @@ dependencies:
63
63
  - - ">="
64
64
  - !ruby/object:Gem::Version
65
65
  version: 3.1.0
66
+ - !ruby/object:Gem::Dependency
67
+ name: json
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ type: :runtime
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
66
80
  description: |-
67
81
  This gem allows automatic connection switching between a primary and one read replica database in ActiveRecord.
68
82
  It pattern matches the SQL statement being sent to decide whether it should go to the replica (SELECT) or the
@@ -94,9 +108,14 @@ files:
94
108
  - lib/active_record_proxy_adapters/connection_handling/postgresql.rb
95
109
  - lib/active_record_proxy_adapters/connection_handling/sqlite3.rb
96
110
  - lib/active_record_proxy_adapters/connection_handling/trilogy.rb
111
+ - lib/active_record_proxy_adapters/context.rb
112
+ - lib/active_record_proxy_adapters/contextualizer.rb
113
+ - lib/active_record_proxy_adapters/database_configuration.rb
97
114
  - lib/active_record_proxy_adapters/database_tasks.rb
98
115
  - lib/active_record_proxy_adapters/hijackable.rb
99
116
  - lib/active_record_proxy_adapters/log_subscriber.rb
117
+ - lib/active_record_proxy_adapters/middleware.rb
118
+ - lib/active_record_proxy_adapters/mixin/configuration.rb
100
119
  - lib/active_record_proxy_adapters/mysql2_proxy.rb
101
120
  - lib/active_record_proxy_adapters/postgresql_proxy.rb
102
121
  - lib/active_record_proxy_adapters/primary_replica_proxy.rb