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 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