distribute_reads 0.4.0 → 1.0.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: 3b3c66d675a295bdde2d0dcc05590ed3af790a8a44718b2044fb6482c3330836
4
- data.tar.gz: 63db83095a8dcadfec723451c89afc9e3f712cef35a01d610b87f308febaa37c
3
+ metadata.gz: 88f83ede9ffd2ad33f91e493d48a6c1ebcc8fc18e6334e5b708345c0f0754d9a
4
+ data.tar.gz: 7d7bdef9c47ee1f8689c8956a7a7480e6b9a0f0bdf6adb8b997248a643d4fc1d
5
5
  SHA512:
6
- metadata.gz: 0ce34129b83a7e5e5511dd96e5590d6bf80333124290591e94fc2db6291b45979f5dfd4467d2f81c1be4040cab55e451db9c41e318a1aa3285b94802e52c0244
7
- data.tar.gz: 725b782d62ad8521407a1fd0b3c4a634e09147c83cb40f150cc04bc0ca64f1ac18565e9cfdb24966cf1296b1e4cd30388a8568ec83d84e5fccb1c1d2ce7e7ee7
6
+ metadata.gz: 0e74d909cd478d90c5c0750d46287d8c6e10d1e7cd8910e4a6e3f9398f92e36fd197bec9a6986c45c982cd5d3e730327d6d99366539c87dd99734715f44c448a
7
+ data.tar.gz: '08d34dd833a3625fd6065e26811b0290848db014be9b5224246fdb1642f4aae0ae8ae675d9ec860699c0c5f8cd60513d442efa2e0d7e456e88570fcaff2661fa'
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 1.0.0 (2025-09-23)
2
+
3
+ - Added support for ActiveRecordProxyAdapters
4
+ - Removed `default_to_primary` option (use `by_default` instead)
5
+ - Dropped support for Makara
6
+ - Dropped support for Ruby < 3.2 and Active Record < 7.1
7
+
8
+ ## 0.5.0 (2024-06-24)
9
+
10
+ - Dropped support for Makara 0.4
11
+ - Dropped support for Ruby < 3.1 and Active Record < 6.1
12
+
1
13
  ## 0.4.0 (2022-12-28)
2
14
 
3
15
  - Made `distribute_reads` method private to behave like `Kernel` methods
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2017-2022 Andrew Kane
1
+ Copyright (c) 2017-2025 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  Scale database reads to replicas in Rails
4
4
 
5
- :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
5
+ **Distribute Reads 1.0 was recently released** - see [how to upgrade](#upgrading)
6
6
 
7
- [![Build Status](https://github.com/ankane/distribute_reads/workflows/build/badge.svg?branch=master)](https://github.com/ankane/distribute_reads/actions)
7
+ :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource) with [Makara](https://github.com/instacart/makara)
8
+
9
+ [![Build Status](https://github.com/ankane/distribute_reads/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/distribute_reads/actions)
8
10
 
9
11
  ## Installation
10
12
 
@@ -16,19 +18,17 @@ gem "distribute_reads"
16
18
 
17
19
  ## How to Use
18
20
 
19
- [Makara](https://github.com/instacart/makara) does most of the work. First, update `database.yml` to use it:
21
+ [ActiveRecordProxyAdapters](https://github.com/Nasdaq/active_record_proxy_adapters) does most of the work. First, update `config/database.yml` to use it:
20
22
 
21
23
  ```yml
22
24
  default: &default
23
- url: postgresql-makara:///
24
- makara:
25
- sticky: true
26
- connections:
27
- - role: master
28
- name: primary
29
- url: <%= ENV["DATABASE_URL"] %>
30
- - name: replica
31
- url: <%= ENV["REPLICA_DATABASE_URL"] %>
25
+ primary:
26
+ adapter: postgresql_proxy
27
+ url: <%= ENV["DATABASE_URL"] %>
28
+ replica:
29
+ adapter: postgresql
30
+ url: <%= ENV["REPLICA_DATABASE_URL"] %>
31
+ replica: true
32
32
 
33
33
  development:
34
34
  <<: *default
@@ -39,6 +39,14 @@ production:
39
39
 
40
40
  **Note:** You can use the same instance for the primary and replica in development.
41
41
 
42
+ Then add `connects_to` to `app/models/application_record.rb`:
43
+
44
+ ```ruby
45
+ class ApplicationRecord < ActiveRecord::Base
46
+ connects_to database: {writing: :primary, reading: :replica}
47
+ end
48
+ ```
49
+
42
50
  By default, all reads go to the primary instance. To use the replica, do:
43
51
 
44
52
  ```ruby
@@ -74,7 +82,7 @@ You can pass any options as well.
74
82
 
75
83
  ## Lazy Evaluation
76
84
 
77
- ActiveRecord uses [lazy evaluation](https://www.theodinproject.com/courses/ruby-on-rails/lessons/active-record-queries), which can delay the execution of a query to outside of a `distribute_reads` block. In this case, the primary will be used.
85
+ Active Record uses [lazy evaluation](https://www.theodinproject.com/lessons/ruby-on-rails-active-record-queries), which can delay the execution of a query to outside of a `distribute_reads` block. In this case, the primary will be used.
78
86
 
79
87
  ```ruby
80
88
  users = distribute_reads { User.where(orders_count: 1) } # not executed yet
@@ -128,13 +136,13 @@ If no replicas are available, primary is used. To prevent this situation from ov
128
136
 
129
137
  ```ruby
130
138
  distribute_reads(failover: false) do
131
- # raises DistributeReads::NoReplicasAvailable
139
+ # ...
132
140
  end
133
141
  ```
134
142
 
135
143
  ### Default Options
136
144
 
137
- Change the defaults
145
+ Change the defaults for `distribute_reads` blocks
138
146
 
139
147
  ```ruby
140
148
  DistributeReads.default_options = {
@@ -177,7 +185,7 @@ Get replication lag in seconds
177
185
  DistributeReads.replication_lag
178
186
  ```
179
187
 
180
- Most of the time, Makara does a great job automatically routing queries to replicas. If it incorrectly routes a query to primary, you can use:
188
+ Most of the time, ActiveRecordProxyAdapters does a great job automatically routing queries to replicas. If it incorrectly routes a query to primary, you can use:
181
189
 
182
190
  ```ruby
183
191
  distribute_reads(replica: true) do
@@ -185,9 +193,9 @@ distribute_reads(replica: true) do
185
193
  end
186
194
  ```
187
195
 
188
- ## Rails 6
196
+ ## Rails
189
197
 
190
- Rails 6 has [native support for replicas](https://guides.rubyonrails.org/active_record_multiple_databases.html) :tada:
198
+ Rails 6+ has [native support for replicas](https://guides.rubyonrails.org/active_record_multiple_databases.html) :tada:
191
199
 
192
200
  ```ruby
193
201
  ActiveRecord::Base.connected_to(role: :reading) do
@@ -195,11 +203,17 @@ ActiveRecord::Base.connected_to(role: :reading) do
195
203
  end
196
204
  ```
197
205
 
198
- However, it’s not able to automatically route queries like Makara just yet.
206
+ However, it’s not able to do automatic statement-based routing yet.
199
207
 
200
208
  ## Thanks
201
209
 
202
- Thanks to [TaskRabbit](https://github.com/taskrabbit) for Makara, [Sherin Kurian](https://github.com/sherinkurian) for the max lag option, and [Nick Elser](https://github.com/nickelser) for the write-through cache.
210
+ Thanks to [Nasdaq](https://github.com/Nasdaq) for ActiveRecordProxyAdapters, [TaskRabbit](https://github.com/taskrabbit) for Makara, [Sherin Kurian](https://github.com/sherin) for the max lag option, and [Nick Elser](https://github.com/nickelser) for the write-through cache.
211
+
212
+ ## Upgrading
213
+
214
+ ### 1.0
215
+
216
+ ActiveRecordProxyAdapters is now used instead of Makara. Update `config/database.yml` and `app/models/application_record.rb` to [use it](#how-to-use).
203
217
 
204
218
  ## History
205
219
 
@@ -1,35 +1,46 @@
1
1
  module DistributeReads
2
2
  module AppropriatePool
3
- def _appropriate_pool(*args)
3
+ def roles_for(...)
4
4
  if Thread.current[:distribute_reads]
5
5
  if Thread.current[:distribute_reads][:replica]
6
- if @slave_pool.completely_blacklisted?
7
- raise DistributeReads::NoReplicasAvailable, "No replicas available" if Thread.current[:distribute_reads][:failover] == false
8
- DistributeReads.log "No replicas available. Falling back to master pool."
9
- @master_pool
10
- else
11
- @slave_pool
12
- end
13
- elsif Thread.current[:distribute_reads][:primary] || needs_master?(*args) || (blacklisted = @slave_pool.completely_blacklisted?)
14
- if blacklisted
15
- if Thread.current[:distribute_reads][:failover] == false
16
- raise DistributeReads::NoReplicasAvailable, "No replicas available"
17
- else
18
- DistributeReads.log "No replicas available. Falling back to master pool."
19
- end
20
- end
21
- stick_to_master(*args) if DistributeReads.by_default
22
- @master_pool
23
- elsif in_transaction?
24
- @master_pool
6
+ [reading_role]
7
+ elsif Thread.current[:distribute_reads][:primary]
8
+ [writing_role]
25
9
  else
26
- @slave_pool
10
+ super
27
11
  end
28
12
  elsif !DistributeReads.by_default
29
- @master_pool
13
+ [writing_role]
30
14
  else
31
15
  super
32
16
  end
33
17
  end
18
+
19
+ def recent_write_to_primary?(...)
20
+ Thread.current[:distribute_reads] ? false : super
21
+ end
22
+
23
+ def connection_for(role, ...)
24
+ return super if role == writing_role
25
+
26
+ begin
27
+ super
28
+ rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
29
+ failover = Thread.current[:distribute_reads] ? Thread.current[:distribute_reads][:failover] : true
30
+ raise if failover == false
31
+ DistributeReads.log "No replicas available. Falling back to primary."
32
+ super(writing_role, ...)
33
+ end
34
+ end
35
+
36
+ # defer error handling to connection_for
37
+ def checkout_replica_connection
38
+ replica_pool.checkout(proxy_checkout_timeout)
39
+ end
40
+
41
+ def update_primary_latest_write_timestamp(...)
42
+ return if !DistributeReads.by_default
43
+ super
44
+ end
34
45
  end
35
46
  end
@@ -23,7 +23,7 @@ module DistributeReads
23
23
  current_lag =
24
24
  begin
25
25
  DistributeReads.replication_lag(connection: base_model.connection)
26
- rescue DistributeReads::NoReplicasAvailable
26
+ rescue ActiveRecord::ConnectionNotEstablished
27
27
  # TODO rescue more exceptions?
28
28
  false
29
29
  end
@@ -44,7 +44,7 @@ module DistributeReads
44
44
  # TODO possibly per connection
45
45
  Thread.current[:distribute_reads][:primary] = true
46
46
  Thread.current[:distribute_reads][:replica] = false
47
- DistributeReads.log "#{message}. Falling back to master pool."
47
+ DistributeReads.log "#{message}. Falling back to primary."
48
48
  break
49
49
  else
50
50
  raise DistributeReads::TooMuchLag, message
@@ -7,7 +7,7 @@ module DistributeReads
7
7
  included do
8
8
  before_perform do
9
9
  if DistributeReads.by_default
10
- Makara::Context.release_all
10
+ ActiveRecord::Base.connection.send(:proxy).send(:current_context=, nil)
11
11
  end
12
12
  end
13
13
  end
@@ -1,3 +1,3 @@
1
1
  module DistributeReads
2
- VERSION = "0.4.0"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,17 +1,15 @@
1
1
  # dependencies
2
2
  require "active_support"
3
- require "makara"
3
+ require "active_record_proxy_adapters"
4
4
 
5
5
  # modules
6
- require "distribute_reads/appropriate_pool"
7
- require "distribute_reads/cache_store"
8
- require "distribute_reads/global_methods"
9
- require "distribute_reads/version"
6
+ require_relative "distribute_reads/appropriate_pool"
7
+ require_relative "distribute_reads/global_methods"
8
+ require_relative "distribute_reads/version"
10
9
 
11
10
  module DistributeReads
12
11
  class Error < StandardError; end
13
12
  class TooMuchLag < Error; end
14
- class NoReplicasAvailable < Error; end
15
13
 
16
14
  class << self
17
15
  attr_accessor :by_default, :default_options, :eager_load
@@ -34,14 +32,9 @@ module DistributeReads
34
32
  def self.replication_lag(connection: nil)
35
33
  connection ||= ActiveRecord::Base.connection
36
34
 
37
- replica_pool = connection.instance_variable_get(:@slave_pool)
38
- if replica_pool && replica_pool.connections.size > 1
39
- log "Multiple replicas available, lag only reported for one"
40
- end
41
-
42
35
  with_replica do
43
36
  case connection.adapter_name
44
- when "PostgreSQL", "PostGIS"
37
+ when "PostgreSQL", "PostGIS", "PostgreSQLProxy"
45
38
  # cache the version number
46
39
  @aurora_postgres ||= {}
47
40
  cache_key = connection.pool.object_id
@@ -80,12 +73,12 @@ module DistributeReads
80
73
  END AS lag".squish
81
74
  ).first["lag"].to_f
82
75
  end
83
- when "MySQL", "Mysql2", "Mysql2Spatial", "Mysql2Rgeo"
76
+ when "MySQL", "Mysql2", "Mysql2Spatial", "Mysql2Rgeo", "Mysql2Proxy", "TrilogyProxy"
84
77
  @aurora_mysql ||= {}
85
78
  cache_key = connection.pool.object_id
86
79
 
87
80
  unless @aurora_mysql.key?(cache_key)
88
- # makara doesn't send SHOW queries to replica by default
81
+ # SHOW queries not sent to replica by default
89
82
  @aurora_mysql[cache_key] = connection.select_all("SHOW VARIABLES LIKE 'aurora_version'").any?
90
83
  end
91
84
 
@@ -130,7 +123,7 @@ module DistributeReads
130
123
  @backtrace_cleaner ||= begin
131
124
  bc = ActiveSupport::BacktraceCleaner.new
132
125
  bc.add_silencer { |line| line.include?("lib/distribute_reads") }
133
- bc.add_silencer { |line| line.include?("lib/makara") }
126
+ bc.add_silencer { |line| line.include?("lib/active_record_proxy_adapters") }
134
127
  bc.add_silencer { |line| line.include?("lib/active_record") }
135
128
  bc
136
129
  end
@@ -147,23 +140,20 @@ module DistributeReads
147
140
  end
148
141
  end
149
142
  private_class_method :with_replica
143
+ end
150
144
 
151
- # legacy
152
- def self.default_to_primary
153
- !by_default
154
- end
145
+ ActiveSupport.on_load(:active_record) do
146
+ require "active_record_proxy_adapters/connection_handling"
147
+ ActiveRecordProxyAdapters::PrimaryReplicaProxy.prepend DistributeReads::AppropriatePool
155
148
 
156
- # legacy
157
- def self.default_to_primary=(value)
158
- self.by_default = !value
159
- end
149
+ require "active_record_proxy_adapters/log_subscriber"
150
+ ActiveRecord::LogSubscriber.detach_from :active_record
160
151
  end
161
152
 
162
- Makara::Proxy.prepend DistributeReads::AppropriatePool
163
153
  Object.include DistributeReads::GlobalMethods
164
154
  Object.send :private, :distribute_reads
165
155
 
166
156
  ActiveSupport.on_load(:active_job) do
167
- require "distribute_reads/job_methods"
157
+ require_relative "distribute_reads/job_methods"
168
158
  include DistributeReads::JobMethods
169
159
  end
metadata CHANGED
@@ -1,30 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: distribute_reads
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2022-12-28 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: makara
13
+ name: activerecord
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '0.4'
18
+ version: '7.1'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '0.4'
27
- description:
25
+ version: '7.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: active_record_proxy_adapters
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0.8'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0.8'
28
40
  email: andrew@ankane.org
29
41
  executables: []
30
42
  extensions: []
@@ -35,7 +47,6 @@ files:
35
47
  - README.md
36
48
  - lib/distribute_reads.rb
37
49
  - lib/distribute_reads/appropriate_pool.rb
38
- - lib/distribute_reads/cache_store.rb
39
50
  - lib/distribute_reads/global_methods.rb
40
51
  - lib/distribute_reads/job_methods.rb
41
52
  - lib/distribute_reads/version.rb
@@ -43,7 +54,6 @@ homepage: https://github.com/ankane/distribute_reads
43
54
  licenses:
44
55
  - MIT
45
56
  metadata: {}
46
- post_install_message:
47
57
  rdoc_options: []
48
58
  require_paths:
49
59
  - lib
@@ -51,15 +61,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
51
61
  requirements:
52
62
  - - ">="
53
63
  - !ruby/object:Gem::Version
54
- version: '2.6'
64
+ version: '3.2'
55
65
  required_rubygems_version: !ruby/object:Gem::Requirement
56
66
  requirements:
57
67
  - - ">="
58
68
  - !ruby/object:Gem::Version
59
69
  version: '0'
60
70
  requirements: []
61
- rubygems_version: 3.4.1
62
- signing_key:
71
+ rubygems_version: 3.6.9
63
72
  specification_version: 4
64
73
  summary: Scale database reads with replicas in Rails
65
74
  test_files: []
@@ -1,29 +0,0 @@
1
- module DistributeReads
2
- class CacheStore
3
- def read(key)
4
- memory_cached = memory_store.read(key)
5
- return nil if memory_cached == :nil
6
- return memory_cached if memory_cached
7
-
8
- store_cached = store.try(:read, key)
9
- memory_store.write(key, store_cached || :nil)
10
- store_cached
11
- end
12
-
13
- def write(*args)
14
- memory_store.write(*args)
15
- store.try(:write, *args)
16
- end
17
-
18
- private
19
-
20
- # use ActiveSupport::Cache::MemoryStore instead?
21
- def memory_store
22
- @memory_store ||= Makara::Cache::MemoryStore.new
23
- end
24
-
25
- def store
26
- @store ||= Rails.cache
27
- end
28
- end
29
- end