distribute_reads 0.1.2 → 0.2.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 +17 -0
- data/README.md +65 -2
- data/lib/distribute_reads.rb +35 -10
- data/lib/distribute_reads/appropriate_pool.rb +4 -3
- data/lib/distribute_reads/cache_store.rb +29 -0
- data/lib/distribute_reads/global_methods.rb +22 -5
- data/lib/distribute_reads/job_methods.rb +15 -3
- data/lib/distribute_reads/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d5f81a8379060ca196ed7550fa1dcafc706afc7
|
|
4
|
+
data.tar.gz: 78f4f6af58f6c6b184d1e56532e9c7d910846719
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 78b965141d1bd574726480b2c795131539fb1fcfa2f8456307f4f10ffc34b893826d2c822d170602588a19ab1b09f3d360ab016555f2b5d8e2e9fb92c4c09f40
|
|
7
|
+
data.tar.gz: ef38d4ed23658799456f197bff9d0a25ab044e81994aaa453a37495990d108e761e02de0ac97dd017115ba3b9ace8cefbacd157755de3ff80fc43e00ca77ce6c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
## 0.2.0
|
|
2
|
+
|
|
3
|
+
Breaking
|
|
4
|
+
|
|
5
|
+
- Jobs default to replica when `default_to_primary` is false
|
|
6
|
+
|
|
7
|
+
Other
|
|
8
|
+
|
|
9
|
+
- Replaced `default_to_primary` with `by_default`
|
|
10
|
+
- Fixed `max_lag` option
|
|
11
|
+
- Added `lag_failover` option
|
|
12
|
+
- Added `failover` option
|
|
13
|
+
- Added `lag_on` option
|
|
14
|
+
- Added `primary` option
|
|
15
|
+
- Added default options
|
|
16
|
+
- Improved lag query
|
|
17
|
+
|
|
1
18
|
## 0.1.2
|
|
2
19
|
|
|
3
20
|
- Raise `ArgumentError` when missing block
|
data/README.md
CHANGED
|
@@ -70,8 +70,12 @@ class TestJob < ApplicationJob
|
|
|
70
70
|
end
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
You can pass any options as well.
|
|
74
|
+
|
|
73
75
|
## Options
|
|
74
76
|
|
|
77
|
+
### Replica Lag
|
|
78
|
+
|
|
75
79
|
Raise an error when replica lag is too high - *PostgreSQL only*
|
|
76
80
|
|
|
77
81
|
```ruby
|
|
@@ -80,12 +84,71 @@ distribute_reads(max_lag: 3) do
|
|
|
80
84
|
end
|
|
81
85
|
```
|
|
82
86
|
|
|
83
|
-
|
|
87
|
+
Instead of raising an error, you can also use primary
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
distribute_reads(max_lag: 3, lag_failover: true) do
|
|
91
|
+
# ...
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If you have multiple databases, this only checks lag on `ActiveRecord::Base` connection. Specify connections to check with
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
distribute_reads(max_lag: 3, lag_on: [ApplicationRecord, LogRecord]) do
|
|
99
|
+
# ...
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Note:** If lag on any connection exceeds the max lag and lag failover is used, *all connections* will use their primary.
|
|
104
|
+
|
|
105
|
+
### Availability
|
|
106
|
+
|
|
107
|
+
If no replicas are available, primary is used. To prevent this situation from overloading the primary, you can raise an error instead.
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
distribute_reads(failover: false) do
|
|
111
|
+
# raises DistributeReads::NoReplicasAvailable
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Default Options
|
|
116
|
+
|
|
117
|
+
Change the defaults
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
DistributeReads.default_options = {
|
|
121
|
+
lag_failover: true,
|
|
122
|
+
failover: false
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Distribute Reads by Default
|
|
127
|
+
|
|
128
|
+
At some point, you may wish to distribute reads by default.
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
DistributeReads.by_default = true
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Once you do this, Makara will use the Rails cache to track its state. To reduce load on the Rails cache, use a write-through cache in front of it.
|
|
84
135
|
|
|
85
136
|
```ruby
|
|
86
|
-
|
|
137
|
+
Makara::Cache.store = DistributeReads::CacheStore.new
|
|
87
138
|
```
|
|
88
139
|
|
|
140
|
+
To make queries go to primary, use:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
distribute_reads(primary: true) do
|
|
144
|
+
# ...
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Thanks
|
|
149
|
+
|
|
150
|
+
Thanks to [TaskRabbit](https://github.com/taskrabbit) for Makara and [Nick Elser](https://github.com/nickelser) for the write-through cache.
|
|
151
|
+
|
|
89
152
|
## History
|
|
90
153
|
|
|
91
154
|
View the [changelog](https://github.com/ankane/distribute_reads/blob/master/CHANGELOG.md)
|
data/lib/distribute_reads.rb
CHANGED
|
@@ -1,29 +1,54 @@
|
|
|
1
1
|
require "makara"
|
|
2
2
|
require "distribute_reads/appropriate_pool"
|
|
3
|
+
require "distribute_reads/cache_store"
|
|
3
4
|
require "distribute_reads/global_methods"
|
|
4
5
|
require "distribute_reads/version"
|
|
5
6
|
|
|
6
7
|
module DistributeReads
|
|
7
|
-
class
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
class TooMuchLag < Error; end
|
|
10
|
+
class NoReplicasAvailable < Error; end
|
|
8
11
|
|
|
9
12
|
class << self
|
|
10
|
-
attr_accessor :
|
|
13
|
+
attr_accessor :by_default
|
|
14
|
+
attr_accessor :default_options
|
|
11
15
|
end
|
|
12
|
-
self.
|
|
16
|
+
self.by_default = false
|
|
17
|
+
self.default_options = {
|
|
18
|
+
failover: true,
|
|
19
|
+
lag_failover: false
|
|
20
|
+
}
|
|
13
21
|
|
|
14
|
-
def self.lag
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
def self.lag(connection: nil)
|
|
23
|
+
raise DistributeReads::Error, "Don't use outside distribute_reads" unless Thread.current[:distribute_reads]
|
|
24
|
+
|
|
25
|
+
connection ||= ActiveRecord::Base.connection
|
|
26
|
+
if %w(PostgreSQL PostGIS).include?(connection.adapter_name)
|
|
27
|
+
replica_pool = connection.instance_variable_get(:@slave_pool)
|
|
28
|
+
if replica_pool && replica_pool.connections.size > 1
|
|
29
|
+
warn "[distribute_reads] Multiple replicas available, lag only reported for one"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
connection.execute(
|
|
18
33
|
"SELECT CASE
|
|
19
|
-
WHEN pg_last_xlog_receive_location() = pg_last_xlog_replay_location() THEN 0
|
|
34
|
+
WHEN NOT pg_is_in_recovery() OR pg_last_xlog_receive_location() = pg_last_xlog_replay_location() THEN 0
|
|
20
35
|
ELSE EXTRACT (EPOCH FROM NOW() - pg_last_xact_replay_timestamp())
|
|
21
36
|
END AS lag"
|
|
22
37
|
).first["lag"].to_f
|
|
23
38
|
else
|
|
24
|
-
raise "Option not supported with this adapter"
|
|
39
|
+
raise DistributeReads::Error, "Option not supported with this adapter"
|
|
25
40
|
end
|
|
26
41
|
end
|
|
42
|
+
|
|
43
|
+
# legacy
|
|
44
|
+
def self.default_to_primary
|
|
45
|
+
!by_default
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# legacy
|
|
49
|
+
def self.default_to_primary=(value)
|
|
50
|
+
self.by_default = !value
|
|
51
|
+
end
|
|
27
52
|
end
|
|
28
53
|
|
|
29
54
|
Makara::Proxy.send :prepend, DistributeReads::AppropriatePool
|
|
@@ -31,5 +56,5 @@ Object.send :include, DistributeReads::GlobalMethods
|
|
|
31
56
|
|
|
32
57
|
ActiveSupport.on_load(:active_job) do
|
|
33
58
|
require "distribute_reads/job_methods"
|
|
34
|
-
|
|
59
|
+
include DistributeReads::JobMethods
|
|
35
60
|
end
|
|
@@ -2,15 +2,16 @@ module DistributeReads
|
|
|
2
2
|
module AppropriatePool
|
|
3
3
|
def _appropriate_pool(*args)
|
|
4
4
|
if Thread.current[:distribute_reads]
|
|
5
|
-
if needs_master?(*args) || @slave_pool.completely_blacklisted?
|
|
6
|
-
|
|
5
|
+
if Thread.current[:distribute_reads][:primary] || needs_master?(*args) || (blacklisted = @slave_pool.completely_blacklisted?)
|
|
6
|
+
raise DistributeReads::NoReplicasAvailable, "No replicas available" if blacklisted && Thread.current[:distribute_reads][:failover] == false
|
|
7
|
+
stick_to_master(*args) if DistributeReads.by_default
|
|
7
8
|
@master_pool
|
|
8
9
|
elsif in_transaction?
|
|
9
10
|
@master_pool
|
|
10
11
|
else
|
|
11
12
|
@slave_pool
|
|
12
13
|
end
|
|
13
|
-
elsif DistributeReads.
|
|
14
|
+
elsif !DistributeReads.by_default
|
|
14
15
|
@master_pool
|
|
15
16
|
else
|
|
16
17
|
super
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
module DistributeReads
|
|
2
2
|
module GlobalMethods
|
|
3
|
-
def distribute_reads(
|
|
3
|
+
def distribute_reads(**options)
|
|
4
4
|
raise ArgumentError, "Missing block" unless block_given?
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
unknown_keywords = options.keys - [:failover, :lag_failover, :lag_on, :max_lag, :primary]
|
|
7
|
+
raise ArgumentError, "Unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
|
|
8
|
+
|
|
9
|
+
options = DistributeReads.default_options.merge(options)
|
|
9
10
|
|
|
10
11
|
previous_value = Thread.current[:distribute_reads]
|
|
11
12
|
begin
|
|
12
|
-
Thread.current[:distribute_reads] =
|
|
13
|
+
Thread.current[:distribute_reads] = {failover: options[:failover], primary: options[:primary]}
|
|
14
|
+
|
|
15
|
+
# TODO ensure same connection is used to test lag and execute queries
|
|
16
|
+
max_lag = options[:max_lag]
|
|
17
|
+
if max_lag && !options[:primary]
|
|
18
|
+
Array(options[:lag_on] || [ActiveRecord::Base]).each do |base_model|
|
|
19
|
+
if DistributeReads.lag(connection: base_model.connection) > max_lag
|
|
20
|
+
if options[:lag_failover]
|
|
21
|
+
# TODO possibly per connection
|
|
22
|
+
Thread.current[:distribute_reads][:primary] = true
|
|
23
|
+
else
|
|
24
|
+
raise DistributeReads::TooMuchLag, "Replica lag over #{max_lag} seconds#{options[:lag_on] ? " on #{base_model.name} connection" : ""}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
13
30
|
value = yield
|
|
14
31
|
warn "[distribute_reads] Call `to_a` inside block to execute query on replica" if value.is_a?(ActiveRecord::Relation) && !previous_value
|
|
15
32
|
value
|
|
@@ -1,8 +1,20 @@
|
|
|
1
|
+
require "active_support/concern"
|
|
2
|
+
|
|
1
3
|
module DistributeReads
|
|
2
4
|
module JobMethods
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
before_perform do
|
|
9
|
+
Makara::Context.set_current(Makara::Context.generate) if DistributeReads.by_default
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class_methods do
|
|
14
|
+
def distribute_reads(*args)
|
|
15
|
+
around_perform do |job, block|
|
|
16
|
+
distribute_reads(*args) { block.call }
|
|
17
|
+
end
|
|
6
18
|
end
|
|
7
19
|
end
|
|
8
20
|
end
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Kane
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2017-
|
|
11
|
+
date: 2017-10-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: makara
|
|
@@ -110,6 +110,7 @@ files:
|
|
|
110
110
|
- distribute_reads.gemspec
|
|
111
111
|
- lib/distribute_reads.rb
|
|
112
112
|
- lib/distribute_reads/appropriate_pool.rb
|
|
113
|
+
- lib/distribute_reads/cache_store.rb
|
|
113
114
|
- lib/distribute_reads/global_methods.rb
|
|
114
115
|
- lib/distribute_reads/job_methods.rb
|
|
115
116
|
- lib/distribute_reads/version.rb
|