active_record_proxy_adapters 0.5.0 → 0.6.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: b25888bc158e220dfb06b349f47c467ef04dee2889a3e7ad6624ead7f2a6ed29
4
- data.tar.gz: e0caf99eb8ae9e535a5c68a2c4a1a74471d67d023abc6afb6b1ebd5e2ade1f75
3
+ metadata.gz: 7884786c370072543b8702a379b372ffa878e281e88e8e96ba603cec78a01523
4
+ data.tar.gz: 80141eaa2a1e1bbd9c4eb985136140acf006c16e5749497a114574f809325e98
5
5
  SHA512:
6
- metadata.gz: cbff3cd696e3e52da998dbd44d1505f207f5bd81a1fc014eb6d95d46237ed49d4793c4084ecf34c761754ffabf5372202036ec33e427b80543f085d9c03a90c9
7
- data.tar.gz: ae8121a5a61036bce0433d040e3b27bf130fae80084f08379108943afa369101cf3cb17e7b3ea14c3770f03b9e03e30098d250a6535646cda41c5ac89e16da61
6
+ metadata.gz: 7f89bd7a1edf1e8813a9b70610c0153603958b64dbcecd5a00cc506301819e774b45ff291b06cfe01d8f25d2db6763853cb2bac4462d98836c33fbada30231b5
7
+ data.tar.gz: b7db70493adab591fd31c4294a630ff976e7cefac58a2e466f2e5cb6511102b1900054fbbc6d632a59cac8875f1f047d4ad0c78a5385f669987ec48bc8420651
data/README.md CHANGED
@@ -40,9 +40,7 @@ Currently supported adapters:
40
40
  - `postgresql`
41
41
  - `mysql2`
42
42
  - `trilogy`
43
-
44
- Coming soon:
45
- - `sqlite`
43
+ - `sqlite3`
46
44
 
47
45
 
48
46
  #### PostgreSQL
@@ -73,6 +71,34 @@ development:
73
71
  # your replica credentials here
74
72
  ```
75
73
 
74
+ #### Trilogy
75
+ ```yaml
76
+ # config/database.yml
77
+ development:
78
+ primary:
79
+ adapter: trilogy_proxy
80
+ # your primary credentials here
81
+
82
+ primary_replica:
83
+ adapter: trilogy
84
+ replica: true
85
+ # your replica credentials here
86
+ ```
87
+
88
+ #### SQLite
89
+ ```yaml
90
+ # config/database.yml
91
+ development:
92
+ primary:
93
+ adapter: sqlite3_proxy
94
+ # your primary credentials here
95
+
96
+ primary_replica:
97
+ adapter: sqlite3
98
+ replica: true
99
+ # your replica credentials here
100
+ ```
101
+
76
102
  ```ruby
77
103
  # app/models/application_record.rb
78
104
  class ApplicationRecord < ActiveRecord::Base
@@ -93,7 +119,7 @@ require "active_record_proxy_adapters/connection_handling"
93
119
  class ApplicationRecord << ActiveRecord::Base
94
120
  establish_connection(
95
121
  {
96
- adapter: 'postgresql_proxy',
122
+ adapter: 'postgresql_proxy', # or any of the following: mysql2_proxy, trilogy_proxy, sqlite3_proxy
97
123
  # your primary credentials here
98
124
  },
99
125
  role: :writing
@@ -101,7 +127,7 @@ class ApplicationRecord << ActiveRecord::Base
101
127
 
102
128
  establish_connection(
103
129
  {
104
- adapter: 'postgresql',
130
+ adapter: 'postgresql', # or any of the following: mysql2, trilogy, sqlite3
105
131
  # your replica credentials here
106
132
  },
107
133
  role: :reading
@@ -195,7 +221,7 @@ production:
195
221
  Then set `PRIMARY_DATABASE_ADAPTER=postgresql_proxy` to enable the proxy.
196
222
  That way you can redeploy your application disabling the proxy completely, without any code change.
197
223
 
198
- ### Sticking to the primary database manually
224
+ ### Sticking to a database manually
199
225
 
200
226
  The proxy respects ActiveRecord's `#connected_to_stack` and will use it if present.
201
227
  You can use that to force connection to the primary or replica and bypass the proxy entirely.
@@ -240,6 +266,84 @@ class SayHelloJob < ApplicationJob
240
266
  end
241
267
  ```
242
268
 
269
+ ## Caching Configuration
270
+
271
+ ActiveRecordProxyAdapters supports caching of SQL pattern matching results to improve performance for frequently executed queries.
272
+
273
+ ### Enabling Caching
274
+
275
+ By default, caching is disabled (using `NullStore`). To enable caching:
276
+
277
+ ```ruby
278
+ ActiveRecordProxyAdapters.configure do |config|
279
+ # Configure the cache store
280
+ config.cache do |cache|
281
+ # Use a specific cache implementation
282
+ # Notice that if using a Memcached or a Redis store, the network latency may outweigh the benefits you would get from caching the pattern matching
283
+ cache.store = ActiveSupport::Cache::MemoryStore.new(size: 64.megabytes)
284
+
285
+ # Optional: Customize the cache key prefix (default: "arpa_")
286
+ cache.key_prefix = "custom_prefix_"
287
+
288
+ # Optional: Customize the cache key generation (default: SHA2 hexdigest)
289
+ cache.key_builder = ->(sql) { "sql_#{Digest::MD5.hexdigest(sql)}" }
290
+ end
291
+ end
292
+ ```
293
+
294
+ ### How Caching Works
295
+ The caching system stores the results of SQL pattern matching operations to determine whether a query should be routed to a primary or replica database. This improves performance by avoiding repeated pattern matching on identical SQL strings.
296
+
297
+ - Cache keys are generated using the configured `key_builder` (SHA2 digest by default).
298
+ - All keys are prefixed with the configured `key_prefix` ("arpa_" by default).
299
+ - Cache misses are instrumented with the `active_record_proxy_adapters.cache_miss` notification. They can be monitored by subscribing to that topic:
300
+ ```ruby
301
+ ActiveSupport::Notifications.subscribe("active_record_proxy_adapters.cache_miss") do |event|
302
+ cache_key, sql = event[:payload].values_at(:cache_key, :sql)
303
+
304
+ logger.info("Cache miss for SQL: #{sql.inspect} with cache key: #{cache_key.inspec}")
305
+ end
306
+ ```
307
+
308
+ ### Busting the cache
309
+
310
+ If you ever need to manually clear the cached SQL patterns:
311
+
312
+ ```ruby
313
+ # This will clear all cached entries with the configured prefix
314
+ ActiveRecordProxyAdapters.bust_query_cache
315
+ ```
316
+
317
+ ### Performance Considerations
318
+ For applications with a high volume of repetitive queries, enabling caching can significantly reduce CPU overhead from SQL parsing. However, this comes with the tradeoff of increased memory usage in your cache store.
319
+
320
+ For optimal results:
321
+ - Consider enabling prepared statements as that will increase cache hit rate, and decrease cache growth rate
322
+ ```ruby
323
+ irb(main):001> (1..10).each { |i| User.where(id: i).exists? }
324
+ ```
325
+ _Without_ Prepared statements yields
326
+ ```
327
+ Cache miss for SQL: "SELECT 1 AS one FROM \"users\" WHERE \"users\".\"id\" = 1 LIMIT 1" with cache key: "arpa_9fa3972e45b27985eef6bfb4aa6269c12d43363c60e7aa67fb290ec317503710"
328
+ Cache miss for SQL: "SELECT 1 AS one FROM \"users\" WHERE \"users\".\"id\" = 2 LIMIT 1" with cache key: "arpa_0e51756270138442ad26087dffcfb53c21df4a430961f1ca3b4270183f4b066d"
329
+ Cache miss for SQL: "SELECT 1 AS one FROM \"users\" WHERE \"users\".\"id\" = 3 LIMIT 1" with cache key: "arpa_db5b8c323ee2c284ba96adc6e20b7ea1373ca07fa9b09969f5207d467bd895b6"
330
+ Cache miss for SQL: "SELECT 1 AS one FROM \"users\" WHERE \"users\".\"id\" = 4 LIMIT 1" with cache key: "arpa_129459a1ba342cad3dbd4458cd8eacda4ed641a94a5d1e6cc23604495e44b565"
331
+ Cache miss for SQL: "SELECT 1 AS one FROM \"users\" WHERE \"users\".\"id\" = 5 LIMIT 1" with cache key: "arpa_9817a74a6f162ea110ed14cef79e95aa78830ff19266cdce75668e0c9c5ccef7"
332
+ Cache miss for SQL: "SELECT 1 AS one FROM \"users\" WHERE \"users\".\"id\" = 6 LIMIT 1" with cache key: "arpa_610e37a117abc81ec1afebafa0f36b35547f57879536ca7535475075ea08d8ac"
333
+ Cache miss for SQL: "SELECT 1 AS one FROM \"users\" WHERE \"users\".\"id\" = 7 LIMIT 1" with cache key: "arpa_79e172d168c59c4e5befbe954861ff9076000f955719dd3cca1423b68fb5f319"
334
+ Cache miss for SQL: "SELECT 1 AS one FROM \"users\" WHERE \"users\".\"id\" = 8 LIMIT 1" with cache key: "arpa_fb29367bdae3e2a48d1fa63cca00fd611c0b6dc84c9f5fd985b9222d49f1f7d9"
335
+ Cache miss for SQL: "SELECT 1 AS one FROM \"users\" WHERE \"users\".\"id\" = 9 LIMIT 1" with cache key: "arpa_e6e9a73cf9066077893b21dde038e5a616bf25731aeb5a4a9cdb41b7d84d1ece"
336
+ Cache miss for SQL: "SELECT 1 AS one FROM \"users\" WHERE \"users\".\"id\" = 10 LIMIT 1" with cache key: "arpa_8c94cdae65b6d529364d6ae8cf68f0e827566d471d2bd1107d6bdca29345759e"
337
+ ```
338
+
339
+ _With_ prepared statments yields
340
+ ```
341
+ Cache miss for SQL: "SELECT 1 AS one FROM \"users\" WHERE \"users\".\"id\" = $1 LIMIT $2" with cache key: "arpa_3c2ef2bb9a5f370adf63eac3bc9994c054554798d31d247818049c8c21cb68be"
342
+ ```
343
+ - Use a cache store with an appropriate size limit, and low latency (Memory Store has lower latency than Memcached or Redis)
344
+ - Monitor cache hit/miss rates using the instrumentation events
345
+ - Consider occasionally busting the cache during low-traffic periods to prevent stale entries, or setting a reasonable expiry window for cached values
346
+
243
347
  ### Thread safety
244
348
 
245
349
  Since Rails already leases exactly one connection per thread from the pool and the adapter operates on that premise, it is safe to use it in multi-threaded servers such as Puma.
@@ -337,7 +441,173 @@ irb(main):051:0> test_multithread_queries
337
441
 
338
442
  ## Building your own proxy
339
443
 
340
- TODO: update instructions
444
+ These instructions assume an active record adapter `ActiveRecord::ConnectionAdapters::FoobarAdapter` already exists and is properly loaded in your environment.
445
+
446
+ To create a proxy adapter for an existing database `FoobarAdapter`, follow these steps under the lib folder of your rails application source code:
447
+
448
+ 1. **Create database tasks for your proxy adapter** to allow Rails tasks like `db:create` and `db:migrate` to work:
449
+
450
+ ```ruby
451
+ # lib/active_record/tasks/foobar_proxy_database_tasks.rb
452
+
453
+ require "active_record_proxy_adapters/database_tasks"
454
+
455
+ module ActiveRecord
456
+ module Tasks
457
+ class FoobarProxyDatabaseTasks < FoobarDatabaseTasks
458
+ include ActiveRecordProxyAdapters::DatabaseTasks
459
+ end
460
+ end
461
+ end
462
+
463
+ ActiveRecord::Tasks::DatabaseTasks.register_task(
464
+ /foobar_proxy/,
465
+ "ActiveRecord::Tasks::FoobarProxyDatabaseTasks"
466
+ )
467
+ ```
468
+
469
+ 2. **Create the proxy implementation class** that will handle the routing logic:
470
+
471
+ ```ruby
472
+ # lib/active_record_proxy_adapters/foobar_proxy.rb
473
+
474
+ require "active_record_proxy_adapters/primary_replica_proxy"
475
+
476
+ module ActiveRecordProxyAdapters
477
+ class FoobarProxy < PrimaryReplicaProxy
478
+ # Override or hijack extra methods here if you need custom behavior
479
+ # For most adapters, the default behavior works fine
480
+ end
481
+ end
482
+ ```
483
+
484
+ 3. **Create the proxy adapter class** that inherits from the underlying adapter, including the `Hijackable` concern. You need to require the database tasks source, the original adapter source, and the proxy source:
485
+
486
+ ```ruby
487
+ # lib/active_record/connection_adapters/foobar_proxy_adapter.rb
488
+
489
+ require "active_record/tasks/foobar_proxy_database_tasks"
490
+ require "active_record/connection_adapters/foobar_adapter"
491
+ require "active_record_proxy_adapters/active_record_context"
492
+ require "active_record_proxy_adapters/hijackable"
493
+ require "active_record_proxy_adapters/foobar_proxy"
494
+
495
+ module ActiveRecord
496
+ module ConnectionAdapters
497
+ class FoobarProxyAdapter < FoobarAdapter
498
+ include ActiveRecordProxyAdapters::Hijackable
499
+
500
+ ADAPTER_NAME = "FoobarProxy" # This is only an ActiveRecord convention and is not required to work
501
+
502
+ delegate_to_proxy(*ActiveRecordProxyAdapters::ActiveRecordContext.hijackable_methods)
503
+
504
+ def initialize(...)
505
+ @proxy = ActiveRecordProxyAdapters::FoobarProxy.new(self)
506
+
507
+ super
508
+ end
509
+
510
+ private
511
+
512
+ attr_reader :proxy
513
+ end
514
+ end
515
+ end
516
+
517
+ # This is only required for Rails 7.2 or greater.
518
+ if ActiveRecordProxyAdapters::ActiveRecordContext.active_record_v7_2_or_greater?
519
+ ActiveRecord::ConnectionAdapters.register(
520
+ "foobar_proxy",
521
+ "ActiveRecord::ConnectionAdapters::FoobarProxyAdapter",
522
+ "active_record/connection_adapters/foobar_proxy_adapter"
523
+ )
524
+ end
525
+
526
+ ActiveSupport.run_load_hooks(:active_record_foobarproxyadapter,
527
+ ActiveRecord::ConnectionAdapters::FoobarProxyAdapter)
528
+ ```
529
+
530
+ 4. **Create connection handling module** for ActiveRecord integration:
531
+
532
+ ```ruby
533
+ # lib/active_record_proxy_adapters/connection_handling/foobar.rb
534
+
535
+ begin
536
+ require "active_record/connection_adapters/foobar_proxy_adapter"
537
+ rescue LoadError
538
+ # foobar not available
539
+ return
540
+ end
541
+
542
+ # This is only required for Rails 7.0 or earlier.
543
+ module ActiveRecordProxyAdapters
544
+ module Foobar
545
+ module ConnectionHandling
546
+ def foobar_proxy_adapter_class
547
+ ActiveRecord::ConnectionAdapters::FoobarProxyAdapter
548
+ end
549
+
550
+ def foobar_proxy_connection(config)
551
+ # copy and paste the contents of the original method foobar_connection here.
552
+ # If the contents contain a hardcoded FooBarAdapter.new instance,
553
+ # replace it with foobar_proxy_adapter_class.new
554
+ end
555
+ end
556
+ end
557
+ end
558
+
559
+ ActiveSupport.on_load(:active_record) do
560
+ ActiveRecord::Base.extend(ActiveRecordProxyAdapters::Foobar::ConnectionHandling)
561
+ end
562
+ ```
563
+
564
+ 5. **In your initializer, load the custom adapter** when the parent adapter is fully loaded:
565
+
566
+ ```ruby
567
+ # config/initializers/active_record_proxy_adapters.rb
568
+
569
+ # The parent adapter should have a load hook already. If not, you might need to monkey patch it.
570
+ ActiveSupport.on_load(:active_record_foobaradapter) do
571
+ require "active_record_proxy_adapters/connection_handling/foobar"
572
+ end
573
+ ```
574
+
575
+ 6. **Add a custom Zeitwerk inflection rule** if your adapter file paths do not follow Rails conventions. You can skip this if it does:
576
+
577
+ ```ruby
578
+ # config/initializers/active_record_proxy_adapters.rb
579
+
580
+ Rails.autoloaders.each do |autoloader|
581
+ autoloader.inflector.inflect(
582
+ "foobar_proxy_adapter" => "FoobarProxyAdapter"
583
+ )
584
+ end
585
+ ```
586
+
587
+ 7. **Configure your database.yml** to use your new adapter:
588
+
589
+ ```yaml
590
+ development:
591
+ primary:
592
+ adapter: foobar_proxy
593
+ # primary database configuration
594
+
595
+ primary_replica:
596
+ adapter: foobar
597
+ replica: true
598
+ # replica database configuration
599
+ ```
600
+
601
+ 8. **Set up your model to use both connections**:
602
+
603
+ ```ruby
604
+ class ApplicationRecord < ActiveRecord::Base
605
+ self.abstract_class = true
606
+ connects_to database: { writing: :primary, reading: :primary_replica }
607
+ end
608
+ ```
609
+
610
+ For testing your adapter, follow the examples in the test suite by creating spec files that match the pattern used for the other adapters.
341
611
 
342
612
  ## Development
343
613
 
@@ -12,6 +12,7 @@ module ActiveRecordProxyAdapters
12
12
  delegate_missing_to :new
13
13
  end
14
14
 
15
+ # rubocop:disable Naming/PredicateMethod
15
16
  NullConnectionHandlingContext = Class.new do
16
17
  def legacy_connection_handling
17
18
  false
@@ -21,6 +22,7 @@ module ActiveRecordProxyAdapters
21
22
  nil
22
23
  end
23
24
  end
25
+ # rubocop:enable Naming/PredicateMethod
24
26
 
25
27
  def connection_class_for(connection)
26
28
  return connection.connection_descriptor.name.constantize if active_record_v8_0_2_or_greater?
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record_proxy_adapters/synchronizable_configuration"
4
+
5
+ module ActiveRecordProxyAdapters
6
+ class CacheConfiguration # rubocop:disable Style/Documentation
7
+ include SynchronizableConfiguration
8
+
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.
14
+ # @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
+ # Thread safe.
17
+ attr_reader :store
18
+
19
+ # @return [String] The prefix to use for cache keys. Defaults to "arpa_".
20
+ attr_reader :key_prefix
21
+
22
+ # @return [Proc] A proc that takes an SQL statement and returns a cache key.
23
+ # Defaults to a SHA2 hexdigest of the SQL statement.
24
+ attr_reader :key_builder
25
+
26
+ def initialize(lock = Monitor.new)
27
+ @lock = lock
28
+ self.store = ActiveSupport::Cache::NullStore.new
29
+ self.key_prefix = "arpa_"
30
+ self.key_builder = ->(sql) { Digest::SHA2.hexdigest(sql) }
31
+ end
32
+
33
+ def store=(store)
34
+ synchronize_update(:"cache.store", from: @store, to: store) do
35
+ @store = store
36
+ end
37
+ end
38
+
39
+ def key_prefix=(key_prefix)
40
+ synchronize_update(:"cache.key_prefix", from: @key_prefix, to: key_prefix) do
41
+ @key_prefix = key_prefix
42
+ end
43
+ end
44
+
45
+ def key_builder=(key_builder)
46
+ synchronize_update(:"cache.key_builder", from: @key_builder, to: key_builder) do
47
+ @key_builder = key_builder
48
+ end
49
+ end
50
+
51
+ def bust
52
+ store.delete_matched("#{key_prefix}*")
53
+ end
54
+ end
55
+ end
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/integer/time"
4
+ require "active_record_proxy_adapters/synchronizable_configuration"
5
+ require "active_record_proxy_adapters/cache_configuration"
4
6
 
5
7
  module ActiveRecordProxyAdapters
6
8
  # Provides a global configuration object to configure how the proxy should behave.
7
9
  class Configuration
10
+ include SynchronizableConfiguration
11
+
8
12
  PROXY_DELAY = 2.seconds.freeze
9
13
  CHECKOUT_TIMEOUT = 2.seconds.freeze
10
14
  LOG_SUBSCRIBER_PRIMARY_PREFIX = proc { |event| "#{event.payload[:connection].class::ADAPTER_NAME} Primary" }.freeze
@@ -30,6 +34,7 @@ module ActiveRecordProxyAdapters
30
34
  self.checkout_timeout = CHECKOUT_TIMEOUT
31
35
  self.log_subscriber_primary_prefix = LOG_SUBSCRIBER_PRIMARY_PREFIX
32
36
  self.log_subscriber_replica_prefix = LOG_SUBSCRIBER_REPLICA_PREFIX
37
+ self.cache_configuration = CacheConfiguration.new(@lock)
33
38
  end
34
39
 
35
40
  def log_subscriber_primary_prefix=(prefix)
@@ -60,17 +65,18 @@ module ActiveRecordProxyAdapters
60
65
  end
61
66
  end
62
67
 
68
+ def cache
69
+ block_given? ? yield(cache_configuration) : cache_configuration
70
+ end
71
+
63
72
  private
64
73
 
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
+ # @return [CacheConfiguration] The cache configuration for the proxy adapters.
75
+ attr_reader :cache_configuration
76
+
77
+ def cache_configuration=(cache_configuration)
78
+ synchronize_update(:cache_configuration, from: @cache_configuration, to: cache_configuration) do
79
+ @cache_configuration = cache_configuration
74
80
  end
75
81
  end
76
82
  end
@@ -110,15 +110,22 @@ module ActiveRecordProxyAdapters
110
110
  end.last
111
111
  end
112
112
 
113
- def roles_for(sql_string)
113
+ def roles_for(sql_string) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
114
114
  return [top_of_connection_stack_role] if top_of_connection_stack_role.present?
115
-
116
- if need_all?(sql_string)
117
- [reading_role, writing_role]
118
- elsif need_primary?(sql_string)
119
- [writing_role]
120
- else
121
- [reading_role]
115
+ return [writing_role] if recent_write_to_primary? || in_transaction?
116
+
117
+ cache_key = cache_key_for(sql_string)
118
+ cache_store.fetch(cache_key) do
119
+ ActiveSupport::Notifications.instrument("active_record_proxy_adapters.cache_miss",
120
+ cache_key: cache_key, sql: sql_string) do
121
+ if need_all?(sql_string)
122
+ [reading_role, writing_role]
123
+ elsif need_primary?(sql_string)
124
+ [writing_role]
125
+ else
126
+ [reading_role]
127
+ end
128
+ end
122
129
  end
123
130
  end
124
131
 
@@ -132,6 +139,10 @@ module ActiveRecordProxyAdapters
132
139
  [reading_role, writing_role].include?(role) ? role : nil
133
140
  end
134
141
 
142
+ def cache_key_for(sql_string)
143
+ cache_config.key_builder.call(sql_string).prepend(cache_config.key_prefix)
144
+ end
145
+
135
146
  def connected_to_stack
136
147
  return connection_class.connected_to_stack if connection_class.respond_to?(:connected_to_stack)
137
148
 
@@ -182,9 +193,7 @@ module ActiveRecordProxyAdapters
182
193
  # @return [TrueClass] if there has been a write within the last {#proxy_delay} seconds
183
194
  # @return [TrueClass] if sql_string matches a write statement (i.e. INSERT, UPDATE, DELETE, SELECT FOR UPDATE)
184
195
  # @return [FalseClass] if sql_string matches a read statement (i.e. SELECT)
185
- def need_primary?(sql_string) # rubocop:disable Metrics/CyclomaticComplexity
186
- return true if recent_write_to_primary?
187
- return true if in_transaction?
196
+ def need_primary?(sql_string)
188
197
  return true if cte_for_write?(sql_string)
189
198
  return true if SQL_PRIMARY_MATCHERS.any?(&match_sql?(sql_string))
190
199
  return false if SQL_REPLICA_MATCHERS.any?(&match_sql?(sql_string))
@@ -226,6 +235,14 @@ module ActiveRecordProxyAdapters
226
235
  @last_write_at = Concurrent.monotonic_time
227
236
  end
228
237
 
238
+ def cache_store
239
+ cache_config.store
240
+ end
241
+
242
+ def cache_config
243
+ proxy_config.cache
244
+ end
245
+
229
246
  def proxy_delay
230
247
  proxy_config.proxy_delay
231
248
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordProxyAdapters
4
+ module SynchronizableConfiguration # rubocop:disable Style/Documentation
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ private
9
+
10
+ attr_reader :lock
11
+
12
+ def synchronize_update(attribute, from:, to:, &block)
13
+ ActiveSupport::Notifications.instrument(
14
+ "active_record_proxy_adapters.configuration_update",
15
+ attribute:,
16
+ who: Thread.current,
17
+ from:,
18
+ to:
19
+ ) do
20
+ lock.synchronize(&block)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordProxyAdapters
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -14,6 +14,10 @@ module ActiveRecordProxyAdapters
14
14
  yield(config)
15
15
  end
16
16
 
17
+ def bust_query_cache
18
+ config.cache.bust
19
+ end
20
+
17
21
  def config
18
22
  @config ||= Configuration.new
19
23
  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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Cruz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-05-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -49,6 +49,20 @@ dependencies:
49
49
  - - "<"
50
50
  - !ruby/object:Gem::Version
51
51
  version: '8.1'
52
+ - !ruby/object:Gem::Dependency
53
+ name: digest
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 3.1.0
59
+ type: :runtime
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: 3.1.0
52
66
  description: |-
53
67
  This gem allows automatic connection switching between a primary and one read replica database in ActiveRecord.
54
68
  It pattern matches the SQL statement being sent to decide whether it should go to the replica (SELECT) or the
@@ -58,8 +72,8 @@ email:
58
72
  executables: []
59
73
  extensions: []
60
74
  extra_rdoc_files:
61
- - README.md
62
75
  - LICENSE.txt
76
+ - README.md
63
77
  files:
64
78
  - LICENSE.txt
65
79
  - README.md
@@ -73,6 +87,7 @@ files:
73
87
  - lib/active_record/tasks/trilogy_proxy_database_tasks.rb
74
88
  - lib/active_record_proxy_adapters.rb
75
89
  - lib/active_record_proxy_adapters/active_record_context.rb
90
+ - lib/active_record_proxy_adapters/cache_configuration.rb
76
91
  - lib/active_record_proxy_adapters/configuration.rb
77
92
  - lib/active_record_proxy_adapters/connection_handling.rb
78
93
  - lib/active_record_proxy_adapters/connection_handling/mysql2.rb
@@ -87,6 +102,7 @@ files:
87
102
  - lib/active_record_proxy_adapters/primary_replica_proxy.rb
88
103
  - lib/active_record_proxy_adapters/railtie.rb
89
104
  - lib/active_record_proxy_adapters/sqlite3_proxy.rb
105
+ - lib/active_record_proxy_adapters/synchronizable_configuration.rb
90
106
  - lib/active_record_proxy_adapters/trilogy_proxy.rb
91
107
  - lib/active_record_proxy_adapters/version.rb
92
108
  homepage: https://github.com/Nasdaq/active_record_proxy_adapters
@@ -112,7 +128,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
128
  - !ruby/object:Gem::Version
113
129
  version: '0'
114
130
  requirements: []
115
- rubygems_version: 3.6.2
131
+ rubygems_version: 3.6.9
116
132
  specification_version: 4
117
133
  summary: Read replica proxy adapters for ActiveRecord!
118
134
  test_files: []