rails_failover 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +6 -0
- data/Gemfile.lock +72 -2
- data/README.md +33 -3
- data/bin/console +2 -1
- data/bin/rspec +1 -1
- data/lib/rails_failover.rb +0 -2
- data/lib/rails_failover/active_record.rb +41 -0
- data/lib/rails_failover/active_record/handler.rb +172 -0
- data/lib/rails_failover/active_record/middleware.rb +42 -0
- data/lib/rails_failover/active_record/railtie.rb +36 -0
- data/lib/rails_failover/redis.rb +9 -0
- data/lib/rails_failover/redis/connector.rb +6 -6
- data/lib/rails_failover/redis/{failover_handler.rb → handler.rb} +1 -1
- data/lib/rails_failover/version.rb +1 -1
- data/makefile +14 -24
- data/postgresql.mk +54 -0
- data/rails_failover.gemspec +6 -2
- data/redis.mk +28 -0
- metadata +41 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 307117d6a7083a17a2bf2799737f6f9be3afa77a58bace4f867bedf38fa05a5c
|
4
|
+
data.tar.gz: 9d9d54a7a46785f259ba3f19aad402534de7b5305bd57993cb9c92fd14efad3b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7c45a722a9d8bbe1d0ea1410630252c5485496a49b6affe5461615779102d25bfafbf0d990bce920d8e52f164ebe1b92e94a1ccd1787d526b98cba54fc039d45
|
7
|
+
data.tar.gz: 3dcd9668ef0266b91c35cb3764533fab3624025a3d4dd6db86ce1adafb8168c9d72dde14bbd57fbcb73f7f0049941e6b077eaba519b70b3fa1b03eb92d730b64
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,20 +1,82 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rails_failover (0.
|
5
|
-
|
4
|
+
rails_failover (0.3.0)
|
5
|
+
activerecord (~> 6.0)
|
6
|
+
listen (~> 3.2)
|
7
|
+
railties (~> 6.0)
|
6
8
|
|
7
9
|
GEM
|
8
10
|
remote: https://rubygems.org/
|
9
11
|
specs:
|
12
|
+
actionpack (6.0.3.1)
|
13
|
+
actionview (= 6.0.3.1)
|
14
|
+
activesupport (= 6.0.3.1)
|
15
|
+
rack (~> 2.0, >= 2.0.8)
|
16
|
+
rack-test (>= 0.6.3)
|
17
|
+
rails-dom-testing (~> 2.0)
|
18
|
+
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
19
|
+
actionview (6.0.3.1)
|
20
|
+
activesupport (= 6.0.3.1)
|
21
|
+
builder (~> 3.1)
|
22
|
+
erubi (~> 1.4)
|
23
|
+
rails-dom-testing (~> 2.0)
|
24
|
+
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
25
|
+
activemodel (6.0.3.1)
|
26
|
+
activesupport (= 6.0.3.1)
|
27
|
+
activerecord (6.0.3.1)
|
28
|
+
activemodel (= 6.0.3.1)
|
29
|
+
activesupport (= 6.0.3.1)
|
30
|
+
activesupport (6.0.3.1)
|
31
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
32
|
+
i18n (>= 0.7, < 2)
|
33
|
+
minitest (~> 5.1)
|
34
|
+
tzinfo (~> 1.1)
|
35
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
10
36
|
ast (2.4.0)
|
37
|
+
builder (3.2.4)
|
11
38
|
byebug (11.1.3)
|
39
|
+
concurrent-ruby (1.1.6)
|
40
|
+
crass (1.0.6)
|
12
41
|
diff-lcs (1.3)
|
42
|
+
erubi (1.9.0)
|
43
|
+
ffi (1.12.2)
|
44
|
+
i18n (1.8.2)
|
45
|
+
concurrent-ruby (~> 1.0)
|
46
|
+
listen (3.2.1)
|
47
|
+
rb-fsevent (~> 0.10, >= 0.10.3)
|
48
|
+
rb-inotify (~> 0.9, >= 0.9.10)
|
49
|
+
loofah (2.5.0)
|
50
|
+
crass (~> 1.0.2)
|
51
|
+
nokogiri (>= 1.5.9)
|
52
|
+
method_source (1.0.0)
|
53
|
+
mini_portile2 (2.4.0)
|
54
|
+
minitest (5.14.1)
|
55
|
+
nokogiri (1.10.9)
|
56
|
+
mini_portile2 (~> 2.4.0)
|
13
57
|
parallel (1.19.1)
|
14
58
|
parser (2.7.1.2)
|
15
59
|
ast (~> 2.4.0)
|
60
|
+
pg (1.2.3)
|
61
|
+
rack (2.2.2)
|
62
|
+
rack-test (1.1.0)
|
63
|
+
rack (>= 1.0, < 3)
|
64
|
+
rails-dom-testing (2.0.3)
|
65
|
+
activesupport (>= 4.2.0)
|
66
|
+
nokogiri (>= 1.6)
|
67
|
+
rails-html-sanitizer (1.3.0)
|
68
|
+
loofah (~> 2.3)
|
69
|
+
railties (6.0.3.1)
|
70
|
+
actionpack (= 6.0.3.1)
|
71
|
+
activesupport (= 6.0.3.1)
|
72
|
+
method_source
|
73
|
+
rake (>= 0.8.7)
|
74
|
+
thor (>= 0.20.3, < 2.0)
|
16
75
|
rainbow (3.0.0)
|
17
76
|
rake (12.3.3)
|
77
|
+
rb-fsevent (0.10.4)
|
78
|
+
rb-inotify (0.10.1)
|
79
|
+
ffi (~> 1.0)
|
18
80
|
redis (4.1.4)
|
19
81
|
rexml (3.2.4)
|
20
82
|
rspec (3.9.0)
|
@@ -43,15 +105,23 @@ GEM
|
|
43
105
|
rubocop-rspec (1.39.0)
|
44
106
|
rubocop (>= 0.68.1)
|
45
107
|
ruby-progressbar (1.10.1)
|
108
|
+
thor (1.0.1)
|
109
|
+
thread_safe (0.3.6)
|
110
|
+
tzinfo (1.2.7)
|
111
|
+
thread_safe (~> 0.1)
|
46
112
|
unicode-display_width (1.7.0)
|
113
|
+
zeitwerk (2.3.0)
|
47
114
|
|
48
115
|
PLATFORMS
|
49
116
|
ruby
|
50
117
|
|
51
118
|
DEPENDENCIES
|
119
|
+
activerecord (~> 6.0)
|
52
120
|
byebug
|
121
|
+
pg (~> 1.2)
|
53
122
|
rails_failover!
|
54
123
|
rake (~> 12.0)
|
124
|
+
redis (~> 4.1)
|
55
125
|
rspec (~> 3.0)
|
56
126
|
rubocop-discourse
|
57
127
|
|
data/README.md
CHANGED
@@ -20,6 +20,22 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
|
+
### ActiveRecord
|
24
|
+
|
25
|
+
In `config/application.rb` add `require 'rails_failover/active_record'` after `require "active_record/railtie"`.
|
26
|
+
|
27
|
+
In your database configuration, simply add `replica_host` and `replica_port` to your database configuration.
|
28
|
+
|
29
|
+
```
|
30
|
+
production:
|
31
|
+
host: <primary db server host>
|
32
|
+
port: <primary db server port>
|
33
|
+
replica_host: <replica db server host>
|
34
|
+
replica_port: <replica db server port>
|
35
|
+
```
|
36
|
+
|
37
|
+
The gem will automatically create an `ActiveRecord::ConnectionAdapters::ConnectionHandler` with the `ActiveRecord::Base.reading_role` as the `handler_key`.
|
38
|
+
|
23
39
|
### Redis
|
24
40
|
|
25
41
|
```
|
@@ -41,13 +57,27 @@ end
|
|
41
57
|
|
42
58
|
## Development
|
43
59
|
|
44
|
-
After checking out the repo, run `bin/setup` to install dependencies.
|
60
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
45
61
|
|
46
62
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
47
63
|
|
64
|
+
### Testing
|
65
|
+
|
66
|
+
#### ActiveRecord
|
67
|
+
|
68
|
+
The ActiveRecord failover tests are run against a dummy Rails server. Run the following commands to run the test:
|
69
|
+
|
70
|
+
1. `make setup_pg`
|
71
|
+
1. `make start_pg`
|
72
|
+
1. `bin/rspec active_record`. You may also run the tests with more unicorn workers by adding the `UNICORN_WORKERS` env variable.
|
73
|
+
|
74
|
+
#### Redis
|
75
|
+
|
76
|
+
`bin/rspec redis`
|
77
|
+
|
48
78
|
## Contributing
|
49
79
|
|
50
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
80
|
+
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).
|
51
81
|
|
52
82
|
|
53
83
|
## License
|
@@ -56,4 +86,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
56
86
|
|
57
87
|
## Code of Conduct
|
58
88
|
|
59
|
-
Everyone interacting in the RailsFailover project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
89
|
+
Everyone interacting in the RailsFailover project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/discourse/rails_failover/blob/master/CODE_OF_CONDUCT.md).
|
data/bin/console
CHANGED
@@ -3,7 +3,8 @@
|
|
3
3
|
|
4
4
|
require "bundler/setup"
|
5
5
|
require "rails_failover"
|
6
|
-
require
|
6
|
+
require "rails_failover/redis"
|
7
|
+
require "rails_failover/active_record"
|
7
8
|
|
8
9
|
# You can add fixtures and/or initialization code here to make experimenting
|
9
10
|
# with your gem easier. You can also use a different console, if you like.
|
data/bin/rspec
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
#!/usr/bin/env bash
|
2
|
-
make
|
2
|
+
RSPEC_PATH=$2 make -s $1
|
data/lib/rails_failover.rb
CHANGED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require_relative 'active_record/railtie'
|
5
|
+
require_relative 'active_record/middleware'
|
6
|
+
require_relative 'active_record/handler'
|
7
|
+
|
8
|
+
module RailsFailover
|
9
|
+
module ActiveRecord
|
10
|
+
def self.logger=(logger)
|
11
|
+
@logger = logger
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.logger
|
15
|
+
@logger || Rails.logger
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.verify_primary_frequency_seconds=(seconds)
|
19
|
+
@verify_primary_frequency_seconds = seconds
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.verify_primary_frequency_seconds
|
23
|
+
@verify_primary_frequency_seconds || 5
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.establish_reading_connection(connection_spec)
|
27
|
+
config = connection_spec.config
|
28
|
+
|
29
|
+
if config[:replica_host] && config[:replica_port]
|
30
|
+
replica_config = config.dup
|
31
|
+
|
32
|
+
replica_config[:host] = replica_config.delete(:replica_host)
|
33
|
+
replica_config[:port] = replica_config.delete(:replica_port)
|
34
|
+
replica_config[:replica] = true
|
35
|
+
|
36
|
+
handler = ::ActiveRecord::Base.connection_handlers[::ActiveRecord::Base.reading_role]
|
37
|
+
handler.establish_connection(replica_config)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'singleton'
|
3
|
+
require 'monitor'
|
4
|
+
require 'listen'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module RailsFailover
|
8
|
+
module ActiveRecord
|
9
|
+
class Handler
|
10
|
+
include Singleton
|
11
|
+
include MonitorMixin
|
12
|
+
|
13
|
+
SEPERATOR = "__RAILS_FAILOVER__"
|
14
|
+
VERIFY_FREQUENCY_BUFFER_PRECENT = 20
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@primaries_down = {}
|
18
|
+
@ancestor_pid = Process.pid
|
19
|
+
|
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
|
+
super() # Monitor#initialize
|
47
|
+
end
|
48
|
+
|
49
|
+
def start_listener
|
50
|
+
@listener.start
|
51
|
+
end
|
52
|
+
|
53
|
+
def verify_primary(handler_key, publish: true)
|
54
|
+
mon_synchronize do
|
55
|
+
primary_down(handler_key)
|
56
|
+
publish_primary_down(handler_key) if publish
|
57
|
+
return if @thread&.alive? && @thread["pid"] == Process.pid
|
58
|
+
|
59
|
+
@thread = Thread.new do
|
60
|
+
loop do
|
61
|
+
initiate_fallback_to_primary
|
62
|
+
break if all_primaries_up
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
@thread["pid"] = Process.pid
|
67
|
+
@thread
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def initiate_fallback_to_primary
|
72
|
+
frequency = RailsFailover::ActiveRecord.verify_primary_frequency_seconds
|
73
|
+
sleep(frequency * ((rand(VERIFY_FREQUENCY_BUFFER_PRECENT) + 100) / 100.0))
|
74
|
+
|
75
|
+
active_handler_keys = []
|
76
|
+
|
77
|
+
primaries_down.keys.each do |handler_key|
|
78
|
+
connection_handler = ::ActiveRecord::Base.connection_handlers[handler_key]
|
79
|
+
spec = connection_handler.retrieve_connection_pool(spec_name).spec
|
80
|
+
config = spec.config
|
81
|
+
logger.warn "#{Process.pid} Checking server for '#{handler_key} #{spec_name}'..."
|
82
|
+
connection_active = false
|
83
|
+
|
84
|
+
begin
|
85
|
+
connection = ::ActiveRecord::Base.public_send(spec.adapter_method, config)
|
86
|
+
connection_active = connection.active?
|
87
|
+
rescue => e
|
88
|
+
logger.warn "#{Process.pid} Connection to server for '#{handler_key} #{spec_name}' failed with '#{e.message}'"
|
89
|
+
ensure
|
90
|
+
connection.disconnect! if connection
|
91
|
+
end
|
92
|
+
|
93
|
+
if connection_active
|
94
|
+
logger.warn "#{Process.pid} Server for '#{handler_key} #{spec_name}' is active."
|
95
|
+
active_handler_keys << handler_key
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
active_handler_keys.each do |handler_key|
|
100
|
+
primary_up(handler_key)
|
101
|
+
publish_primary_up(handler_key)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def primary_down?(handler_key)
|
106
|
+
primaries_down[handler_key]
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def all_primaries_up
|
112
|
+
mon_synchronize do
|
113
|
+
primaries_down.empty?
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
def primary_down(handler_key)
|
119
|
+
mon_synchronize do
|
120
|
+
primaries_down[handler_key] = true
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
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
|
+
def primary_up(handler_key)
|
137
|
+
mon_synchronize do
|
138
|
+
primaries_down.delete(handler_key)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def spec_name
|
143
|
+
::ActiveRecord::Base.connection_specification_name
|
144
|
+
end
|
145
|
+
|
146
|
+
def primaries_down
|
147
|
+
process_pid = Process.pid
|
148
|
+
return @primaries_down[process_pid] if @primaries_down[process_pid]
|
149
|
+
|
150
|
+
mon_synchronize do
|
151
|
+
if !@primaries_down[process_pid]
|
152
|
+
@primaries_down[process_pid] = @primaries_down[@ancestor_pid] || {}
|
153
|
+
|
154
|
+
if process_pid != @ancestor_pid
|
155
|
+
@primaries_down.delete(@ancestor_pid)
|
156
|
+
|
157
|
+
@primaries_down[process_pid].each_key do |handler_key|
|
158
|
+
verify_primary(handler_key)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
@primaries_down[process_pid]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def logger
|
168
|
+
Rails.logger
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsFailover
|
4
|
+
module ActiveRecord
|
5
|
+
class Middleware
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
writing_role = ::ActiveRecord::Base.writing_role
|
12
|
+
|
13
|
+
role =
|
14
|
+
if primary_down = Handler.instance.primary_down?(writing_role)
|
15
|
+
::ActiveRecord::Base.reading_role
|
16
|
+
else
|
17
|
+
::ActiveRecord::Base.writing_role
|
18
|
+
end
|
19
|
+
|
20
|
+
::ActiveRecord::Base.connected_to(role: role) do
|
21
|
+
env["rails_failover.role"] = role
|
22
|
+
@app.call(env)
|
23
|
+
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
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def resolve_cause(error)
|
34
|
+
if error.cause
|
35
|
+
resolve_cause(error.cause)
|
36
|
+
else
|
37
|
+
error
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module RailsFailover
|
2
|
+
module ActiveRecord
|
3
|
+
class Railtie < ::Rails::Railtie
|
4
|
+
initializer "rails_failover.init", after: "active_record.initialize_database" do
|
5
|
+
::ActiveSupport.on_load(:active_record) do
|
6
|
+
Handler.instance
|
7
|
+
|
8
|
+
# We are doing this manually for now since we're awaiting Rails 6.1 to be released which will
|
9
|
+
# have more stable ActiveRecord APIs for handling multiple databases with different roles.
|
10
|
+
::ActiveRecord::Base.connection_handlers[::ActiveRecord::Base.reading_role] =
|
11
|
+
::ActiveRecord::ConnectionAdapters::ConnectionHandler.new
|
12
|
+
|
13
|
+
::ActiveRecord::Base.connection_handlers[::ActiveRecord::Base.writing_role].connection_pools.each do |connection_pool|
|
14
|
+
RailsFailover::ActiveRecord.establish_reading_connection(connection_pool.spec)
|
15
|
+
end
|
16
|
+
|
17
|
+
begin
|
18
|
+
::ActiveRecord::Base.connection
|
19
|
+
rescue ::ActiveRecord::NoDatabaseError
|
20
|
+
# Do nothing since database hasn't been created
|
21
|
+
rescue ::PG::Error => e
|
22
|
+
Handler.instance.verify_primary(::ActiveRecord::Base.writing_role)
|
23
|
+
::ActiveRecord::Base.connection_handler = ::ActiveRecord::Base.lookup_connection_handler(:reading)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
initializer "rails_failover.insert_middleware" do |app|
|
29
|
+
app.middleware.insert_after(
|
30
|
+
::ActionDispatch::ActionableExceptions,
|
31
|
+
::RailsFailover::ActiveRecord::Middleware
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/rails_failover/redis.rb
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'redis'
|
4
|
+
|
5
|
+
supported_version = '4'
|
6
|
+
|
7
|
+
if Gem::Version.new(Redis::VERSION) < Gem::Version.new(supported_version)
|
8
|
+
raise "redis #{Redis::VERSION} is not supported. Please upgrade to Redis #{supported_version}."
|
9
|
+
end
|
10
|
+
|
11
|
+
require_relative "../redis/patches/client"
|
3
12
|
require_relative 'redis/connector'
|
4
13
|
|
5
14
|
module RailsFailover
|
@@ -1,10 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative 'handler'
|
4
4
|
|
5
5
|
module RailsFailover
|
6
6
|
class Redis
|
7
|
-
class Connector
|
7
|
+
class Connector < ::Redis::Client::Connector
|
8
8
|
def initialize(options)
|
9
9
|
options[:original_driver] = options[:driver]
|
10
10
|
|
@@ -22,7 +22,7 @@ module RailsFailover
|
|
22
22
|
Errno::ETIMEDOUT,
|
23
23
|
Errno::EINVAL => e
|
24
24
|
|
25
|
-
|
25
|
+
Handler.instance.verify_master(options.dup)
|
26
26
|
raise e
|
27
27
|
end
|
28
28
|
end
|
@@ -33,15 +33,15 @@ module RailsFailover
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def resolve
|
36
|
-
|
36
|
+
Handler.instance.master ? @options : @replica_options
|
37
37
|
end
|
38
38
|
|
39
39
|
def check(client)
|
40
|
-
|
40
|
+
Handler.instance.register_client(client)
|
41
41
|
end
|
42
42
|
|
43
43
|
def on_disconnect(client)
|
44
|
-
|
44
|
+
Handler.instance.deregister_client(client)
|
45
45
|
end
|
46
46
|
|
47
47
|
private
|
data/makefile
CHANGED
@@ -1,31 +1,21 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
SOCKET_PATH := /tmp/redis.sock
|
4
|
-
DBFILENAME := master.rdb
|
5
|
-
replica_port := 6382
|
6
|
-
REPLICA_PID_PATH := /tmp/redis_replica.pid
|
7
|
-
REPLICA_SOCKET_PATH := /tmp/redis_replica.sock
|
8
|
-
REPLICA_DBFILENAME := replica.rdb
|
1
|
+
include postgresql.mk
|
2
|
+
include redis.mk
|
9
3
|
|
10
|
-
|
11
|
-
@make -s all
|
4
|
+
all: redis
|
12
5
|
|
13
|
-
|
6
|
+
active_record: teardown_dummy_rails_server setup_dummy_rails_server test_active_record
|
14
7
|
|
15
|
-
|
16
|
-
bundle exec rspec ${RSPEC_PATH}
|
8
|
+
test_active_record:
|
9
|
+
@ACTIVE_RECORD=1 bundle exec rspec --tag type:active_record ${RSPEC_PATH}
|
17
10
|
|
18
|
-
|
19
|
-
|
11
|
+
setup_dummy_rails_server:
|
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
|
20
13
|
|
21
|
-
|
22
|
-
@
|
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
|
23
16
|
|
24
|
-
|
25
|
-
@
|
17
|
+
stop_dummy_rails_server:
|
18
|
+
@kill -TERM $(shell cat spec/support/dummy_app/tmp/pids/unicorn.pid)
|
26
19
|
|
27
|
-
|
28
|
-
@
|
29
|
-
|
30
|
-
start_replica:
|
31
|
-
@redis-server --daemonize yes --pidfile ${REPLICA_PID_PATH} --port ${replica_port} --unixsocket ${REPLICA_SOCKET_PATH} --replicaof 127.0.0.1 ${PORT} --dbfilename ${REPLICA_DBFILENAME} --logfile /dev/null
|
20
|
+
teardown_dummy_rails_server:
|
21
|
+
@cd spec/support/dummy_app && DISABLE_DATABASE_ENVIRONMENT_CHECK=1 RAILS_ENV=production $(BUNDLER_BIN) exec rails db:drop
|
data/postgresql.mk
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
PG_BIN_DIR := $(shell pg_config --bindir)
|
2
|
+
PWD := $(shell pwd)
|
3
|
+
PG_PRIMARY_DIR := $(PWD)/tmp/primary
|
4
|
+
PG_PRIMARY_DATA_DIR := $(PG_PRIMARY_DIR)/data
|
5
|
+
PG_PRIMARY_RUN_DIR := $(PG_PRIMARY_DIR)/run
|
6
|
+
PG_REPLICA_DIR := $(PWD)/tmp/replica
|
7
|
+
PG_REPLICA_DATA_DIR := $(PG_REPLICA_DIR)/data
|
8
|
+
PG_REPLICA_RUN_DIR := $(PG_REPLICA_DIR)/run
|
9
|
+
PG_PRIMARY_PORT := 5434
|
10
|
+
PG_REPLICA_PORT := 5435
|
11
|
+
PG_REPLICATION_USER := replication
|
12
|
+
PG_REPLICATION_PASSWORD := password
|
13
|
+
PG_REPLICATION_SLOT_NAME := replication
|
14
|
+
|
15
|
+
setup_pg: init_primary start_pg_primary setup_primary init_replica stop_pg_primary
|
16
|
+
|
17
|
+
setup_primary:
|
18
|
+
@$(PG_BIN_DIR)/psql -p $(PG_PRIMARY_PORT) -h $(PG_PRIMARY_RUN_DIR) -d postgres -c "CREATE USER $(PG_REPLICATION_USER) WITH REPLICATION ENCRYPTED PASSWORD '$(PG_REPLICATION_PASSWORD)';"
|
19
|
+
@$(PG_BIN_DIR)/psql -p $(PG_PRIMARY_PORT) -h $(PG_PRIMARY_RUN_DIR) -d postgres -c "SELECT * FROM pg_create_physical_replication_slot('$(PG_REPLICATION_SLOT_NAME)');"
|
20
|
+
@$(PG_BIN_DIR)/psql -p $(PG_PRIMARY_PORT) -h $(PG_PRIMARY_RUN_DIR) -d postgres -c "CREATE USER test;"
|
21
|
+
@$(PG_BIN_DIR)/psql -p $(PG_PRIMARY_PORT) -h $(PG_PRIMARY_RUN_DIR) -d postgres -c "CREATE DATABASE test;"
|
22
|
+
|
23
|
+
start_pg: start_pg_primary start_pg_replica
|
24
|
+
|
25
|
+
stop_pg: stop_pg_replica stop_pg_primary
|
26
|
+
|
27
|
+
init_primary:
|
28
|
+
@mkdir -p $(PG_PRIMARY_DATA_DIR)
|
29
|
+
@mkdir -p $(PG_PRIMARY_RUN_DIR)
|
30
|
+
@$(PG_BIN_DIR)/initdb -E UTF8 -D $(PG_PRIMARY_DATA_DIR)
|
31
|
+
|
32
|
+
init_replica:
|
33
|
+
@mkdir -p $(PG_REPLICA_DATA_DIR)
|
34
|
+
@mkdir -p $(PG_REPLICA_RUN_DIR)
|
35
|
+
@PGPASSWORD=$(PG_REPLICATION_PASSWORD) $(PG_BIN_DIR)/pg_basebackup -D $(PG_REPLICA_DATA_DIR) -X stream -h $(PG_PRIMARY_RUN_DIR) -p $(PG_PRIMARY_PORT) -U $(PG_REPLICATION_USER) -w -R
|
36
|
+
@chmod 0700 $(PG_REPLICA_DATA_DIR)
|
37
|
+
|
38
|
+
start_pg_primary:
|
39
|
+
@$(PG_BIN_DIR)/pg_ctl --silent --log /dev/null -w -D $(PG_PRIMARY_DATA_DIR) -o "-p $(PG_PRIMARY_PORT)" -o "-k $(PG_PRIMARY_RUN_DIR)" start
|
40
|
+
|
41
|
+
start_pg_replica:
|
42
|
+
@$(PG_BIN_DIR)/pg_ctl --silent --log /dev/null -w -D $(PG_REPLICA_DATA_DIR) -o "-p $(PG_REPLICA_PORT)" -o "-k $(PG_REPLICA_RUN_DIR)" start
|
43
|
+
|
44
|
+
restart_pg_primary:
|
45
|
+
@$(PG_BIN_DIR)/pg_ctl --silent --log /dev/null -w -D $(PG_PRIMARY_DATA_DIR) -o "-p $(PG_PRIMARY_PORT)" -o "-k $(PG_PRIMARY_RUN_DIR)" restart
|
46
|
+
|
47
|
+
stop_pg_primary:
|
48
|
+
@$(PG_BIN_DIR)/pg_ctl --silent --log /dev/null -w -D $(PG_PRIMARY_DATA_DIR) -o "-p $(PG_PRIMARY_PORT)" -o "-k $(PG_PRIMARY_RUN_DIR)" stop
|
49
|
+
|
50
|
+
stop_pg_replica:
|
51
|
+
@$(PG_BIN_DIR)/pg_ctl --silent --log /dev/null -w -D $(PG_REPLICA_DATA_DIR) -o "-p $(PG_REPLICA_PORT)" -o "-k $(PG_REPLICA_RUN_DIR)" stop
|
52
|
+
|
53
|
+
cleanup_pg:
|
54
|
+
@rm -rf $(PG_PRIMARY_DIR) $(PG_REPLICA_DIR)
|
data/rails_failover.gemspec
CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.authors = ["Alan Tan"]
|
9
9
|
spec.email = ["tgx@discourse.org"]
|
10
10
|
|
11
|
-
spec.summary = %q{Failover for
|
11
|
+
spec.summary = %q{Failover for ActiveRecord and Redis}
|
12
12
|
spec.license = "MIT"
|
13
13
|
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
14
14
|
|
@@ -21,5 +21,9 @@ 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
|
24
|
+
spec.add_dependency 'listen', "~> 3.2"
|
25
|
+
|
26
|
+
["activerecord", "railties"].each do |gem_name|
|
27
|
+
spec.add_dependency gem_name, "~> 6.0"
|
28
|
+
end
|
25
29
|
end
|
data/redis.mk
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
REDIS_PORT := 6381
|
2
|
+
REDIS_PID_PATH := /tmp/redis.pid
|
3
|
+
REDIS_SOCKET_PATH := /tmp/redis.sock
|
4
|
+
REDIS_DBFILENAME := master.rdb
|
5
|
+
REDIS_REPLICA_PORT := 6382
|
6
|
+
REDIS_REPLICA_PID_PATH := /tmp/redis_replica.pid
|
7
|
+
REDIS_REPLICA_SOCKET_PATH := /tmp/redis_replica.sock
|
8
|
+
REDIS_REPLICA_DBFILENAME := replica.rdb
|
9
|
+
|
10
|
+
redis: start_redis test_redis stop_redis
|
11
|
+
|
12
|
+
test_redis:
|
13
|
+
@REDIS=1 bundle exec rspec --tag type:redis ${RSPEC_PATH}
|
14
|
+
|
15
|
+
start_redis: start_redis_master start_redis_replica
|
16
|
+
stop_redis: stop_redis_replica stop_redis_master
|
17
|
+
|
18
|
+
stop_redis_master:
|
19
|
+
@redis-cli -p ${REDIS_PORT} shutdown
|
20
|
+
|
21
|
+
start_redis_master:
|
22
|
+
@redis-server --daemonize yes --pidfile ${REDIS_PID_PATH} --port ${REDIS_PORT} --unixsocket ${REDIS_SOCKET_PATH} --dbfilename ${REDIS_DBFILENAME} --logfile /dev/null
|
23
|
+
|
24
|
+
stop_redis_replica:
|
25
|
+
@redis-cli -p ${REDIS_REPLICA_PORT} shutdown
|
26
|
+
|
27
|
+
start_redis_replica:
|
28
|
+
@redis-server --daemonize yes --pidfile ${REDIS_REPLICA_PID_PATH} --port ${REDIS_REPLICA_PORT} --unixsocket ${REDIS_REPLICA_SOCKET_PATH} --replicaof 127.0.0.1 ${REDIS_PORT} --dbfilename ${REDIS_REPLICA_DBFILENAME} --logfile /dev/null
|
metadata
CHANGED
@@ -1,29 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails_failover
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
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-05-
|
11
|
+
date: 2020-05-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: listen
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '3.2'
|
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: '
|
26
|
+
version: '3.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activerecord
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '6.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '6.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: railties
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '6.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '6.0'
|
27
55
|
description:
|
28
56
|
email:
|
29
57
|
- tgx@discourse.org
|
@@ -45,13 +73,19 @@ files:
|
|
45
73
|
- bin/rspec
|
46
74
|
- bin/setup
|
47
75
|
- lib/rails_failover.rb
|
76
|
+
- lib/rails_failover/active_record.rb
|
77
|
+
- lib/rails_failover/active_record/handler.rb
|
78
|
+
- lib/rails_failover/active_record/middleware.rb
|
79
|
+
- lib/rails_failover/active_record/railtie.rb
|
48
80
|
- lib/rails_failover/redis.rb
|
49
81
|
- lib/rails_failover/redis/connector.rb
|
50
|
-
- lib/rails_failover/redis/
|
82
|
+
- lib/rails_failover/redis/handler.rb
|
51
83
|
- lib/rails_failover/version.rb
|
52
84
|
- lib/redis/patches/client.rb
|
53
85
|
- makefile
|
86
|
+
- postgresql.mk
|
54
87
|
- rails_failover.gemspec
|
88
|
+
- redis.mk
|
55
89
|
homepage:
|
56
90
|
licenses:
|
57
91
|
- MIT
|
@@ -74,5 +108,5 @@ requirements: []
|
|
74
108
|
rubygems_version: 3.0.3
|
75
109
|
signing_key:
|
76
110
|
specification_version: 4
|
77
|
-
summary: Failover for
|
111
|
+
summary: Failover for ActiveRecord and Redis
|
78
112
|
test_files: []
|