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 +4 -4
- data/CHANGELOG.md +12 -0
- data/LICENSE.txt +1 -1
- data/README.md +34 -20
- data/lib/distribute_reads/appropriate_pool.rb +33 -22
- data/lib/distribute_reads/global_methods.rb +2 -2
- data/lib/distribute_reads/job_methods.rb +1 -1
- data/lib/distribute_reads/version.rb +1 -1
- data/lib/distribute_reads.rb +15 -25
- metadata +21 -12
- data/lib/distribute_reads/cache_store.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88f83ede9ffd2ad33f91e493d48a6c1ebcc8fc18e6334e5b708345c0f0754d9a
|
4
|
+
data.tar.gz: 7d7bdef9c47ee1f8689c8956a7a7480e6b9a0f0bdf6adb8b997248a643d4fc1d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/README.md
CHANGED
@@ -2,9 +2,11 @@
|
|
2
2
|
|
3
3
|
Scale database reads to replicas in Rails
|
4
4
|
|
5
|
-
|
5
|
+
**Distribute Reads 1.0 was recently released** - see [how to upgrade](#upgrading)
|
6
6
|
|
7
|
-
[
|
7
|
+
:tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource) with [Makara](https://github.com/instacart/makara)
|
8
|
+
|
9
|
+
[](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
|
-
[
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
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
|
-
#
|
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,
|
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
|
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
|
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/
|
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
|
3
|
+
def roles_for(...)
|
4
4
|
if Thread.current[:distribute_reads]
|
5
5
|
if Thread.current[:distribute_reads][:replica]
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
10
|
+
super
|
27
11
|
end
|
28
12
|
elsif !DistributeReads.by_default
|
29
|
-
|
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
|
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
|
47
|
+
DistributeReads.log "#{message}. Falling back to primary."
|
48
48
|
break
|
49
49
|
else
|
50
50
|
raise DistributeReads::TooMuchLag, message
|
data/lib/distribute_reads.rb
CHANGED
@@ -1,17 +1,15 @@
|
|
1
1
|
# dependencies
|
2
2
|
require "active_support"
|
3
|
-
require "
|
3
|
+
require "active_record_proxy_adapters"
|
4
4
|
|
5
5
|
# modules
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
#
|
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/
|
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
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
157
|
-
|
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
|
-
|
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
|
+
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:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
13
|
+
name: activerecord
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
16
15
|
requirements:
|
17
16
|
- - ">="
|
18
17
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
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: '
|
27
|
-
|
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
|
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.
|
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
|