distribute_reads 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|