rails_failover 0.2.0 → 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 +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: []
|