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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 731daba05fd408780f3548cac44ddeda0d4c0c7c
4
- data.tar.gz: 403b13ce349e56794b0bdc3457222724f1da4f8b
3
+ metadata.gz: 0d5f81a8379060ca196ed7550fa1dcafc706afc7
4
+ data.tar.gz: 78f4f6af58f6c6b184d1e56532e9c7d910846719
5
5
  SHA512:
6
- metadata.gz: 76250c6a063c2797328938254bdc0b9c18508929c11a969f9f03d05ada6fe419038d1d25b0772ba536a486f3daf2cfa58673c5ec637324d49ebcddfb04e625d5
7
- data.tar.gz: 4e7289ee561734260874b0a4098f1f991d2ee4965132b59a2257530a3ad54d13510bf6ad3de9fb2c0498b1b9a058cd5f25b29995bc577891178dff05d32504c6
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
- Don’t default to primary (default Makara behavior)
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
- DistributeReads.default_to_primary = false
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)
@@ -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 TooMuchLag < StandardError; end
8
+ class Error < StandardError; end
9
+ class TooMuchLag < Error; end
10
+ class NoReplicasAvailable < Error; end
8
11
 
9
12
  class << self
10
- attr_accessor :default_to_primary
13
+ attr_accessor :by_default
14
+ attr_accessor :default_options
11
15
  end
12
- self.default_to_primary = true
16
+ self.by_default = false
17
+ self.default_options = {
18
+ failover: true,
19
+ lag_failover: false
20
+ }
13
21
 
14
- def self.lag
15
- conn = ActiveRecord::Base.connection
16
- if %w(PostgreSQL PostGIS).include?(conn.adapter_name)
17
- conn.execute(
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
- extend DistributeReads::JobMethods
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
- stick_to_master(*args) unless DistributeReads.default_to_primary
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.default_to_primary
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(max_lag: nil)
3
+ def distribute_reads(**options)
4
4
  raise ArgumentError, "Missing block" unless block_given?
5
5
 
6
- if max_lag && DistributeReads.lag > max_lag
7
- raise DistributeReads::TooMuchLag, "Replica lag over #{max_lag} seconds"
8
- end
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] = true
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
- def distribute_reads(max_lag: nil)
4
- around_perform do |job, block|
5
- distribute_reads(max_lag: max_lag) { block.call }
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
@@ -1,3 +1,3 @@
1
1
  module DistributeReads
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  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.1.2
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-09-21 00:00:00.000000000 Z
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