distribute_reads 0.2.4 → 0.3.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
  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