rails_failover 0.3.0 → 0.5.3
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 +4 -4
- data/.rubocop.yml +4 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +6 -11
- data/Gemfile.lock +5 -12
- data/README.md +46 -7
- data/lib/rails_failover/active_record.rb +26 -5
- data/lib/rails_failover/active_record/handler.rb +26 -58
- data/lib/rails_failover/active_record/middleware.rb +75 -14
- data/lib/rails_failover/active_record/railtie.rb +43 -21
- data/lib/rails_failover/redis.rb +23 -15
- data/lib/rails_failover/redis/connector.rb +10 -5
- data/lib/rails_failover/redis/handler.rb +130 -46
- data/lib/rails_failover/version.rb +1 -1
- data/makefile +1 -1
- data/rails_failover.gemspec +0 -2
- data/redis.mk +6 -6
- metadata +4 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31e5bf77f85686776c2cf344b2e145af8e99a984696102cbdf51b457d45f1668
|
|
4
|
+
data.tar.gz: aa6556a7bf399949066078bd463eeb9744db5e16a7152bf112a1ba023226b4bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 46a47eab782fa173e3a18b8d0159798129f39739ffdc9e910f06ba698c6a78689df33120d67278b1b56d1dda0b83d25cbbcd4919ae2384eb932fbc33481f087f
|
|
7
|
+
data.tar.gz: aa69d48628da810cbefbd386a093cff557425d4e77f22985bcc7bbb75b25a522417b4d36a3ae9172832f880974614fd7056187b71e05049c605ec8d92e80de96
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
All notable changes to this project will be documented in this file.
|
|
3
|
+
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.5.2] - 2020-06-23
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- FIX: Only rescue from connection errors.q
|
data/Gemfile
CHANGED
|
@@ -6,14 +6,9 @@ gemspec
|
|
|
6
6
|
|
|
7
7
|
gem "rake", "~> 12.0"
|
|
8
8
|
gem "rspec", "~> 3.0"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
gem 'byebug'
|
|
16
|
-
gem 'redis', '~> 4.1'
|
|
17
|
-
gem 'pg', '~> 1.2'
|
|
18
|
-
gem 'activerecord', '~> 6.0'
|
|
19
|
-
end
|
|
9
|
+
gem 'rubocop-discourse'
|
|
10
|
+
gem 'byebug'
|
|
11
|
+
gem 'redis', '~> 4.1'
|
|
12
|
+
gem 'pg', '~> 1.2'
|
|
13
|
+
gem 'activerecord', '~> 6.0'
|
|
14
|
+
gem 'rack'
|
data/Gemfile.lock
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
rails_failover (0.3
|
|
4
|
+
rails_failover (0.5.3)
|
|
5
5
|
activerecord (~> 6.0)
|
|
6
|
-
listen (~> 3.2)
|
|
7
6
|
railties (~> 6.0)
|
|
8
7
|
|
|
9
8
|
GEM
|
|
@@ -40,25 +39,21 @@ GEM
|
|
|
40
39
|
crass (1.0.6)
|
|
41
40
|
diff-lcs (1.3)
|
|
42
41
|
erubi (1.9.0)
|
|
43
|
-
ffi (1.12.2)
|
|
44
42
|
i18n (1.8.2)
|
|
45
43
|
concurrent-ruby (~> 1.0)
|
|
46
|
-
|
|
47
|
-
rb-fsevent (~> 0.10, >= 0.10.3)
|
|
48
|
-
rb-inotify (~> 0.9, >= 0.9.10)
|
|
49
|
-
loofah (2.5.0)
|
|
44
|
+
loofah (2.6.0)
|
|
50
45
|
crass (~> 1.0.2)
|
|
51
46
|
nokogiri (>= 1.5.9)
|
|
52
47
|
method_source (1.0.0)
|
|
53
48
|
mini_portile2 (2.4.0)
|
|
54
49
|
minitest (5.14.1)
|
|
55
|
-
nokogiri (1.10.
|
|
50
|
+
nokogiri (1.10.10)
|
|
56
51
|
mini_portile2 (~> 2.4.0)
|
|
57
52
|
parallel (1.19.1)
|
|
58
53
|
parser (2.7.1.2)
|
|
59
54
|
ast (~> 2.4.0)
|
|
60
55
|
pg (1.2.3)
|
|
61
|
-
rack (2.2.
|
|
56
|
+
rack (2.2.3)
|
|
62
57
|
rack-test (1.1.0)
|
|
63
58
|
rack (>= 1.0, < 3)
|
|
64
59
|
rails-dom-testing (2.0.3)
|
|
@@ -74,9 +69,6 @@ GEM
|
|
|
74
69
|
thor (>= 0.20.3, < 2.0)
|
|
75
70
|
rainbow (3.0.0)
|
|
76
71
|
rake (12.3.3)
|
|
77
|
-
rb-fsevent (0.10.4)
|
|
78
|
-
rb-inotify (0.10.1)
|
|
79
|
-
ffi (~> 1.0)
|
|
80
72
|
redis (4.1.4)
|
|
81
73
|
rexml (3.2.4)
|
|
82
74
|
rspec (3.9.0)
|
|
@@ -119,6 +111,7 @@ DEPENDENCIES
|
|
|
119
111
|
activerecord (~> 6.0)
|
|
120
112
|
byebug
|
|
121
113
|
pg (~> 1.2)
|
|
114
|
+
rack
|
|
122
115
|
rails_failover!
|
|
123
116
|
rake (~> 12.0)
|
|
124
117
|
redis (~> 4.1)
|
data/README.md
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# RailsFailover
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Automatic failover and recovery for primary/replica setup for:
|
|
4
|
+
|
|
5
|
+
1. Redis
|
|
6
|
+
1. ActiveRecord (PostgreSQL/MySQL Adapters)
|
|
4
7
|
|
|
5
8
|
## Installation
|
|
6
9
|
|
|
7
10
|
Add this line to your application's Gemfile:
|
|
8
11
|
|
|
9
12
|
```ruby
|
|
10
|
-
gem 'rails_failover'
|
|
13
|
+
gem 'rails_failover', require: false
|
|
11
14
|
```
|
|
12
15
|
|
|
13
16
|
And then execute:
|
|
@@ -36,21 +39,58 @@ production:
|
|
|
36
39
|
|
|
37
40
|
The gem will automatically create an `ActiveRecord::ConnectionAdapters::ConnectionHandler` with the `ActiveRecord::Base.reading_role` as the `handler_key`.
|
|
38
41
|
|
|
42
|
+
#### Failover/Fallback Hooks
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
RailsFailover::ActiveRecord.on_failover do
|
|
46
|
+
# Enable readonly mode
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
RailsFailover::ActiveRecord.on_fallback do
|
|
50
|
+
# Disable readonly mode
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
#### Multiple connection handlers
|
|
55
|
+
|
|
56
|
+
Note: This API is unstable and is likely to changes when Rails 6.1 is released with sharding support.
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
# config/database.yml
|
|
60
|
+
|
|
61
|
+
production:
|
|
62
|
+
primary:
|
|
63
|
+
host: <primary db server host>
|
|
64
|
+
port: <primary db server port>
|
|
65
|
+
replica_host: <replica db server host>
|
|
66
|
+
replica_port: <replica db server port>
|
|
67
|
+
second_database_writing:
|
|
68
|
+
host: <primary db server host>
|
|
69
|
+
port: <primary db server port>
|
|
70
|
+
replica_host: <replica db server host>
|
|
71
|
+
replica_port: <replica db server port>
|
|
72
|
+
|
|
73
|
+
# In your ActiveRecord base model or model.
|
|
74
|
+
|
|
75
|
+
connects_to database: { writing: :primary, second_database_writing: :second_database_writing
|
|
76
|
+
```
|
|
77
|
+
|
|
39
78
|
### Redis
|
|
40
79
|
|
|
80
|
+
Add `require 'rails_failover/redis'` before creating a `Redis` instance.
|
|
81
|
+
|
|
41
82
|
```
|
|
42
83
|
Redis.new(host: "127.0.0.1", port: 6379, replica_host: "127.0.0.1", replica_port: 6380, connector: RailsFailover::Redis::Connector))
|
|
43
84
|
```
|
|
44
85
|
|
|
45
|
-
Callbacks can be registered when the
|
|
46
|
-
|
|
86
|
+
Callbacks can be registered when the primary connection is down and when it is up.
|
|
47
87
|
|
|
48
88
|
```
|
|
49
|
-
RailsFailover::Redis.
|
|
89
|
+
RailsFailover::Redis.on_failover_callback do
|
|
50
90
|
# Switch site to read-only mode
|
|
51
91
|
end
|
|
52
92
|
|
|
53
|
-
RailsFailover::Redis.
|
|
93
|
+
RailsFailover::Redis.on_fallback_callback do
|
|
54
94
|
# Switch site out of read-only mode
|
|
55
95
|
end
|
|
56
96
|
```
|
|
@@ -79,7 +119,6 @@ The ActiveRecord failover tests are run against a dummy Rails server. Run the fo
|
|
|
79
119
|
|
|
80
120
|
Bug reports and pull requests are welcome on GitHub at https://github.com/discourse/rails_failover. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/discourse/rails_failover/blob/master/CODE_OF_CONDUCT.md).
|
|
81
121
|
|
|
82
|
-
|
|
83
122
|
## License
|
|
84
123
|
|
|
85
124
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'active_record'
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
if defined?(::Rails)
|
|
6
|
+
require_relative 'active_record/railtie'
|
|
7
|
+
end
|
|
8
|
+
|
|
5
9
|
require_relative 'active_record/middleware'
|
|
6
10
|
require_relative 'active_record/handler'
|
|
7
11
|
|
|
@@ -23,19 +27,36 @@ module RailsFailover
|
|
|
23
27
|
@verify_primary_frequency_seconds || 5
|
|
24
28
|
end
|
|
25
29
|
|
|
26
|
-
def self.establish_reading_connection(connection_spec)
|
|
30
|
+
def self.establish_reading_connection(handler, connection_spec)
|
|
27
31
|
config = connection_spec.config
|
|
28
32
|
|
|
29
33
|
if config[:replica_host] && config[:replica_port]
|
|
30
34
|
replica_config = config.dup
|
|
31
|
-
|
|
32
35
|
replica_config[:host] = replica_config.delete(:replica_host)
|
|
33
36
|
replica_config[:port] = replica_config.delete(:replica_port)
|
|
34
37
|
replica_config[:replica] = true
|
|
35
|
-
|
|
36
|
-
handler = ::ActiveRecord::Base.connection_handlers[::ActiveRecord::Base.reading_role]
|
|
37
38
|
handler.establish_connection(replica_config)
|
|
38
39
|
end
|
|
39
40
|
end
|
|
41
|
+
|
|
42
|
+
def self.register_force_reading_role_callback(&block)
|
|
43
|
+
Middleware.force_reading_role_callback = block
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.on_failover(&block)
|
|
47
|
+
@on_failover_callback = block
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.on_failover_callback
|
|
51
|
+
@on_failover_callback
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.on_fallback(&block)
|
|
55
|
+
@on_fallback_callback = block
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.on_fallback_callback
|
|
59
|
+
@on_fallback_callback
|
|
60
|
+
end
|
|
40
61
|
end
|
|
41
62
|
end
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require 'singleton'
|
|
3
3
|
require 'monitor'
|
|
4
|
-
require 'listen'
|
|
5
|
-
require 'fileutils'
|
|
6
4
|
|
|
7
5
|
module RailsFailover
|
|
8
6
|
module ActiveRecord
|
|
@@ -10,61 +8,45 @@ module RailsFailover
|
|
|
10
8
|
include Singleton
|
|
11
9
|
include MonitorMixin
|
|
12
10
|
|
|
13
|
-
SEPERATOR = "__RAILS_FAILOVER__"
|
|
14
11
|
VERIFY_FREQUENCY_BUFFER_PRECENT = 20
|
|
15
12
|
|
|
16
13
|
def initialize
|
|
17
14
|
@primaries_down = {}
|
|
18
15
|
@ancestor_pid = Process.pid
|
|
19
16
|
|
|
20
|
-
@dir = '/tmp/rails_failover'
|
|
21
|
-
FileUtils.remove_dir(@dir) if Dir.exists?(@dir)
|
|
22
|
-
FileUtils.mkdir_p(@dir)
|
|
23
|
-
|
|
24
|
-
@listener = Listen.to(@dir) do |modified, added, removed|
|
|
25
|
-
if added.length > 0
|
|
26
|
-
added.each do |f|
|
|
27
|
-
pid, handler_key = File.basename(f).split(SEPERATOR)
|
|
28
|
-
|
|
29
|
-
if Process.pid != pid
|
|
30
|
-
verify_primary(handler_key.to_sym, publish: false)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
if removed.length > 0
|
|
36
|
-
removed.each do |f|
|
|
37
|
-
pid, handler_key = File.basename(f).split(SEPERATOR)
|
|
38
|
-
|
|
39
|
-
if Process.pid != pid
|
|
40
|
-
primary_up(handler_key.to_sym)
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
17
|
super() # Monitor#initialize
|
|
47
18
|
end
|
|
48
19
|
|
|
49
|
-
def
|
|
50
|
-
@listener.start
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def verify_primary(handler_key, publish: true)
|
|
20
|
+
def verify_primary(handler_key)
|
|
54
21
|
mon_synchronize do
|
|
55
22
|
primary_down(handler_key)
|
|
56
|
-
|
|
57
|
-
|
|
23
|
+
return if @thread&.alive?
|
|
24
|
+
|
|
25
|
+
logger.warn "Failover for ActiveRecord has been initiated"
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
RailsFailover::ActiveRecord.on_failover_callback&.call
|
|
29
|
+
rescue => e
|
|
30
|
+
logger.warn("RailsFailover::ActiveRecord.on_failover_callback failed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}")
|
|
31
|
+
end
|
|
58
32
|
|
|
59
33
|
@thread = Thread.new do
|
|
60
34
|
loop do
|
|
61
35
|
initiate_fallback_to_primary
|
|
62
|
-
|
|
36
|
+
|
|
37
|
+
if all_primaries_up
|
|
38
|
+
logger.warn "Fallback to primary for ActiveRecord has been completed."
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
RailsFailover::ActiveRecord.on_fallback_callback&.call
|
|
42
|
+
rescue => e
|
|
43
|
+
logger.warn("RailsFailover::ActiveRecord.on_fallback_callback failed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
break
|
|
47
|
+
end
|
|
63
48
|
end
|
|
64
49
|
end
|
|
65
|
-
|
|
66
|
-
@thread["pid"] = Process.pid
|
|
67
|
-
@thread
|
|
68
50
|
end
|
|
69
51
|
end
|
|
70
52
|
|
|
@@ -78,27 +60,26 @@ module RailsFailover
|
|
|
78
60
|
connection_handler = ::ActiveRecord::Base.connection_handlers[handler_key]
|
|
79
61
|
spec = connection_handler.retrieve_connection_pool(spec_name).spec
|
|
80
62
|
config = spec.config
|
|
81
|
-
logger.
|
|
63
|
+
logger.debug "#{Process.pid} Checking server for '#{handler_key} #{spec_name}'..."
|
|
82
64
|
connection_active = false
|
|
83
65
|
|
|
84
66
|
begin
|
|
85
67
|
connection = ::ActiveRecord::Base.public_send(spec.adapter_method, config)
|
|
86
68
|
connection_active = connection.active?
|
|
87
69
|
rescue => e
|
|
88
|
-
logger.
|
|
70
|
+
logger.debug "#{Process.pid} Connection to server for '#{handler_key} #{spec_name}' failed with '#{e.message}'"
|
|
89
71
|
ensure
|
|
90
72
|
connection.disconnect! if connection
|
|
91
73
|
end
|
|
92
74
|
|
|
93
75
|
if connection_active
|
|
94
|
-
logger.
|
|
76
|
+
logger.debug "#{Process.pid} Server for '#{handler_key} #{spec_name}' is active."
|
|
95
77
|
active_handler_keys << handler_key
|
|
96
78
|
end
|
|
97
79
|
end
|
|
98
80
|
|
|
99
81
|
active_handler_keys.each do |handler_key|
|
|
100
82
|
primary_up(handler_key)
|
|
101
|
-
publish_primary_up(handler_key)
|
|
102
83
|
end
|
|
103
84
|
end
|
|
104
85
|
|
|
@@ -114,25 +95,12 @@ module RailsFailover
|
|
|
114
95
|
end
|
|
115
96
|
end
|
|
116
97
|
|
|
117
|
-
|
|
118
98
|
def primary_down(handler_key)
|
|
119
99
|
mon_synchronize do
|
|
120
100
|
primaries_down[handler_key] = true
|
|
121
101
|
end
|
|
122
102
|
end
|
|
123
103
|
|
|
124
|
-
def publish_primary_down(handler_key)
|
|
125
|
-
FileUtils.touch("#{@dir}/#{Process.pid}#{SEPERATOR}#{handler_key}")
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def publish_primary_up(handler_key)
|
|
129
|
-
path = "#{@dir}/#{Process.pid}#{SEPERATOR}#{handler_key}"
|
|
130
|
-
|
|
131
|
-
if File.exists?(path)
|
|
132
|
-
FileUtils.rm("#{@dir}/#{Process.pid}#{SEPERATOR}#{handler_key}")
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
104
|
def primary_up(handler_key)
|
|
137
105
|
mon_synchronize do
|
|
138
106
|
primaries_down.delete(handler_key)
|
|
@@ -165,7 +133,7 @@ module RailsFailover
|
|
|
165
133
|
end
|
|
166
134
|
|
|
167
135
|
def logger
|
|
168
|
-
Rails.logger
|
|
136
|
+
::Rails.logger
|
|
169
137
|
end
|
|
170
138
|
end
|
|
171
139
|
end
|
|
@@ -2,39 +2,100 @@
|
|
|
2
2
|
|
|
3
3
|
module RailsFailover
|
|
4
4
|
module ActiveRecord
|
|
5
|
+
class Interceptor
|
|
6
|
+
def self.adapter_errors
|
|
7
|
+
@adapter_errors ||= begin
|
|
8
|
+
if defined?(::PG)
|
|
9
|
+
[::PG::UnableToSend, ::PG::ConnectionBad]
|
|
10
|
+
elsif defined?(::Mysql2)
|
|
11
|
+
[::Mysql2::Error::ConnectionError]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.handle(request, exception)
|
|
17
|
+
exception = resolve_cause(exception)
|
|
18
|
+
|
|
19
|
+
if adapter_errors.any? { |error| exception.is_a?(error) }
|
|
20
|
+
Handler.instance.verify_primary(request.env[Middleware::WRITING_ROLE_HEADER])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.resolve_cause(exception)
|
|
25
|
+
if exception.cause
|
|
26
|
+
resolve_cause(exception.cause)
|
|
27
|
+
else
|
|
28
|
+
exception
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
5
33
|
class Middleware
|
|
34
|
+
class << self
|
|
35
|
+
attr_accessor :force_reading_role_callback
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
CURRENT_ROLE_HEADER = "rails_failover.role"
|
|
39
|
+
WRITING_ROLE_HEADER = "rails_failover.writing_role"
|
|
40
|
+
|
|
6
41
|
def initialize(app)
|
|
7
42
|
@app = app
|
|
8
43
|
end
|
|
9
44
|
|
|
10
45
|
def call(env)
|
|
11
|
-
|
|
46
|
+
current_role = ::ActiveRecord::Base.current_role || ::ActiveRecord::Base.writing_role
|
|
47
|
+
is_writing_role = current_role.to_s.end_with?(::ActiveRecord::Base.writing_role.to_s)
|
|
48
|
+
writing_role = resolve_writing_role(current_role, is_writing_role)
|
|
12
49
|
|
|
13
50
|
role =
|
|
14
|
-
if primary_down = Handler.instance.primary_down?(writing_role)
|
|
15
|
-
|
|
51
|
+
if primary_down = self.class.force_reading_role_callback&.call(env) || Handler.instance.primary_down?(writing_role)
|
|
52
|
+
reading_role = resolve_reading_role(current_role, is_writing_role)
|
|
53
|
+
ensure_reading_connection_established!(writing_role: writing_role, reading_role: reading_role)
|
|
54
|
+
reading_role
|
|
16
55
|
else
|
|
17
|
-
|
|
56
|
+
writing_role
|
|
18
57
|
end
|
|
19
58
|
|
|
20
59
|
::ActiveRecord::Base.connected_to(role: role) do
|
|
21
|
-
env[
|
|
60
|
+
env[CURRENT_ROLE_HEADER] = role
|
|
61
|
+
env[WRITING_ROLE_HEADER] = writing_role
|
|
22
62
|
@app.call(env)
|
|
23
63
|
end
|
|
24
|
-
rescue Exception => e
|
|
25
|
-
if (resolve_cause(e).is_a?(::PG::Error))
|
|
26
|
-
Handler.instance.verify_primary(writing_role)
|
|
27
|
-
raise
|
|
28
|
-
end
|
|
29
64
|
end
|
|
30
65
|
|
|
31
66
|
private
|
|
32
67
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
68
|
+
def ensure_reading_connection_established!(writing_role:, reading_role:)
|
|
69
|
+
::ActiveRecord::Base.connection_handlers[reading_role] ||= begin
|
|
70
|
+
handler = ::ActiveRecord::ConnectionAdapters::ConnectionHandler.new
|
|
71
|
+
|
|
72
|
+
::ActiveRecord::Base.connection_handlers[writing_role].connection_pools.each do |pool|
|
|
73
|
+
::RailsFailover::ActiveRecord.establish_reading_connection(handler, pool.spec)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
handler
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def resolve_writing_role(current_role, is_writing_role)
|
|
81
|
+
if is_writing_role
|
|
82
|
+
current_role
|
|
83
|
+
else
|
|
84
|
+
current_role.to_s.sub(
|
|
85
|
+
/#{::ActiveRecord::Base.reading_role}$/,
|
|
86
|
+
::ActiveRecord::Base.writing_role.to_s
|
|
87
|
+
).to_sym
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def resolve_reading_role(current_role, is_writing_role)
|
|
92
|
+
if is_writing_role
|
|
93
|
+
current_role.to_s.sub(
|
|
94
|
+
/#{::ActiveRecord::Base.writing_role}$/,
|
|
95
|
+
::ActiveRecord::Base.reading_role.to_s
|
|
96
|
+
).to_sym
|
|
36
97
|
else
|
|
37
|
-
|
|
98
|
+
current_role
|
|
38
99
|
end
|
|
39
100
|
end
|
|
40
101
|
end
|
|
@@ -1,35 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module RailsFailover
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
class Railtie < ::Rails::Railtie
|
|
4
|
-
initializer "rails_failover.init", after: "active_record.initialize_database" do
|
|
5
|
-
::
|
|
6
|
-
|
|
6
|
+
initializer "rails_failover.init", after: "active_record.initialize_database" do |app|
|
|
7
|
+
config = ::ActiveRecord::Base.connection_config
|
|
8
|
+
app.config.active_record_rails_failover = false
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
::ActiveRecord::Base.connection_handlers[::ActiveRecord::Base.reading_role] =
|
|
11
|
-
::ActiveRecord::ConnectionAdapters::ConnectionHandler.new
|
|
10
|
+
if !!(config[:replica_host] && config[:replica_port])
|
|
11
|
+
app.config.active_record_rails_failover = true
|
|
12
12
|
|
|
13
|
-
::
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
::ActiveSupport.on_load(:active_record) do
|
|
14
|
+
Handler.instance
|
|
15
|
+
|
|
16
|
+
# We are doing this manually for now since we're awaiting Rails 6.1 to be released which will
|
|
17
|
+
# have more stable ActiveRecord APIs for handling multiple databases with different roles.
|
|
18
|
+
::ActiveRecord::Base.connection_handlers[::ActiveRecord::Base.reading_role] =
|
|
19
|
+
::ActiveRecord::ConnectionAdapters::ConnectionHandler.new
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
::ActiveRecord::Base.connection_handlers[::ActiveRecord::Base.writing_role].connection_pools.each do |connection_pool|
|
|
22
|
+
RailsFailover::ActiveRecord.establish_reading_connection(
|
|
23
|
+
::ActiveRecord::Base.connection_handlers[::ActiveRecord::Base.reading_role],
|
|
24
|
+
connection_pool.spec
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
::ActiveRecord::Base.connection
|
|
30
|
+
rescue ::ActiveRecord::NoDatabaseError
|
|
31
|
+
# Do nothing since database hasn't been created
|
|
32
|
+
rescue ::PG::Error => e
|
|
33
|
+
Handler.instance.verify_primary(::ActiveRecord::Base.writing_role)
|
|
34
|
+
::ActiveRecord::Base.connection_handler = ::ActiveRecord::Base.lookup_connection_handler(:reading)
|
|
35
|
+
end
|
|
24
36
|
end
|
|
25
37
|
end
|
|
26
38
|
end
|
|
27
39
|
|
|
28
40
|
initializer "rails_failover.insert_middleware" do |app|
|
|
29
|
-
app.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
if app.config.active_record_rails_failover
|
|
42
|
+
ActionDispatch::DebugExceptions.register_interceptor do |request, exception|
|
|
43
|
+
RailsFailover::ActiveRecord::Interceptor.handle(request, exception)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if !skip_middleware?(app.config)
|
|
47
|
+
app.middleware.unshift(::RailsFailover::ActiveRecord::Middleware)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def skip_middleware?(config)
|
|
53
|
+
return false if !config.respond_to?(:skip_rails_failover_active_record_middleware)
|
|
54
|
+
config.skip_rails_failover_active_record_middleware
|
|
33
55
|
end
|
|
34
56
|
end
|
|
35
57
|
end
|
data/lib/rails_failover/redis.rb
CHANGED
|
@@ -18,33 +18,41 @@ module RailsFailover
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def self.logger
|
|
21
|
-
@logger
|
|
21
|
+
if @logger
|
|
22
|
+
@logger
|
|
23
|
+
elsif defined?(::Rails)
|
|
24
|
+
::Rails.logger
|
|
25
|
+
end
|
|
22
26
|
end
|
|
23
27
|
|
|
24
|
-
def self.
|
|
25
|
-
@
|
|
28
|
+
def self.verify_primary_frequency_seconds=(seconds)
|
|
29
|
+
@verify_primary_frequency_seconds = seconds
|
|
26
30
|
end
|
|
27
31
|
|
|
28
|
-
def self.
|
|
29
|
-
@
|
|
32
|
+
def self.verify_primary_frequency_seconds
|
|
33
|
+
@verify_primary_frequency_seconds || 5
|
|
30
34
|
end
|
|
31
35
|
|
|
32
|
-
def self.
|
|
33
|
-
@
|
|
34
|
-
@master_down_callbacks.push(block)
|
|
36
|
+
def self.on_failover(&block)
|
|
37
|
+
@on_failover_callback = block
|
|
35
38
|
end
|
|
36
39
|
|
|
37
|
-
def self.
|
|
38
|
-
@
|
|
40
|
+
def self.on_failover_callback
|
|
41
|
+
@on_failover_callback
|
|
39
42
|
end
|
|
40
43
|
|
|
41
|
-
def self.
|
|
42
|
-
@
|
|
43
|
-
@master_up_callbacks.push(block)
|
|
44
|
+
def self.on_fallback(&block)
|
|
45
|
+
@on_fallback_callback = block
|
|
44
46
|
end
|
|
45
47
|
|
|
46
|
-
def self.
|
|
47
|
-
@
|
|
48
|
+
def self.on_fallback_callback
|
|
49
|
+
@on_fallback_callback
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# For testing
|
|
53
|
+
def self.clear_callbacks
|
|
54
|
+
@on_fallback_callback = nil
|
|
55
|
+
@on_failover_callback = nil
|
|
48
56
|
end
|
|
49
57
|
end
|
|
50
58
|
end
|
|
@@ -6,7 +6,7 @@ module RailsFailover
|
|
|
6
6
|
class Redis
|
|
7
7
|
class Connector < ::Redis::Client::Connector
|
|
8
8
|
def initialize(options)
|
|
9
|
-
|
|
9
|
+
orignal_driver = options[:driver]
|
|
10
10
|
|
|
11
11
|
options[:driver] = Class.new(options[:driver]) do
|
|
12
12
|
def self.connect(options)
|
|
@@ -22,18 +22,24 @@ module RailsFailover
|
|
|
22
22
|
Errno::ETIMEDOUT,
|
|
23
23
|
Errno::EINVAL => e
|
|
24
24
|
|
|
25
|
-
Handler.instance.
|
|
25
|
+
Handler.instance.verify_primary(options)
|
|
26
26
|
raise e
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
options[:original_driver] = orignal_driver
|
|
30
31
|
options.delete(:connector)
|
|
31
|
-
|
|
32
|
+
options[:id] ||= "#{options[:host]}:#{options[:port]}"
|
|
32
33
|
@replica_options = replica_options(options)
|
|
34
|
+
@options = options.dup
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
def resolve
|
|
36
|
-
Handler.instance.
|
|
38
|
+
if Handler.instance.primary_down?(@options)
|
|
39
|
+
@replica_options
|
|
40
|
+
else
|
|
41
|
+
@options
|
|
42
|
+
end
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
def check(client)
|
|
@@ -50,7 +56,6 @@ module RailsFailover
|
|
|
50
56
|
opts = options.dup
|
|
51
57
|
opts[:host] = opts.delete(:replica_host)
|
|
52
58
|
opts[:port] = opts.delete(:replica_port)
|
|
53
|
-
opts[:driver] = opts.delete(:original_driver)
|
|
54
59
|
opts
|
|
55
60
|
end
|
|
56
61
|
end
|
|
@@ -9,100 +9,184 @@ module RailsFailover
|
|
|
9
9
|
include Singleton
|
|
10
10
|
include MonitorMixin
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
PRIMARY_ROLE_STATUS = "role:master"
|
|
13
|
+
PRIMARY_LOADED_STATUS = "loading:0"
|
|
14
|
+
VERIFY_FREQUENCY_BUFFER_PRECENT = 20
|
|
14
15
|
|
|
15
16
|
def initialize
|
|
16
|
-
@
|
|
17
|
-
@clients =
|
|
17
|
+
@primaries_down = {}
|
|
18
|
+
@clients = {}
|
|
19
|
+
@ancestor_pid = Process.pid
|
|
18
20
|
|
|
19
21
|
super() # Monitor#initialize
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
def
|
|
24
|
+
def verify_primary(options)
|
|
23
25
|
mon_synchronize do
|
|
26
|
+
primary_down(options)
|
|
27
|
+
disconnect_clients(options)
|
|
28
|
+
|
|
24
29
|
return if @thread&.alive?
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
logger&.warn "Failover for Redis has been initiated"
|
|
32
|
+
|
|
33
|
+
begin
|
|
34
|
+
RailsFailover::Redis.on_failover_callback&.call
|
|
35
|
+
rescue => e
|
|
36
|
+
logger&.warn("RailsFailover::Redis.on_failover_callback failed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}")
|
|
37
|
+
end
|
|
29
38
|
|
|
30
39
|
@thread = Thread.new do
|
|
31
40
|
loop do
|
|
32
|
-
thread = Thread.new {
|
|
41
|
+
thread = Thread.new { initiate_fallback_to_primary }
|
|
33
42
|
thread.join
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
|
|
44
|
+
if all_primaries_up
|
|
45
|
+
logger&.warn "Fallback to primary for Redis has been completed."
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
RailsFailover::Redis.on_fallback_callback&.call
|
|
49
|
+
rescue => e
|
|
50
|
+
logger&.warn("RailsFailover::Redis.on_fallback_callback failed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
break
|
|
54
|
+
end
|
|
38
55
|
end
|
|
39
56
|
end
|
|
40
57
|
end
|
|
41
58
|
end
|
|
42
59
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
def initiate_fallback_to_primary
|
|
61
|
+
frequency = RailsFailover::Redis.verify_primary_frequency_seconds
|
|
62
|
+
sleep(frequency * ((rand(VERIFY_FREQUENCY_BUFFER_PRECENT) + 100) / 100.0))
|
|
63
|
+
|
|
64
|
+
active_primaries_keys = {}
|
|
65
|
+
|
|
66
|
+
primaries_down.each do |key, options|
|
|
67
|
+
info = nil
|
|
68
|
+
options = options.dup
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
options[:driver] = options[:original_driver]
|
|
72
|
+
primary_client = ::Redis::Client.new(options)
|
|
73
|
+
logger&.debug "Checking connection to primary server (#{key})"
|
|
74
|
+
info = primary_client.call([:info])
|
|
75
|
+
rescue => e
|
|
76
|
+
logger&.debug "Connection to primary server (#{key}) failed with '#{e.message}'"
|
|
77
|
+
ensure
|
|
78
|
+
primary_client&.disconnect
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if info && info.include?(PRIMARY_LOADED_STATUS) && info.include?(PRIMARY_ROLE_STATUS)
|
|
82
|
+
active_primaries_keys[key] = options
|
|
83
|
+
logger&.debug "Primary server (#{key}) is active, disconnecting clients from replica"
|
|
84
|
+
end
|
|
54
85
|
end
|
|
55
86
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
disconnect_clients
|
|
60
|
-
RailsFailover::Redis.master_up_callbacks.each { |callback| callback.call }
|
|
87
|
+
active_primaries_keys.each do |key, options|
|
|
88
|
+
primary_up(options)
|
|
89
|
+
disconnect_clients(options)
|
|
61
90
|
end
|
|
62
91
|
end
|
|
63
92
|
|
|
64
93
|
def register_client(client)
|
|
94
|
+
key = client.options[:id]
|
|
95
|
+
|
|
65
96
|
mon_synchronize do
|
|
66
|
-
|
|
97
|
+
clients[key] ||= []
|
|
98
|
+
clients[key] << client
|
|
67
99
|
end
|
|
68
100
|
end
|
|
69
101
|
|
|
70
102
|
def deregister_client(client)
|
|
103
|
+
key = client.options[:id]
|
|
104
|
+
|
|
71
105
|
mon_synchronize do
|
|
72
|
-
|
|
106
|
+
if clients[key]
|
|
107
|
+
clients[key].delete(client)
|
|
108
|
+
|
|
109
|
+
if clients[key].empty?
|
|
110
|
+
clients.delete(key)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
73
113
|
end
|
|
74
114
|
end
|
|
75
115
|
|
|
76
|
-
def
|
|
77
|
-
mon_synchronize
|
|
116
|
+
def primary_down?(options)
|
|
117
|
+
mon_synchronize do
|
|
118
|
+
primaries_down[options[:id]]
|
|
119
|
+
end
|
|
78
120
|
end
|
|
79
121
|
|
|
80
|
-
|
|
81
|
-
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def all_primaries_up
|
|
125
|
+
mon_synchronize { primaries_down.empty? }
|
|
82
126
|
end
|
|
83
127
|
|
|
84
|
-
def
|
|
85
|
-
mon_synchronize
|
|
128
|
+
def primary_up(options)
|
|
129
|
+
mon_synchronize do
|
|
130
|
+
primaries_down.delete(options[:id])
|
|
131
|
+
end
|
|
86
132
|
end
|
|
87
133
|
|
|
88
|
-
|
|
134
|
+
def primary_down(options)
|
|
135
|
+
mon_synchronize do
|
|
136
|
+
primaries_down[options[:id]] = options.dup
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def clients
|
|
141
|
+
process_pid = Process.pid
|
|
142
|
+
return @clients[process_pid] if @clients[process_pid]
|
|
89
143
|
|
|
90
|
-
def disconnect_clients
|
|
91
144
|
mon_synchronize do
|
|
92
|
-
|
|
93
|
-
|
|
145
|
+
if !@clients[process_pid]
|
|
146
|
+
@clients[process_pid] = {}
|
|
147
|
+
|
|
148
|
+
if process_pid != @ancestor_pid
|
|
149
|
+
@clients.delete(@ancestor_pid)
|
|
150
|
+
end
|
|
94
151
|
end
|
|
152
|
+
|
|
153
|
+
@clients[process_pid]
|
|
95
154
|
end
|
|
96
155
|
end
|
|
97
156
|
|
|
98
|
-
def
|
|
99
|
-
|
|
100
|
-
|
|
157
|
+
def primaries_down
|
|
158
|
+
process_pid = Process.pid
|
|
159
|
+
return @primaries_down[process_pid] if @primaries_down[process_pid]
|
|
160
|
+
|
|
161
|
+
mon_synchronize do
|
|
162
|
+
if !@primaries_down[process_pid]
|
|
163
|
+
@primaries_down[process_pid] = @primaries_down[@ancestor_pid] || {}
|
|
164
|
+
|
|
165
|
+
if process_pid != @ancestor_pid
|
|
166
|
+
@primaries_down.delete(@ancestor_pid).each do |id, options|
|
|
167
|
+
verify_primary(options)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
@primaries_down[process_pid]
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def disconnect_clients(options)
|
|
177
|
+
key = options[:id]
|
|
178
|
+
|
|
179
|
+
mon_synchronize do
|
|
180
|
+
if clients[key]
|
|
181
|
+
clients[key].dup.each do |c|
|
|
182
|
+
c.disconnect
|
|
183
|
+
end
|
|
184
|
+
end
|
|
101
185
|
end
|
|
102
186
|
end
|
|
103
187
|
|
|
104
|
-
def
|
|
105
|
-
|
|
188
|
+
def logger
|
|
189
|
+
RailsFailover::Redis.logger
|
|
106
190
|
end
|
|
107
191
|
end
|
|
108
192
|
end
|
data/makefile
CHANGED
|
@@ -12,7 +12,7 @@ setup_dummy_rails_server:
|
|
|
12
12
|
@cd spec/support/dummy_app && bundle install --quiet --without test --without development && yarn install && RAILS_ENV=production $(BUNDLER_BIN) exec rails db:create db:migrate db:seed
|
|
13
13
|
|
|
14
14
|
start_dummy_rails_server:
|
|
15
|
-
@cd spec/support/dummy_app && BUNDLE_GEMFILE=Gemfile bundle exec unicorn -c config/unicorn.conf.rb -D -E production
|
|
15
|
+
@cd spec/support/dummy_app && BUNDLE_GEMFILE=Gemfile UNICORN_WORKERS=5 SECRET_KEY_BASE=somekey bundle exec unicorn -c config/unicorn.conf.rb -D -E production
|
|
16
16
|
|
|
17
17
|
stop_dummy_rails_server:
|
|
18
18
|
@kill -TERM $(shell cat spec/support/dummy_app/tmp/pids/unicorn.pid)
|
data/rails_failover.gemspec
CHANGED
|
@@ -21,8 +21,6 @@ Gem::Specification.new do |spec|
|
|
|
21
21
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
22
22
|
spec.require_paths = ["lib"]
|
|
23
23
|
|
|
24
|
-
spec.add_dependency 'listen', "~> 3.2"
|
|
25
|
-
|
|
26
24
|
["activerecord", "railties"].each do |gem_name|
|
|
27
25
|
spec.add_dependency gem_name, "~> 6.0"
|
|
28
26
|
end
|
data/redis.mk
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
REDIS_PORT := 6381
|
|
2
2
|
REDIS_PID_PATH := /tmp/redis.pid
|
|
3
3
|
REDIS_SOCKET_PATH := /tmp/redis.sock
|
|
4
|
-
REDIS_DBFILENAME :=
|
|
4
|
+
REDIS_DBFILENAME := primary.rdb
|
|
5
5
|
REDIS_REPLICA_PORT := 6382
|
|
6
6
|
REDIS_REPLICA_PID_PATH := /tmp/redis_replica.pid
|
|
7
7
|
REDIS_REPLICA_SOCKET_PATH := /tmp/redis_replica.sock
|
|
@@ -12,17 +12,17 @@ redis: start_redis test_redis stop_redis
|
|
|
12
12
|
test_redis:
|
|
13
13
|
@REDIS=1 bundle exec rspec --tag type:redis ${RSPEC_PATH}
|
|
14
14
|
|
|
15
|
-
start_redis:
|
|
16
|
-
stop_redis: stop_redis_replica
|
|
15
|
+
start_redis: start_redis_primary start_redis_replica
|
|
16
|
+
stop_redis: stop_redis_replica stop_redis_primary
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
stop_redis_primary:
|
|
19
19
|
@redis-cli -p ${REDIS_PORT} shutdown
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
start_redis_primary:
|
|
22
22
|
@redis-server --daemonize yes --pidfile ${REDIS_PID_PATH} --port ${REDIS_PORT} --unixsocket ${REDIS_SOCKET_PATH} --dbfilename ${REDIS_DBFILENAME} --logfile /dev/null
|
|
23
23
|
|
|
24
24
|
stop_redis_replica:
|
|
25
25
|
@redis-cli -p ${REDIS_REPLICA_PORT} shutdown
|
|
26
26
|
|
|
27
27
|
start_redis_replica:
|
|
28
|
-
@redis-server --daemonize yes --pidfile ${REDIS_REPLICA_PID_PATH} --port ${REDIS_REPLICA_PORT} --unixsocket ${REDIS_REPLICA_SOCKET_PATH} --
|
|
28
|
+
@redis-server --daemonize yes --pidfile ${REDIS_REPLICA_PID_PATH} --port ${REDIS_REPLICA_PORT} --unixsocket ${REDIS_REPLICA_SOCKET_PATH} --slaveof 127.0.0.1 ${REDIS_PORT} --dbfilename ${REDIS_REPLICA_DBFILENAME} --logfile /dev/null
|
metadata
CHANGED
|
@@ -1,29 +1,15 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_failover
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3
|
|
4
|
+
version: 0.5.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alan Tan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2020-
|
|
11
|
+
date: 2020-07-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
-
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: listen
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - "~>"
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: '3.2'
|
|
20
|
-
type: :runtime
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - "~>"
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: '3.2'
|
|
27
13
|
- !ruby/object:Gem::Dependency
|
|
28
14
|
name: activerecord
|
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -63,6 +49,7 @@ files:
|
|
|
63
49
|
- ".rspec"
|
|
64
50
|
- ".rubocop.yml"
|
|
65
51
|
- ".travis.yml"
|
|
52
|
+
- CHANGELOG.md
|
|
66
53
|
- CODE_OF_CONDUCT.md
|
|
67
54
|
- Gemfile
|
|
68
55
|
- Gemfile.lock
|
|
@@ -105,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
105
92
|
- !ruby/object:Gem::Version
|
|
106
93
|
version: '0'
|
|
107
94
|
requirements: []
|
|
108
|
-
rubygems_version: 3.
|
|
95
|
+
rubygems_version: 3.1.2
|
|
109
96
|
signing_key:
|
|
110
97
|
specification_version: 4
|
|
111
98
|
summary: Failover for ActiveRecord and Redis
|