distribute_reads 0.5.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: 498115398e131f837029f3ad86c21ea646204b399f732a800b32687135bee154
4
- data.tar.gz: 40c983fa023cab9fe00b5f0e7b03925d2f1d7c6e3d30056a26e9c147fd92d4b4
3
+ metadata.gz: 88f83ede9ffd2ad33f91e493d48a6c1ebcc8fc18e6334e5b708345c0f0754d9a
4
+ data.tar.gz: 7d7bdef9c47ee1f8689c8956a7a7480e6b9a0f0bdf6adb8b997248a643d4fc1d
5
5
  SHA512:
6
- metadata.gz: 3a3a60620721ef9b23d1faf8a742386dfd58d3830ee7b110dbc44d100d7fd7670189e8893f1d21eab4d7bced008942f8fe8116756ae1aae6cec0f9427b3018d4
7
- data.tar.gz: 56b65735bffb296bcfc8719c847988c0ecc4610c21ecad650115181431ac85b0b7cd686c7a409cede3cfc25c9a54de74e6f053556ea9ca6c28feb242e6ad1709
6
+ metadata.gz: 0e74d909cd478d90c5c0750d46287d8c6e10d1e7cd8910e4a6e3f9398f92e36fd197bec9a6986c45c982cd5d3e730327d6d99366539c87dd99734715f44c448a
7
+ data.tar.gz: '08d34dd833a3625fd6065e26811b0290848db014be9b5224246fdb1642f4aae0ae8ae675d9ec860699c0c5f8cd60513d442efa2e0d7e456e88570fcaff2661fa'
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
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
+
1
8
  ## 0.5.0 (2024-06-24)
2
9
 
3
10
  - Dropped support for Makara 0.4
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2017-2024 Andrew Kane
1
+ Copyright (c) 2017-2025 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -2,7 +2,9 @@
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
+
7
+ :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource) with [Makara](https://github.com/instacart/makara)
6
8
 
7
9
  [![Build Status](https://github.com/ankane/distribute_reads/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/distribute_reads/actions)
8
10
 
@@ -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
- Active Record 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
@@ -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 do automatic statement-based routing like Makara 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/sherin) 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.5.0"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,6 +1,6 @@
1
1
  # dependencies
2
2
  require "active_support"
3
- require "makara"
3
+ require "active_record_proxy_adapters"
4
4
 
5
5
  # modules
6
6
  require_relative "distribute_reads/appropriate_pool"
@@ -10,7 +10,6 @@ require_relative "distribute_reads/version"
10
10
  module DistributeReads
11
11
  class Error < StandardError; end
12
12
  class TooMuchLag < Error; end
13
- class NoReplicasAvailable < Error; end
14
13
 
15
14
  class << self
16
15
  attr_accessor :by_default, :default_options, :eager_load
@@ -33,14 +32,9 @@ module DistributeReads
33
32
  def self.replication_lag(connection: nil)
34
33
  connection ||= ActiveRecord::Base.connection
35
34
 
36
- replica_pool = connection.instance_variable_get(:@slave_pool)
37
- if replica_pool && replica_pool.connections.size > 1
38
- log "Multiple replicas available, lag only reported for one"
39
- end
40
-
41
35
  with_replica do
42
36
  case connection.adapter_name
43
- when "PostgreSQL", "PostGIS"
37
+ when "PostgreSQL", "PostGIS", "PostgreSQLProxy"
44
38
  # cache the version number
45
39
  @aurora_postgres ||= {}
46
40
  cache_key = connection.pool.object_id
@@ -79,12 +73,12 @@ module DistributeReads
79
73
  END AS lag".squish
80
74
  ).first["lag"].to_f
81
75
  end
82
- when "MySQL", "Mysql2", "Mysql2Spatial", "Mysql2Rgeo"
76
+ when "MySQL", "Mysql2", "Mysql2Spatial", "Mysql2Rgeo", "Mysql2Proxy", "TrilogyProxy"
83
77
  @aurora_mysql ||= {}
84
78
  cache_key = connection.pool.object_id
85
79
 
86
80
  unless @aurora_mysql.key?(cache_key)
87
- # makara doesn't send SHOW queries to replica by default
81
+ # SHOW queries not sent to replica by default
88
82
  @aurora_mysql[cache_key] = connection.select_all("SHOW VARIABLES LIKE 'aurora_version'").any?
89
83
  end
90
84
 
@@ -129,7 +123,7 @@ module DistributeReads
129
123
  @backtrace_cleaner ||= begin
130
124
  bc = ActiveSupport::BacktraceCleaner.new
131
125
  bc.add_silencer { |line| line.include?("lib/distribute_reads") }
132
- bc.add_silencer { |line| line.include?("lib/makara") }
126
+ bc.add_silencer { |line| line.include?("lib/active_record_proxy_adapters") }
133
127
  bc.add_silencer { |line| line.include?("lib/active_record") }
134
128
  bc
135
129
  end
@@ -146,19 +140,16 @@ module DistributeReads
146
140
  end
147
141
  end
148
142
  private_class_method :with_replica
143
+ end
149
144
 
150
- # legacy
151
- def self.default_to_primary
152
- !by_default
153
- end
145
+ ActiveSupport.on_load(:active_record) do
146
+ require "active_record_proxy_adapters/connection_handling"
147
+ ActiveRecordProxyAdapters::PrimaryReplicaProxy.prepend DistributeReads::AppropriatePool
154
148
 
155
- # legacy
156
- def self.default_to_primary=(value)
157
- self.by_default = !value
158
- end
149
+ require "active_record_proxy_adapters/log_subscriber"
150
+ ActiveRecord::LogSubscriber.detach_from :active_record
159
151
  end
160
152
 
161
- Makara::Proxy.prepend DistributeReads::AppropriatePool
162
153
  Object.include DistributeReads::GlobalMethods
163
154
  Object.send :private, :distribute_reads
164
155
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: distribute_reads
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.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: 2024-06-24 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
13
  name: activerecord
@@ -16,29 +15,28 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '6.1'
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: '6.1'
25
+ version: '7.1'
27
26
  - !ruby/object:Gem::Dependency
28
- name: makara
27
+ name: active_record_proxy_adapters
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - ">="
32
31
  - !ruby/object:Gem::Version
33
- version: '0.5'
32
+ version: '0.8'
34
33
  type: :runtime
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
- version: '0.5'
41
- description:
39
+ version: '0.8'
42
40
  email: andrew@ankane.org
43
41
  executables: []
44
42
  extensions: []
@@ -56,7 +54,6 @@ homepage: https://github.com/ankane/distribute_reads
56
54
  licenses:
57
55
  - MIT
58
56
  metadata: {}
59
- post_install_message:
60
57
  rdoc_options: []
61
58
  require_paths:
62
59
  - lib
@@ -64,15 +61,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
64
61
  requirements:
65
62
  - - ">="
66
63
  - !ruby/object:Gem::Version
67
- version: '3.1'
64
+ version: '3.2'
68
65
  required_rubygems_version: !ruby/object:Gem::Requirement
69
66
  requirements:
70
67
  - - ">="
71
68
  - !ruby/object:Gem::Version
72
69
  version: '0'
73
70
  requirements: []
74
- rubygems_version: 3.5.11
75
- signing_key:
71
+ rubygems_version: 3.6.9
76
72
  specification_version: 4
77
73
  summary: Scale database reads with replicas in Rails
78
74
  test_files: []