rails_failover 0.2.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b47ce7eef70576e83fe137949d50e280677f6f5e2444790a9bdf7cc1708f540
4
- data.tar.gz: eea78a86a311a24926c83ab4450eae6ed59b0f39a60faafdf9affe050074aa7d
3
+ metadata.gz: 2bff6dd429dd01de4684f7ec70f8ccaeb36f403be9c25f2dceb5789ea4d19bef
4
+ data.tar.gz: 0b7a7b8a1ad27498e49079c88604d5365800823a3c5c8b7740af7d71683f59a8
5
5
  SHA512:
6
- metadata.gz: 46aea75592369b8a0af7a48ee1898d2fdfecb8a80ebc777ba386052709678258d719a7e2e2d4d00f1c6c3ca7d158747eecb0a329373ff3a8f6423c6f70824f27
7
- data.tar.gz: aa006f95128b6ec9fe600652c62d20c32b948aeff049cf2155d5366b97a57d851631e8de6c128f2f0587c0e4d97a797252b5c2f196a98d7d245e3b0a539d8b53
6
+ metadata.gz: 0013d14b3dd577feb3eb627a223ffa94f1b781310194d8a430524f4f1f4728ad32e117ffbd333ae476b1d87194c202b9e3511e8989d2aefa718f3c97f6270e2a
7
+ data.tar.gz: c6fbfb3a9a22557a73b1208f39c51bd876b607c50b22438183fd0249d92af4926ea6cb517dea3ce556bbb08c02dd8a6a4f93d6cd1c7699ebd8e3d54eecf955f2
@@ -1,3 +1,7 @@
1
+ AllCops:
2
+ Exclude:
3
+ - spec/support/dummy_app/**/*
4
+
1
5
  inherit_gem:
2
6
  rubocop-discourse: default.yml
3
7
 
@@ -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,8 +6,9 @@ gemspec
6
6
 
7
7
  gem "rake", "~> 12.0"
8
8
  gem "rspec", "~> 3.0"
9
-
10
- group :development do
11
- gem 'rubocop-discourse'
12
- gem 'byebug'
13
- 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'
@@ -1,18 +1,72 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rails_failover (0.1.0)
5
- redis (~> 4)
4
+ rails_failover (0.5.2)
5
+ activerecord (~> 6.0)
6
+ railties (~> 6.0)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
9
10
  specs:
11
+ actionpack (6.0.3.1)
12
+ actionview (= 6.0.3.1)
13
+ activesupport (= 6.0.3.1)
14
+ rack (~> 2.0, >= 2.0.8)
15
+ rack-test (>= 0.6.3)
16
+ rails-dom-testing (~> 2.0)
17
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
18
+ actionview (6.0.3.1)
19
+ activesupport (= 6.0.3.1)
20
+ builder (~> 3.1)
21
+ erubi (~> 1.4)
22
+ rails-dom-testing (~> 2.0)
23
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
24
+ activemodel (6.0.3.1)
25
+ activesupport (= 6.0.3.1)
26
+ activerecord (6.0.3.1)
27
+ activemodel (= 6.0.3.1)
28
+ activesupport (= 6.0.3.1)
29
+ activesupport (6.0.3.1)
30
+ concurrent-ruby (~> 1.0, >= 1.0.2)
31
+ i18n (>= 0.7, < 2)
32
+ minitest (~> 5.1)
33
+ tzinfo (~> 1.1)
34
+ zeitwerk (~> 2.2, >= 2.2.2)
10
35
  ast (2.4.0)
36
+ builder (3.2.4)
11
37
  byebug (11.1.3)
38
+ concurrent-ruby (1.1.6)
39
+ crass (1.0.6)
12
40
  diff-lcs (1.3)
41
+ erubi (1.9.0)
42
+ i18n (1.8.2)
43
+ concurrent-ruby (~> 1.0)
44
+ loofah (2.6.0)
45
+ crass (~> 1.0.2)
46
+ nokogiri (>= 1.5.9)
47
+ method_source (1.0.0)
48
+ mini_portile2 (2.4.0)
49
+ minitest (5.14.1)
50
+ nokogiri (1.10.9)
51
+ mini_portile2 (~> 2.4.0)
13
52
  parallel (1.19.1)
14
53
  parser (2.7.1.2)
15
54
  ast (~> 2.4.0)
55
+ pg (1.2.3)
56
+ rack (2.2.2)
57
+ rack-test (1.1.0)
58
+ rack (>= 1.0, < 3)
59
+ rails-dom-testing (2.0.3)
60
+ activesupport (>= 4.2.0)
61
+ nokogiri (>= 1.6)
62
+ rails-html-sanitizer (1.3.0)
63
+ loofah (~> 2.3)
64
+ railties (6.0.3.1)
65
+ actionpack (= 6.0.3.1)
66
+ activesupport (= 6.0.3.1)
67
+ method_source
68
+ rake (>= 0.8.7)
69
+ thor (>= 0.20.3, < 2.0)
16
70
  rainbow (3.0.0)
17
71
  rake (12.3.3)
18
72
  redis (4.1.4)
@@ -43,15 +97,24 @@ GEM
43
97
  rubocop-rspec (1.39.0)
44
98
  rubocop (>= 0.68.1)
45
99
  ruby-progressbar (1.10.1)
100
+ thor (1.0.1)
101
+ thread_safe (0.3.6)
102
+ tzinfo (1.2.7)
103
+ thread_safe (~> 0.1)
46
104
  unicode-display_width (1.7.0)
105
+ zeitwerk (2.3.0)
47
106
 
48
107
  PLATFORMS
49
108
  ruby
50
109
 
51
110
  DEPENDENCIES
111
+ activerecord (~> 6.0)
52
112
  byebug
113
+ pg (~> 1.2)
114
+ rack
53
115
  rails_failover!
54
116
  rake (~> 12.0)
117
+ redis (~> 4.1)
55
118
  rspec (~> 3.0)
56
119
  rubocop-discourse
57
120
 
data/README.md CHANGED
@@ -1,13 +1,16 @@
1
1
  # RailsFailover
2
2
 
3
- * Automatic failover and recovery for simple master-replica Redis setup
3
+ Automatic failover and recovery for primary/replica setup for:
4
+
5
+ 1. Redis
6
+ 1. ActiveRecord (PostgreSQL/MySQL)
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:
@@ -20,35 +23,101 @@ Or install it yourself as:
20
23
 
21
24
  ## Usage
22
25
 
26
+ ### ActiveRecord
27
+
28
+ In `config/application.rb` add `require 'rails_failover/active_record'` after `require "active_record/railtie"`.
29
+
30
+ In your database configuration, simply add `replica_host` and `replica_port` to your database configuration.
31
+
32
+ ```
33
+ production:
34
+ host: <primary db server host>
35
+ port: <primary db server port>
36
+ replica_host: <replica db server host>
37
+ replica_port: <replica db server port>
38
+ ```
39
+
40
+ The gem will automatically create an `ActiveRecord::ConnectionAdapters::ConnectionHandler` with the `ActiveRecord::Base.reading_role` as the `handler_key`.
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
+
23
78
  ### Redis
24
79
 
80
+ Add `require 'rails_failover/redis'` before creating a `Redis` instance.
81
+
25
82
  ```
26
83
  Redis.new(host: "127.0.0.1", port: 6379, replica_host: "127.0.0.1", replica_port: 6380, connector: RailsFailover::Redis::Connector))
27
84
  ```
28
85
 
29
- Callbacks can be registered when the master connection is down and when it is up.
30
-
86
+ Callbacks can be registered when the primary connection is down and when it is up.
31
87
 
32
88
  ```
33
- RailsFailover::Redis.register_master_up_callback do
89
+ RailsFailover::Redis.on_failover_callback do
34
90
  # Switch site to read-only mode
35
91
  end
36
92
 
37
- RailsFailover::Redis.register_master_down_callback do
93
+ RailsFailover::Redis.on_fallback_callback do
38
94
  # Switch site out of read-only mode
39
95
  end
40
96
  ```
41
97
 
42
98
  ## Development
43
99
 
44
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
100
+ 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
101
 
46
102
  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
103
 
48
- ## Contributing
104
+ ### Testing
105
+
106
+ #### ActiveRecord
107
+
108
+ The ActiveRecord failover tests are run against a dummy Rails server. Run the following commands to run the test:
109
+
110
+ 1. `make setup_pg`
111
+ 1. `make start_pg`
112
+ 1. `bin/rspec active_record`. You may also run the tests with more unicorn workers by adding the `UNICORN_WORKERS` env variable.
49
113
 
50
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/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/[USERNAME]/rails_failover/blob/master/CODE_OF_CONDUCT.md).
114
+ #### Redis
115
+
116
+ `bin/rspec redis`
117
+
118
+ ## Contributing
51
119
 
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).
52
121
 
53
122
  ## License
54
123
 
@@ -56,4 +125,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
56
125
 
57
126
  ## Code of Conduct
58
127
 
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/[USERNAME]/rails_failover/blob/master/CODE_OF_CONDUCT.md).
128
+ 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).
@@ -3,7 +3,8 @@
3
3
 
4
4
  require "bundler/setup"
5
5
  require "rails_failover"
6
- require 'redis'
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 RSPEC_PATH=$1
2
+ RSPEC_PATH=$2 make -s $1
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails_failover/version"
4
- require "redis/patches/client"
5
- require "rails_failover/redis"
6
4
 
7
5
  module RailsFailover
8
6
  class Error < StandardError; end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+
5
+ if defined?(::Rails)
6
+ require_relative 'active_record/railtie'
7
+ end
8
+
9
+ require_relative 'active_record/middleware'
10
+ require_relative 'active_record/handler'
11
+
12
+ module RailsFailover
13
+ module ActiveRecord
14
+ def self.logger=(logger)
15
+ @logger = logger
16
+ end
17
+
18
+ def self.logger
19
+ @logger || Rails.logger
20
+ end
21
+
22
+ def self.verify_primary_frequency_seconds=(seconds)
23
+ @verify_primary_frequency_seconds = seconds
24
+ end
25
+
26
+ def self.verify_primary_frequency_seconds
27
+ @verify_primary_frequency_seconds || 5
28
+ end
29
+
30
+ def self.establish_reading_connection(handler, connection_spec)
31
+ config = connection_spec.config
32
+
33
+ if config[:replica_host] && config[:replica_port]
34
+ replica_config = config.dup
35
+ replica_config[:host] = replica_config.delete(:replica_host)
36
+ replica_config[:port] = replica_config.delete(:replica_port)
37
+ replica_config[:replica] = true
38
+ handler.establish_connection(replica_config)
39
+ end
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
61
+ end
62
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+ require 'singleton'
3
+ require 'monitor'
4
+
5
+ module RailsFailover
6
+ module ActiveRecord
7
+ class Handler
8
+ include Singleton
9
+ include MonitorMixin
10
+
11
+ VERIFY_FREQUENCY_BUFFER_PRECENT = 20
12
+
13
+ def initialize
14
+ @primaries_down = {}
15
+ @ancestor_pid = Process.pid
16
+
17
+ super() # Monitor#initialize
18
+ end
19
+
20
+ def verify_primary(handler_key)
21
+ mon_synchronize do
22
+ primary_down(handler_key)
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
32
+
33
+ @thread = Thread.new do
34
+ loop do
35
+ initiate_fallback_to_primary
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
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ def initiate_fallback_to_primary
54
+ frequency = RailsFailover::ActiveRecord.verify_primary_frequency_seconds
55
+ sleep(frequency * ((rand(VERIFY_FREQUENCY_BUFFER_PRECENT) + 100) / 100.0))
56
+
57
+ active_handler_keys = []
58
+
59
+ primaries_down.keys.each do |handler_key|
60
+ connection_handler = ::ActiveRecord::Base.connection_handlers[handler_key]
61
+ spec = connection_handler.retrieve_connection_pool(spec_name).spec
62
+ config = spec.config
63
+ logger.debug "#{Process.pid} Checking server for '#{handler_key} #{spec_name}'..."
64
+ connection_active = false
65
+
66
+ begin
67
+ connection = ::ActiveRecord::Base.public_send(spec.adapter_method, config)
68
+ connection_active = connection.active?
69
+ rescue => e
70
+ logger.debug "#{Process.pid} Connection to server for '#{handler_key} #{spec_name}' failed with '#{e.message}'"
71
+ ensure
72
+ connection.disconnect! if connection
73
+ end
74
+
75
+ if connection_active
76
+ logger.debug "#{Process.pid} Server for '#{handler_key} #{spec_name}' is active."
77
+ active_handler_keys << handler_key
78
+ end
79
+ end
80
+
81
+ active_handler_keys.each do |handler_key|
82
+ primary_up(handler_key)
83
+ end
84
+ end
85
+
86
+ def primary_down?(handler_key)
87
+ primaries_down[handler_key]
88
+ end
89
+
90
+ private
91
+
92
+ def all_primaries_up
93
+ mon_synchronize do
94
+ primaries_down.empty?
95
+ end
96
+ end
97
+
98
+ def primary_down(handler_key)
99
+ mon_synchronize do
100
+ primaries_down[handler_key] = true
101
+ end
102
+ end
103
+
104
+ def primary_up(handler_key)
105
+ mon_synchronize do
106
+ primaries_down.delete(handler_key)
107
+ end
108
+ end
109
+
110
+ def spec_name
111
+ ::ActiveRecord::Base.connection_specification_name
112
+ end
113
+
114
+ def primaries_down
115
+ process_pid = Process.pid
116
+ return @primaries_down[process_pid] if @primaries_down[process_pid]
117
+
118
+ mon_synchronize do
119
+ if !@primaries_down[process_pid]
120
+ @primaries_down[process_pid] = @primaries_down[@ancestor_pid] || {}
121
+
122
+ if process_pid != @ancestor_pid
123
+ @primaries_down.delete(@ancestor_pid)
124
+
125
+ @primaries_down[process_pid].each_key do |handler_key|
126
+ verify_primary(handler_key)
127
+ end
128
+ end
129
+ end
130
+
131
+ @primaries_down[process_pid]
132
+ end
133
+ end
134
+
135
+ def logger
136
+ ::Rails.logger
137
+ end
138
+ end
139
+ end
140
+ end