distribute_reads 0.2.4 → 0.3.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 +6 -0
- data/LICENSE.txt +1 -1
- data/README.md +30 -0
- data/lib/distribute_reads.rb +64 -39
- data/lib/distribute_reads/global_methods.rb +19 -2
- data/lib/distribute_reads/version.rb +1 -1
- metadata +12 -19
- data/.gitignore +0 -9
- data/.travis.yml +0 -14
- data/Gemfile +0 -4
- data/Rakefile +0 -11
- data/distribute_reads.gemspec +0 -31
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4fb973f4af2e8828fc62a2c921605d595ee89bf521ba9f2b65f4c4f695225a76
|
|
4
|
+
data.tar.gz: 26ac05c9a814401ed03e56dfd7ceafa637e3ac0dcdb8aba79e12f9d664b5bbac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f40892ea166bb86ea89809d7b0e1cb74616ba55251f3208272319dab0d71322eb2c4efca3aaa2c134a9e0ae6adc1c20ea2cae16451d3c2fd6205e4f344cd27a8
|
|
7
|
+
data.tar.gz: 415fa474c9e3185f9d39b5410747f3d0a9feaf775e9a7e92d1595bc28d94e8ce69caa06d1c36eb646a98c94f963bc16ae27e54780260172689792066cf06ada2
|
data/CHANGELOG.md
CHANGED
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -137,6 +137,16 @@ DistributeReads.default_options = {
|
|
|
137
137
|
}
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
### Logging
|
|
141
|
+
|
|
142
|
+
Messages about failover are logged to the Active Record logger by default. Set a different logger with:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
DistributeReads.logger = Logger.new(STDERR)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Or use `nil` to disable logging.
|
|
149
|
+
|
|
140
150
|
## Distribute Reads by Default
|
|
141
151
|
|
|
142
152
|
At some point, you may wish to distribute reads by default.
|
|
@@ -161,6 +171,26 @@ Get replication lag in seconds
|
|
|
161
171
|
DistributeReads.replication_lag
|
|
162
172
|
```
|
|
163
173
|
|
|
174
|
+
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:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
distribute_reads(replica: true) do
|
|
178
|
+
# send all queries in block to replica
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Rails 6
|
|
183
|
+
|
|
184
|
+
Rails 6 has [native support for replicas](https://edgeguides.rubyonrails.org/active_record_multiple_databases.html) :tada:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
ActiveRecord::Base.connected_to(role: :reading) do
|
|
188
|
+
# do reads
|
|
189
|
+
end
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
However, it’s not able to automatically route queries like Makara just yet.
|
|
193
|
+
|
|
164
194
|
## Thanks
|
|
165
195
|
|
|
166
196
|
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.
|
data/lib/distribute_reads.rb
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
# dependencies
|
|
2
|
+
require "active_support"
|
|
1
3
|
require "makara"
|
|
4
|
+
|
|
5
|
+
# modules
|
|
2
6
|
require "distribute_reads/appropriate_pool"
|
|
3
7
|
require "distribute_reads/cache_store"
|
|
4
8
|
require "distribute_reads/global_methods"
|
|
@@ -12,6 +16,7 @@ module DistributeReads
|
|
|
12
16
|
class << self
|
|
13
17
|
attr_accessor :by_default
|
|
14
18
|
attr_accessor :default_options
|
|
19
|
+
attr_writer :logger
|
|
15
20
|
end
|
|
16
21
|
self.by_default = false
|
|
17
22
|
self.default_options = {
|
|
@@ -19,15 +24,14 @@ module DistributeReads
|
|
|
19
24
|
lag_failover: false
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
def self.
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
def self.logger
|
|
28
|
+
unless defined?(@logger)
|
|
29
|
+
@logger = ActiveRecord::Base.logger
|
|
25
30
|
end
|
|
31
|
+
@logger
|
|
26
32
|
end
|
|
27
33
|
|
|
28
|
-
def self.
|
|
29
|
-
raise DistributeReads::Error, "Don't use outside distribute_reads" unless Thread.current[:distribute_reads]
|
|
30
|
-
|
|
34
|
+
def self.replication_lag(connection: nil)
|
|
31
35
|
connection ||= ActiveRecord::Base.connection
|
|
32
36
|
|
|
33
37
|
replica_pool = connection.instance_variable_get(:@slave_pool)
|
|
@@ -35,35 +39,33 @@ module DistributeReads
|
|
|
35
39
|
log "Multiple replicas available, lag only reported for one"
|
|
36
40
|
end
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# makara doesn't send SHOW queries to replica, so we must force it
|
|
61
|
-
Thread.current[:distribute_reads][:replica] = true
|
|
62
|
-
|
|
42
|
+
with_replica do
|
|
43
|
+
case connection.adapter_name
|
|
44
|
+
when "PostgreSQL", "PostGIS"
|
|
45
|
+
# cache the version number
|
|
46
|
+
@server_version_num ||= {}
|
|
47
|
+
cache_key = connection.pool.object_id
|
|
48
|
+
@server_version_num[cache_key] ||= connection.execute("SHOW server_version_num").first["server_version_num"].to_i
|
|
49
|
+
|
|
50
|
+
lag_condition =
|
|
51
|
+
if @server_version_num[cache_key] >= 100000
|
|
52
|
+
"pg_last_wal_receive_lsn() = pg_last_wal_replay_lsn()"
|
|
53
|
+
else
|
|
54
|
+
"pg_last_xlog_receive_location() = pg_last_xlog_replay_location()"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
connection.execute(
|
|
58
|
+
"SELECT CASE
|
|
59
|
+
WHEN NOT pg_is_in_recovery() OR #{lag_condition} THEN 0
|
|
60
|
+
ELSE EXTRACT (EPOCH FROM NOW() - pg_last_xact_replay_timestamp())
|
|
61
|
+
END AS lag".squish
|
|
62
|
+
).first["lag"].to_f
|
|
63
|
+
when "MySQL", "Mysql2", "Mysql2Spatial", "Mysql2Rgeo"
|
|
63
64
|
@aurora_mysql ||= {}
|
|
64
65
|
cache_key = connection.pool.object_id
|
|
65
66
|
|
|
66
67
|
unless @aurora_mysql.key?(cache_key)
|
|
68
|
+
# makara doesn't send SHOW queries to replica by default
|
|
67
69
|
@aurora_mysql[cache_key] = connection.exec_query("SHOW VARIABLES LIKE 'aurora_version'").to_hash.any?
|
|
68
70
|
end
|
|
69
71
|
|
|
@@ -72,18 +74,41 @@ module DistributeReads
|
|
|
72
74
|
status ? status["Replica_lag_in_msec"].to_f / 1000.0 : 0.0
|
|
73
75
|
else
|
|
74
76
|
status = connection.exec_query("SHOW SLAVE STATUS").to_hash.first
|
|
75
|
-
|
|
77
|
+
if status
|
|
78
|
+
if status["Seconds_Behind_Master"].nil?
|
|
79
|
+
# replication stopped
|
|
80
|
+
# https://dev.mysql.com/doc/refman/8.0/en/show-slave-status.html
|
|
81
|
+
nil
|
|
82
|
+
else
|
|
83
|
+
status["Seconds_Behind_Master"].to_f
|
|
84
|
+
end
|
|
85
|
+
else
|
|
86
|
+
# not a replica
|
|
87
|
+
0.0
|
|
88
|
+
end
|
|
76
89
|
end
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
when "SQLite"
|
|
91
|
+
# never a replica
|
|
92
|
+
0.0
|
|
93
|
+
else
|
|
94
|
+
raise DistributeReads::Error, "Option not supported with this adapter"
|
|
79
95
|
end
|
|
80
|
-
else
|
|
81
|
-
raise DistributeReads::Error, "Option not supported with this adapter"
|
|
82
96
|
end
|
|
83
97
|
end
|
|
84
98
|
|
|
85
99
|
def self.log(message)
|
|
86
|
-
|
|
100
|
+
logger.info("[distribute_reads] #{message}") if logger
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# private
|
|
104
|
+
def self.with_replica
|
|
105
|
+
previous_value = Thread.current[:distribute_reads]
|
|
106
|
+
begin
|
|
107
|
+
Thread.current[:distribute_reads] = {replica: true, failover: false}
|
|
108
|
+
yield
|
|
109
|
+
ensure
|
|
110
|
+
Thread.current[:distribute_reads] = previous_value
|
|
111
|
+
end
|
|
87
112
|
end
|
|
88
113
|
|
|
89
114
|
# private
|
|
@@ -105,8 +130,8 @@ module DistributeReads
|
|
|
105
130
|
end
|
|
106
131
|
end
|
|
107
132
|
|
|
108
|
-
Makara::Proxy.
|
|
109
|
-
Object.
|
|
133
|
+
Makara::Proxy.prepend DistributeReads::AppropriatePool
|
|
134
|
+
Object.include DistributeReads::GlobalMethods
|
|
110
135
|
|
|
111
136
|
ActiveSupport.on_load(:active_job) do
|
|
112
137
|
require "distribute_reads/job_methods"
|
|
@@ -20,8 +20,25 @@ module DistributeReads
|
|
|
20
20
|
max_lag = options[:max_lag]
|
|
21
21
|
if max_lag && !options[:primary]
|
|
22
22
|
Array(options[:lag_on] || [ActiveRecord::Base]).each do |base_model|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
current_lag =
|
|
24
|
+
begin
|
|
25
|
+
DistributeReads.replication_lag(connection: base_model.connection)
|
|
26
|
+
rescue DistributeReads::NoReplicasAvailable => e
|
|
27
|
+
# TODO rescue more exceptions?
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if !current_lag || current_lag > max_lag
|
|
32
|
+
message =
|
|
33
|
+
if current_lag.nil?
|
|
34
|
+
"Replication stopped"
|
|
35
|
+
elsif !current_lag
|
|
36
|
+
"No replicas available for lag check"
|
|
37
|
+
else
|
|
38
|
+
"Replica lag over #{max_lag} seconds"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
message = "#{message} on #{base_model.name} connection" if options[:lag_on]
|
|
25
42
|
|
|
26
43
|
if options[:lag_failover]
|
|
27
44
|
# TODO possibly per connection
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: distribute_reads
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Kane
|
|
8
8
|
autorequire:
|
|
9
|
-
bindir:
|
|
9
|
+
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2019-06-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: makara
|
|
@@ -16,14 +16,14 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0'
|
|
19
|
+
version: '0.3'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0'
|
|
26
|
+
version: '0.3'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: bundler
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -70,16 +70,16 @@ dependencies:
|
|
|
70
70
|
name: pg
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
72
72
|
requirements:
|
|
73
|
-
- - "
|
|
73
|
+
- - ">="
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '
|
|
75
|
+
version: '0'
|
|
76
76
|
type: :development
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
|
-
- - "
|
|
80
|
+
- - ">="
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '
|
|
82
|
+
version: '0'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
84
|
name: mysql2
|
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -109,20 +109,14 @@ dependencies:
|
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
110
|
version: '0'
|
|
111
111
|
description:
|
|
112
|
-
email:
|
|
113
|
-
- andrew@chartkick.com
|
|
112
|
+
email: andrew@chartkick.com
|
|
114
113
|
executables: []
|
|
115
114
|
extensions: []
|
|
116
115
|
extra_rdoc_files: []
|
|
117
116
|
files:
|
|
118
|
-
- ".gitignore"
|
|
119
|
-
- ".travis.yml"
|
|
120
117
|
- CHANGELOG.md
|
|
121
|
-
- Gemfile
|
|
122
118
|
- LICENSE.txt
|
|
123
119
|
- README.md
|
|
124
|
-
- Rakefile
|
|
125
|
-
- distribute_reads.gemspec
|
|
126
120
|
- lib/distribute_reads.rb
|
|
127
121
|
- lib/distribute_reads/appropriate_pool.rb
|
|
128
122
|
- lib/distribute_reads/cache_store.rb
|
|
@@ -141,15 +135,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
141
135
|
requirements:
|
|
142
136
|
- - ">="
|
|
143
137
|
- !ruby/object:Gem::Version
|
|
144
|
-
version: '
|
|
138
|
+
version: '2.4'
|
|
145
139
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
146
140
|
requirements:
|
|
147
141
|
- - ">="
|
|
148
142
|
- !ruby/object:Gem::Version
|
|
149
143
|
version: '0'
|
|
150
144
|
requirements: []
|
|
151
|
-
|
|
152
|
-
rubygems_version: 2.7.6
|
|
145
|
+
rubygems_version: 3.0.3
|
|
153
146
|
signing_key:
|
|
154
147
|
specification_version: 4
|
|
155
148
|
summary: Scale database reads with replicas in Rails
|
data/.gitignore
DELETED
data/.travis.yml
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
sudo: false
|
|
2
|
-
language: ruby
|
|
3
|
-
rvm: 2.5.1
|
|
4
|
-
script: bundle exec rake test
|
|
5
|
-
before_script:
|
|
6
|
-
- psql -c 'create database distribute_reads_test_primary;' -U postgres
|
|
7
|
-
- psql -c 'create database distribute_reads_test_replica;' -U postgres
|
|
8
|
-
notifications:
|
|
9
|
-
email:
|
|
10
|
-
on_success: never
|
|
11
|
-
on_failure: change
|
|
12
|
-
gemfile:
|
|
13
|
-
- Gemfile
|
|
14
|
-
- test/gemfiles/makara3.gemfile
|
data/Gemfile
DELETED
data/Rakefile
DELETED
data/distribute_reads.gemspec
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# coding: utf-8
|
|
2
|
-
lib = File.expand_path("../lib", __FILE__)
|
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
-
require "distribute_reads/version"
|
|
5
|
-
|
|
6
|
-
Gem::Specification.new do |spec|
|
|
7
|
-
spec.name = "distribute_reads"
|
|
8
|
-
spec.version = DistributeReads::VERSION
|
|
9
|
-
spec.authors = ["Andrew Kane"]
|
|
10
|
-
spec.email = ["andrew@chartkick.com"]
|
|
11
|
-
|
|
12
|
-
spec.summary = "Scale database reads with replicas in Rails"
|
|
13
|
-
spec.homepage = "https://github.com/ankane/distribute_reads"
|
|
14
|
-
spec.license = "MIT"
|
|
15
|
-
|
|
16
|
-
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
17
|
-
f.match(%r{^(test|spec|features)/})
|
|
18
|
-
end
|
|
19
|
-
spec.bindir = "exe"
|
|
20
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
21
|
-
spec.require_paths = ["lib"]
|
|
22
|
-
|
|
23
|
-
spec.add_dependency "makara"
|
|
24
|
-
|
|
25
|
-
spec.add_development_dependency "bundler"
|
|
26
|
-
spec.add_development_dependency "rake"
|
|
27
|
-
spec.add_development_dependency "minitest"
|
|
28
|
-
spec.add_development_dependency "pg", "< 1"
|
|
29
|
-
spec.add_development_dependency "mysql2"
|
|
30
|
-
spec.add_development_dependency "activejob"
|
|
31
|
-
end
|