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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e39298a87b7d9af49c9bb2dd6bffc0174a569d994432fc86c6668f00d7b8b4b
4
- data.tar.gz: 585c11f47cb258259b91ff9b3f7917ff83d06b7b1a7010732174790adac45917
3
+ metadata.gz: 4fb973f4af2e8828fc62a2c921605d595ee89bf521ba9f2b65f4c4f695225a76
4
+ data.tar.gz: 26ac05c9a814401ed03e56dfd7ceafa637e3ac0dcdb8aba79e12f9d664b5bbac
5
5
  SHA512:
6
- metadata.gz: 527754102963062a8988612b29f92d2f6bf7f1d4e8084536983c7bf6fdbdaf92db5ae17e4d5e6ee7ae92865b5ad4e98a13a9710675dec85d8365acfeae4e7e72
7
- data.tar.gz: 81c561439791aab9164e62d928d2a3ea60d9bbf9deb6217938fd36632d842e138106c11aacfb951ee48435a02bb0503d542ff861b736f9fcdac7ebfebdb5d6f2
6
+ metadata.gz: f40892ea166bb86ea89809d7b0e1cb74616ba55251f3208272319dab0d71322eb2c4efca3aaa2c134a9e0ae6adc1c20ea2cae16451d3c2fd6205e4f344cd27a8
7
+ data.tar.gz: 415fa474c9e3185f9d39b5410747f3d0a9feaf775e9a7e92d1595bc28d94e8ce69caa06d1c36eb646a98c94f963bc16ae27e54780260172689792066cf06ada2
@@ -1,3 +1,9 @@
1
+ ## 0.3.0
2
+
3
+ - Use logger instead of stderr
4
+ - Handle `NULL` replication lag for MySQL
5
+ - Fixed replication lag check running on primary when replicas blacklisted
6
+
1
7
  ## 0.2.4
2
8
 
3
9
  - Added support for Aurora MySQL replication lag
@@ -1,4 +1,4 @@
1
- Copyright (c) 2017 Andrew Kane
1
+ Copyright (c) 2017-2019 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
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.
@@ -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.replication_lag(connection: nil)
23
- distribute_reads do
24
- lag(connection: connection)
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.lag(connection: nil)
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
- if %w(PostgreSQL PostGIS).include?(connection.adapter_name)
39
- # cache the version number
40
- @server_version_num ||= {}
41
- cache_key = connection.pool.object_id
42
- @server_version_num[cache_key] ||= connection.execute("SHOW server_version_num").first["server_version_num"].to_i
43
-
44
- lag_condition =
45
- if @server_version_num[cache_key] >= 100000
46
- "pg_last_wal_receive_lsn() = pg_last_wal_replay_lsn()"
47
- else
48
- "pg_last_xlog_receive_location() = pg_last_xlog_replay_location()"
49
- end
50
-
51
- connection.execute(
52
- "SELECT CASE
53
- WHEN NOT pg_is_in_recovery() OR #{lag_condition} THEN 0
54
- ELSE EXTRACT (EPOCH FROM NOW() - pg_last_xact_replay_timestamp())
55
- END AS lag".squish
56
- ).first["lag"].to_f
57
- elsif %w(MySQL Mysql2 Mysql2Spatial Mysql2Rgeo).include?(connection.adapter_name)
58
- replica_value = Thread.current[:distribute_reads][:replica]
59
- begin
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
- status ? status["Seconds_Behind_Master"].to_f : 0.0
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
- ensure
78
- Thread.current[:distribute_reads][:replica] = replica_value
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
- warn "[distribute_reads] #{message}"
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.send :prepend, DistributeReads::AppropriatePool
109
- Object.send :include, DistributeReads::GlobalMethods
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
- if DistributeReads.lag(connection: base_model.connection) > max_lag
24
- message = "Replica lag over #{max_lag} seconds#{options[:lag_on] ? " on #{base_model.name} connection" : ""}"
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
@@ -1,3 +1,3 @@
1
1
  module DistributeReads
2
- VERSION = "0.2.4"
2
+ VERSION = "0.3.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.2.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-14 00:00:00.000000000 Z
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: '1'
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: '1'
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: '0'
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
- rubyforge_project:
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
@@ -1,9 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
9
- *.lock
@@ -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
@@ -1,4 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- # Specify your gem's dependencies in distribute_reads.gemspec
4
- gemspec
data/Rakefile DELETED
@@ -1,11 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
3
-
4
- Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.libs << "lib"
7
- t.test_files = FileList["test/**/*_test.rb"]
8
- t.warning = false
9
- end
10
-
11
- task default: :test
@@ -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