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 +4 -4
- data/README.md +21 -2
- data/lib/active_record_proxy_adapters/active_record_context.rb +1 -1
- data/lib/active_record_proxy_adapters/cache_configuration.rb +5 -5
- data/lib/active_record_proxy_adapters/configuration.rb +46 -39
- data/lib/active_record_proxy_adapters/context.rb +46 -0
- data/lib/active_record_proxy_adapters/contextualizer.rb +20 -0
- data/lib/active_record_proxy_adapters/database_configuration.rb +74 -0
- data/lib/active_record_proxy_adapters/log_subscriber.rb +16 -10
- data/lib/active_record_proxy_adapters/middleware.rb +80 -0
- data/lib/active_record_proxy_adapters/mixin/configuration.rb +82 -0
- data/lib/active_record_proxy_adapters/primary_replica_proxy.rb +14 -31
- data/lib/active_record_proxy_adapters/railtie.rb +5 -0
- data/lib/active_record_proxy_adapters/synchronizable_configuration.rb +0 -2
- data/lib/active_record_proxy_adapters/version.rb +1 -1
- metadata +20 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eaa34c675630f13046dbc7170773fca75998eecef0b6e4990ce53110bdfdecef
|
4
|
+
data.tar.gz: 92c024345625535b4f3bfcb241b898e3a26c09836a5f3c6dac61749c913aef85
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
161
|
-
config.log_subscriber_replica_prefix = "My replica tag" # defaults to
|
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
|
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
|
-
|
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 [
|
28
|
-
attr_reader :
|
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.
|
34
|
-
self.
|
35
|
-
|
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
|
-
|
28
|
+
default_database_config.log_subscriber_primary_prefix = prefix
|
29
|
+
end
|
42
30
|
|
43
|
-
|
44
|
-
|
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
|
-
|
36
|
+
default_database_config.log_subscriber_replica_prefix = prefix
|
37
|
+
end
|
50
38
|
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
75
|
-
|
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
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
37
|
+
class NullConfig # rubocop:disable Style/Documentation
|
38
|
+
def method_missing(...)
|
39
|
+
nil
|
40
|
+
end
|
36
41
|
|
37
|
-
|
38
|
-
|
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, :
|
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
|
-
|
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
|
-
|
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
|
252
|
-
|
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
|
256
|
-
|
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
|
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.
|
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
|