rails_failover 0.8.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +56 -31
- data/.rspec +1 -0
- data/.rubocop.yml +1 -0
- data/.streerc +2 -0
- data/CHANGELOG.md +15 -1
- data/Gemfile +0 -9
- data/README.md +12 -13
- data/lib/rails_failover/active_record/handler.rb +16 -23
- data/lib/rails_failover/active_record/middleware.rb +43 -48
- data/lib/rails_failover/active_record/railtie.rb +13 -44
- data/lib/rails_failover/active_record.rb +54 -40
- data/lib/rails_failover/redis/connector.rb +8 -14
- data/lib/rails_failover/redis/handler.rb +17 -16
- data/lib/rails_failover/redis.rb +9 -5
- data/lib/rails_failover/version.rb +1 -1
- data/lib/rails_failover.rb +2 -1
- data/lib/redis/patches/client.rb +1 -1
- data/makefile +3 -3
- data/rails_failover.gemspec +27 -17
- metadata +143 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b769d81f278cffd6d960f9a43da2feb15d46e23dd35d2608dbef0fdb04688a70
|
4
|
+
data.tar.gz: a72536269e00455cf0506f68dfbbdf6c98a7b7c8a24064532a7219fa31b4bec6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 33f8835985676d80ba1701eda774322db0701040691e3164218bbf931901314a277ad63c926634f7ac6270f43e5899a7a90030b03f7ec53badb618bac0a187b3
|
7
|
+
data.tar.gz: a219d8441c37438bc1ea367fc99920cf3090728a5be482dd59a68f80a5690effa3f2a2a059085675980ed2e816e662ca3c485b12543437bde2801e07d4adfa6b
|
data/.github/workflows/ci.yml
CHANGED
@@ -4,75 +4,100 @@ on:
|
|
4
4
|
pull_request:
|
5
5
|
push:
|
6
6
|
branches:
|
7
|
-
- master
|
8
7
|
- main
|
9
8
|
|
10
9
|
jobs:
|
11
|
-
|
10
|
+
lint:
|
12
11
|
runs-on: ubuntu-latest
|
13
12
|
|
14
|
-
|
15
|
-
|
13
|
+
steps:
|
14
|
+
- uses: actions/checkout@v3
|
15
|
+
|
16
|
+
- name: Setup ruby
|
17
|
+
uses: ruby/setup-ruby@v1
|
18
|
+
with:
|
19
|
+
ruby-version: '3.2'
|
20
|
+
bundler-cache: true
|
21
|
+
|
22
|
+
- name: Rubocop
|
23
|
+
run: bundle exec rubocop
|
24
|
+
|
25
|
+
- name: syntax_tree
|
26
|
+
if: ${{ !cancelled() }}
|
27
|
+
run: |
|
28
|
+
set -E
|
29
|
+
bundle exec stree check Gemfile rails_failover.gemspec $(git ls-files '*.rb')
|
30
|
+
|
31
|
+
redis:
|
32
|
+
name: 'Redis (Ruby ${{ matrix.ruby }})'
|
33
|
+
runs-on: ubuntu-latest
|
16
34
|
|
17
35
|
strategy:
|
18
36
|
fail-fast: false
|
19
37
|
matrix:
|
20
|
-
ruby: [
|
21
|
-
build_types: ["REDIS", "ACTIVERECORD"]
|
22
|
-
include:
|
23
|
-
- ruby: "3.1"
|
24
|
-
build_types: "LINT"
|
38
|
+
ruby: ['3.0', '3.1', '3.2']
|
25
39
|
|
26
40
|
steps:
|
27
|
-
- uses: actions/checkout@
|
41
|
+
- uses: actions/checkout@v3
|
28
42
|
|
29
43
|
- name: Setup ruby
|
30
44
|
uses: ruby/setup-ruby@v1
|
31
45
|
with:
|
32
46
|
ruby-version: ${{ matrix.ruby }}
|
33
|
-
|
34
|
-
- name: Setup bundler
|
35
|
-
run: gem install bundler
|
36
|
-
|
37
|
-
- name: Setup gems
|
38
|
-
run: bundle install
|
39
|
-
|
40
|
-
- name: Rubocop
|
41
|
-
run: bundle exec rubocop
|
42
|
-
if: env.BUILD_TYPE == 'LINT'
|
47
|
+
bundler-cache: true
|
43
48
|
|
44
49
|
- name: Setup redis
|
45
50
|
run: sudo apt-get install redis-server
|
46
|
-
if: env.BUILD_TYPE == 'REDIS'
|
47
51
|
|
48
52
|
- name: Redis specs
|
49
53
|
run: bin/rspec redis
|
50
|
-
if: env.BUILD_TYPE == 'REDIS'
|
51
54
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
+
active_record:
|
56
|
+
runs-on: ubuntu-latest
|
57
|
+
name: 'ActiveRecord ~>${{ matrix.rails }} (Ruby ${{ matrix.ruby }})'
|
58
|
+
|
59
|
+
strategy:
|
60
|
+
fail-fast: false
|
61
|
+
matrix:
|
62
|
+
ruby: ['3.2', '3.1', '3.0']
|
63
|
+
rails: ['7.0.0']
|
64
|
+
include:
|
65
|
+
- ruby: '3.2'
|
66
|
+
rails: '6.1.0'
|
67
|
+
- ruby: '3.2'
|
68
|
+
rails: 'edge'
|
69
|
+
|
70
|
+
steps:
|
71
|
+
- uses: actions/checkout@v3
|
72
|
+
|
73
|
+
- name: Setup ruby
|
74
|
+
uses: ruby/setup-ruby@v1
|
75
|
+
with:
|
76
|
+
ruby-version: ${{ matrix.ruby }}
|
77
|
+
|
78
|
+
- name: Setup gems
|
79
|
+
run: bundle install
|
55
80
|
|
56
81
|
- name: Setup postgres
|
57
82
|
run: |
|
58
83
|
make setup_pg
|
59
84
|
make start_pg
|
60
|
-
if: env.BUILD_TYPE == 'ACTIVERECORD'
|
61
85
|
|
62
86
|
- name: ActiveRecord specs
|
87
|
+
env:
|
88
|
+
RAILS_VERSION: ${{ matrix.rails }}
|
63
89
|
run: bin/rspec active_record
|
64
|
-
if: env.BUILD_TYPE == 'ACTIVERECORD'
|
65
90
|
|
66
91
|
publish:
|
67
|
-
if: github.event_name == 'push' &&
|
68
|
-
needs:
|
92
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
93
|
+
needs: [lint, redis, active_record]
|
69
94
|
runs-on: ubuntu-latest
|
70
95
|
|
71
96
|
steps:
|
72
|
-
- uses: actions/checkout@
|
97
|
+
- uses: actions/checkout@v3
|
73
98
|
|
74
99
|
- name: Release Gem
|
75
|
-
uses: discourse/publish-rubygems-action@
|
100
|
+
uses: discourse/publish-rubygems-action@v3
|
76
101
|
env:
|
77
102
|
RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
|
78
103
|
GIT_EMAIL: team@discourse.org
|
data/.rspec
CHANGED
data/.rubocop.yml
CHANGED
data/.streerc
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# Changelog
|
2
|
+
|
2
3
|
All notable changes to this project will be documented in this file.
|
3
4
|
|
4
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
@@ -6,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
6
7
|
|
7
8
|
## [Unreleased]
|
8
9
|
|
10
|
+
## [2.0.0] - 2023-05-16
|
11
|
+
|
12
|
+
- DEV: Compatibility with Rails 7.1+ (drop support for Rails 6.0 & ruby 2.7)
|
13
|
+
|
14
|
+
## [1.0.0] - 2023-04-07
|
15
|
+
|
16
|
+
- DEV: Remove the support for Ruby < 2.7
|
17
|
+
- DEV: Compatibility with Rails 7.1+
|
18
|
+
|
9
19
|
## [0.8.0] - 2022-01-17
|
10
20
|
|
11
21
|
- FEATURE: Compatibility with Rails 7.0+
|
@@ -20,7 +30,7 @@ No changes.
|
|
20
30
|
|
21
31
|
## [0.7.1] - 2021-04-14
|
22
32
|
|
23
|
-
- FIX: Backward
|
33
|
+
- FIX: Backward compatibility with Rails 6.0
|
24
34
|
|
25
35
|
## [0.7.0] - 2021-04-14
|
26
36
|
|
@@ -49,6 +59,7 @@ No changes.
|
|
49
59
|
Previously, a replica failing would cause it to be added to the 'primaries_down' list. The fallback handler would then continuously try and fallback the replica to itself, looping forever, and meaning that fallback to primary would never happen.
|
50
60
|
|
51
61
|
## [0.6.0] - 2020-11-09
|
62
|
+
|
52
63
|
- FEATURE: Run failover/fallback callbacks once for each backend
|
53
64
|
|
54
65
|
Previously the failover callback would only fire when the first backend failed, and the fallback callback would only fire when the last backend recovered. Now both failover and fallback callbacks will be triggered for each backend. The key for each backend is also passed to the callbacks for consumption by consuming applications.
|
@@ -58,6 +69,7 @@ No changes.
|
|
58
69
|
This is intended for consumption by monitoring systems (e.g. the Discourse prometheus exporter)
|
59
70
|
|
60
71
|
## [0.5.9] - 2020-11-06
|
72
|
+
|
61
73
|
- FIX: Ignore errors from the redis socket shutdown call
|
62
74
|
|
63
75
|
This can fail with various i/o errors, but in all cases we want the thread to continue closing the connection with the error, and all the other connections.
|
@@ -67,6 +79,7 @@ No changes.
|
|
67
79
|
- FIX: Handle concurrency issues during redis disconnection (#10)
|
68
80
|
|
69
81
|
This handles concurrency issues which can happen during redis failover/fallback:
|
82
|
+
|
70
83
|
- Previously, 'subscribed' redis clients were skipped during the disconnect process. This is resolved by directly accessing the original_client from the ::Redis instance
|
71
84
|
- Trying to acquire the mutex on a subscribed redis client is impossible, so the close operation would never complete. Now we send the shutdown() signal to the thread, then allow up to 1 second for the mutex to be released before we close the socket
|
72
85
|
- Failover is almost always triggered inside a redis client mutex. Failover then has its own mutex, within which we attempted to acquire mutexes for all redis clients. This logic causes a deadlock when multiple clients failover simultaneously. Now, all disconnection is performed by the Redis::Handler failover thread, outside of any other mutexes. To make this safe, the primary/replica state is stored in the connection driver, and disconnect_clients is updated to specifically target primary/replica connections.
|
@@ -107,4 +120,5 @@ No changes.
|
|
107
120
|
## [0.5.2] - 2020-06-23
|
108
121
|
|
109
122
|
### Changed
|
123
|
+
|
110
124
|
- FIX: Only rescue from connection errors.
|
data/Gemfile
CHANGED
@@ -3,12 +3,3 @@ source "https://rubygems.org"
|
|
3
3
|
|
4
4
|
# Specify your gem's dependencies in rails_failover.gemspec
|
5
5
|
gemspec
|
6
|
-
|
7
|
-
gem "rake", "~> 12.0"
|
8
|
-
gem "rspec", "~> 3.0"
|
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/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
Automatic failover and recovery for primary/replica setup for:
|
4
4
|
|
5
5
|
1. Redis
|
6
|
-
|
6
|
+
2. ActiveRecord (PostgreSQL/MySQL Adapters)
|
7
7
|
|
8
8
|
## Installation
|
9
9
|
|
@@ -29,7 +29,7 @@ In `config/application.rb` add `require 'rails_failover/active_record'` after `r
|
|
29
29
|
|
30
30
|
In your database configuration, simply add `replica_host` and `replica_port` to your database configuration.
|
31
31
|
|
32
|
-
```
|
32
|
+
```yml
|
33
33
|
production:
|
34
34
|
host: <primary db server host>
|
35
35
|
port: <primary db server port>
|
@@ -37,11 +37,12 @@ production:
|
|
37
37
|
replica_port: <replica db server port>
|
38
38
|
```
|
39
39
|
|
40
|
-
The gem will automatically create
|
40
|
+
The gem will automatically create a role (using `ActiveRecord.reading_role`) on
|
41
|
+
the default `ActiveRecord` connection handler.
|
41
42
|
|
42
43
|
#### Failover/Fallback Hooks
|
43
44
|
|
44
|
-
```
|
45
|
+
```ruby
|
45
46
|
RailsFailover::ActiveRecord.on_failover do
|
46
47
|
# Enable readonly mode
|
47
48
|
end
|
@@ -53,9 +54,7 @@ end
|
|
53
54
|
|
54
55
|
#### Multiple connection handlers
|
55
56
|
|
56
|
-
|
57
|
-
|
58
|
-
```
|
57
|
+
```yml
|
59
58
|
# config/database.yml
|
60
59
|
|
61
60
|
production:
|
@@ -72,20 +71,20 @@ production:
|
|
72
71
|
|
73
72
|
# In your ActiveRecord base model or model.
|
74
73
|
|
75
|
-
connects_to database: { writing: :primary, second_database_writing: :second_database_writing
|
74
|
+
connects_to database: { writing: :primary, second_database_writing: :second_database_writing }
|
76
75
|
```
|
77
76
|
|
78
77
|
### Redis
|
79
78
|
|
80
79
|
Add `require 'rails_failover/redis'` before creating a `Redis` instance.
|
81
80
|
|
82
|
-
```
|
83
|
-
Redis.new(host: "127.0.0.1", port: 6379, replica_host: "127.0.0.1", replica_port: 6380, connector: RailsFailover::Redis::Connector)
|
81
|
+
```ruby
|
82
|
+
Redis.new(host: "127.0.0.1", port: 6379, replica_host: "127.0.0.1", replica_port: 6380, connector: RailsFailover::Redis::Connector)
|
84
83
|
```
|
85
84
|
|
86
85
|
Callbacks can be registered when the primary connection is down and when it is up.
|
87
86
|
|
88
|
-
```
|
87
|
+
```ruby
|
89
88
|
RailsFailover::Redis.on_failover_callback do
|
90
89
|
# Switch site to read-only mode
|
91
90
|
end
|
@@ -108,8 +107,8 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
108
107
|
The ActiveRecord failover tests are run against a dummy Rails server. Run the following commands to run the test:
|
109
108
|
|
110
109
|
1. `make setup_pg`
|
111
|
-
|
112
|
-
|
110
|
+
2. `make start_pg`
|
111
|
+
3. `bin/rspec active_record`. You may also run the tests with more unicorn workers by adding the `UNICORN_WORKERS` env variable.
|
113
112
|
|
114
113
|
#### Redis
|
115
114
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
2
|
+
require "singleton"
|
3
|
+
require "monitor"
|
4
|
+
require "concurrent"
|
5
5
|
|
6
6
|
module RailsFailover
|
7
7
|
module ActiveRecord
|
@@ -9,7 +9,7 @@ module RailsFailover
|
|
9
9
|
include Singleton
|
10
10
|
include MonitorMixin
|
11
11
|
|
12
|
-
|
12
|
+
VERIFY_FREQUENCY_BUFFER_PERCENT = 20
|
13
13
|
|
14
14
|
def initialize
|
15
15
|
@primaries_down = Concurrent::Map.new
|
@@ -50,26 +50,20 @@ module RailsFailover
|
|
50
50
|
|
51
51
|
def initiate_fallback_to_primary
|
52
52
|
frequency = RailsFailover::ActiveRecord.verify_primary_frequency_seconds
|
53
|
-
sleep(frequency * ((rand(
|
53
|
+
sleep(frequency * ((rand(VERIFY_FREQUENCY_BUFFER_PERCENT) + 100) / 100.0))
|
54
54
|
|
55
55
|
active_handler_keys = []
|
56
56
|
|
57
57
|
primaries_down.keys.each do |handler_key|
|
58
|
-
connection_handler = ::ActiveRecord::Base.connection_handlers[handler_key]
|
59
|
-
|
60
|
-
connection_pool = connection_handler.retrieve_connection_pool(spec_name)
|
61
|
-
if connection_pool.respond_to?(:db_config)
|
62
|
-
config = connection_pool.db_config.configuration_hash
|
63
|
-
adapter_method = connection_pool.db_config.adapter_method
|
64
|
-
else
|
65
|
-
config = connection_pool.spec.config
|
66
|
-
adapter_method = connection_pool.spec.adapter_method
|
67
|
-
end
|
68
58
|
logger.debug "#{Process.pid} Checking server for '#{handler_key} #{spec_name}'..."
|
69
59
|
connection_active = false
|
70
60
|
|
71
61
|
begin
|
72
|
-
connection =
|
62
|
+
connection =
|
63
|
+
::ActiveRecord::Base.connection_handler.retrieve_connection(
|
64
|
+
spec_name,
|
65
|
+
role: handler_key,
|
66
|
+
)
|
73
67
|
connection_active = connection.active?
|
74
68
|
rescue => e
|
75
69
|
logger.debug "#{Process.pid} Connection to server for '#{handler_key} #{spec_name}' failed with '#{e.message}'"
|
@@ -83,9 +77,7 @@ module RailsFailover
|
|
83
77
|
end
|
84
78
|
end
|
85
79
|
|
86
|
-
active_handler_keys.each
|
87
|
-
primary_up(handler_key)
|
88
|
-
end
|
80
|
+
active_handler_keys.each { |handler_key| primary_up(handler_key) }
|
89
81
|
end
|
90
82
|
|
91
83
|
def all_primaries_up
|
@@ -108,10 +100,11 @@ module RailsFailover
|
|
108
100
|
|
109
101
|
def primaries_down
|
110
102
|
ancestor_pids = nil
|
111
|
-
value =
|
112
|
-
|
113
|
-
|
114
|
-
|
103
|
+
value =
|
104
|
+
@primaries_down.compute_if_absent(Process.pid) do
|
105
|
+
ancestor_pids = @primaries_down.keys
|
106
|
+
@primaries_down.values.first || Concurrent::Map.new
|
107
|
+
end
|
115
108
|
|
116
109
|
ancestor_pids&.each do |pid|
|
117
110
|
@primaries_down.delete(pid)&.each_key { |key| verify_primary(key) }
|
@@ -4,20 +4,18 @@ module RailsFailover
|
|
4
4
|
module ActiveRecord
|
5
5
|
class Interceptor
|
6
6
|
def self.adapter_errors
|
7
|
-
@adapter_errors ||=
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
@adapter_errors ||=
|
8
|
+
begin
|
9
|
+
if defined?(::PG)
|
10
|
+
[::PG::UnableToSend, ::PG::ConnectionBad]
|
11
|
+
elsif defined?(::Mysql2)
|
12
|
+
[::Mysql2::Error::ConnectionError]
|
13
|
+
end
|
12
14
|
end
|
13
|
-
end
|
14
15
|
end
|
15
16
|
|
16
17
|
def self.handle(request, exception)
|
17
|
-
verify_primary(
|
18
|
-
exception,
|
19
|
-
request.env[Middleware::WRITING_ROLE_HEADER]
|
20
|
-
)
|
18
|
+
verify_primary(exception, request.env[Middleware::WRITING_ROLE_HEADER])
|
21
19
|
end
|
22
20
|
|
23
21
|
def self.verify_primary(exception, writing_role)
|
@@ -29,11 +27,7 @@ module RailsFailover
|
|
29
27
|
end
|
30
28
|
|
31
29
|
def self.resolve_cause(exception)
|
32
|
-
|
33
|
-
resolve_cause(exception.cause)
|
34
|
-
else
|
35
|
-
exception
|
36
|
-
end
|
30
|
+
exception.cause ? resolve_cause(exception.cause) : exception
|
37
31
|
end
|
38
32
|
end
|
39
33
|
|
@@ -50,14 +44,18 @@ module RailsFailover
|
|
50
44
|
end
|
51
45
|
|
52
46
|
def call(env)
|
53
|
-
current_role = ::ActiveRecord::Base.current_role || ::ActiveRecord
|
54
|
-
is_writing_role = current_role.to_s.end_with?(::ActiveRecord
|
47
|
+
current_role = ::ActiveRecord::Base.current_role || RailsFailover::ActiveRecord.writing_role
|
48
|
+
is_writing_role = current_role.to_s.end_with?(RailsFailover::ActiveRecord.writing_role.to_s)
|
55
49
|
writing_role = resolve_writing_role(current_role, is_writing_role)
|
56
50
|
|
57
51
|
role =
|
58
|
-
if
|
52
|
+
if self.class.force_reading_role_callback&.call(env) ||
|
53
|
+
Handler.instance.primary_down?(writing_role)
|
59
54
|
reading_role = resolve_reading_role(current_role, is_writing_role)
|
60
|
-
ensure_reading_connection_established!(
|
55
|
+
ensure_reading_connection_established!(
|
56
|
+
writing_role: writing_role,
|
57
|
+
reading_role: reading_role,
|
58
|
+
)
|
61
59
|
reading_role
|
62
60
|
else
|
63
61
|
writing_role
|
@@ -76,42 +74,39 @@ module RailsFailover
|
|
76
74
|
private
|
77
75
|
|
78
76
|
def ensure_reading_connection_established!(writing_role:, reading_role:)
|
79
|
-
::ActiveRecord::Base.
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
config
|
87
|
-
|
88
|
-
|
77
|
+
connection_handler = ::ActiveRecord::Base.connection_handler
|
78
|
+
connection_handler
|
79
|
+
.connection_pools(writing_role)
|
80
|
+
.each do |connection_pool|
|
81
|
+
config = connection_pool.db_config.configuration_hash
|
82
|
+
RailsFailover::ActiveRecord.establish_reading_connection(
|
83
|
+
connection_handler,
|
84
|
+
config,
|
85
|
+
role: reading_role,
|
86
|
+
)
|
89
87
|
end
|
90
|
-
|
91
|
-
handler
|
92
|
-
end
|
93
88
|
end
|
94
89
|
|
95
90
|
def resolve_writing_role(current_role, is_writing_role)
|
96
|
-
if is_writing_role
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
/#{::ActiveRecord
|
101
|
-
::ActiveRecord
|
102
|
-
)
|
103
|
-
|
91
|
+
return current_role if is_writing_role
|
92
|
+
current_role
|
93
|
+
.to_s
|
94
|
+
.sub(
|
95
|
+
/#{RailsFailover::ActiveRecord.reading_role}$/,
|
96
|
+
RailsFailover::ActiveRecord.writing_role.to_s,
|
97
|
+
)
|
98
|
+
.to_sym
|
104
99
|
end
|
105
100
|
|
106
101
|
def resolve_reading_role(current_role, is_writing_role)
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
102
|
+
return current_role unless is_writing_role
|
103
|
+
current_role
|
104
|
+
.to_s
|
105
|
+
.sub(
|
106
|
+
/#{RailsFailover::ActiveRecord.writing_role}$/,
|
107
|
+
RailsFailover::ActiveRecord.reading_role.to_s,
|
108
|
+
)
|
109
|
+
.to_sym
|
115
110
|
end
|
116
111
|
end
|
117
112
|
end
|
@@ -4,48 +4,18 @@ module RailsFailover
|
|
4
4
|
module ActiveRecord
|
5
5
|
class Railtie < ::Rails::Railtie
|
6
6
|
initializer "rails_failover.init", after: "active_record.initialize_database" do |app|
|
7
|
-
|
8
|
-
# AR 6.0 / 6.1 compat
|
9
|
-
config =
|
10
|
-
if ::ActiveRecord::Base.respond_to? :connection_db_config
|
11
|
-
::ActiveRecord::Base.connection_db_config.configuration_hash
|
12
|
-
else
|
13
|
-
::ActiveRecord::Base.connection_config
|
14
|
-
end
|
15
|
-
|
16
7
|
app.config.active_record_rails_failover = false
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
#
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
::ActiveRecord::Base.connection_handlers[::ActiveRecord::Base.writing_role].connection_pools.each do |connection_pool|
|
30
|
-
if connection_pool.respond_to?(:db_config)
|
31
|
-
config = connection_pool.db_config.configuration_hash
|
32
|
-
else
|
33
|
-
config = connection_pool.spec.config
|
34
|
-
end
|
35
|
-
RailsFailover::ActiveRecord.establish_reading_connection(
|
36
|
-
::ActiveRecord::Base.connection_handlers[::ActiveRecord::Base.reading_role],
|
37
|
-
config
|
38
|
-
)
|
39
|
-
end
|
40
|
-
|
41
|
-
begin
|
42
|
-
::ActiveRecord::Base.connection
|
43
|
-
rescue ::ActiveRecord::NoDatabaseError
|
44
|
-
# Do nothing since database hasn't been created
|
45
|
-
rescue ::PG::Error, ::ActiveRecord::ConnectionNotEstablished
|
46
|
-
Handler.instance.verify_primary(::ActiveRecord::Base.writing_role)
|
47
|
-
::ActiveRecord::Base.connection_handler = ::ActiveRecord::Base.lookup_connection_handler(:reading)
|
48
|
-
end
|
8
|
+
config = RailsFailover::ActiveRecord.config
|
9
|
+
break unless config[:replica_host] && config[:replica_port]
|
10
|
+
|
11
|
+
app.config.active_record_rails_failover = true
|
12
|
+
::ActiveSupport.on_load(:active_record) do
|
13
|
+
begin
|
14
|
+
::ActiveRecord::Base.connection
|
15
|
+
rescue ::ActiveRecord::NoDatabaseError
|
16
|
+
# Do nothing since database hasn't been created
|
17
|
+
rescue ::PG::Error, ::ActiveRecord::ConnectionNotEstablished
|
18
|
+
Handler.instance.verify_primary(RailsFailover::ActiveRecord.writing_role)
|
49
19
|
end
|
50
20
|
end
|
51
21
|
end
|
@@ -57,14 +27,13 @@ module RailsFailover
|
|
57
27
|
end
|
58
28
|
|
59
29
|
if !skip_middleware?(app.config)
|
60
|
-
app.middleware.unshift(
|
30
|
+
app.middleware.unshift(RailsFailover::ActiveRecord::Middleware)
|
61
31
|
end
|
62
32
|
end
|
63
33
|
end
|
64
34
|
|
65
35
|
def skip_middleware?(config)
|
66
|
-
|
67
|
-
config.skip_rails_failover_active_record_middleware
|
36
|
+
config.try(:skip_rails_failover_active_record_middleware)
|
68
37
|
end
|
69
38
|
end
|
70
39
|
end
|
@@ -1,65 +1,79 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "active_record"
|
4
4
|
|
5
|
-
if defined?(::Rails)
|
6
|
-
require_relative 'active_record/railtie'
|
7
|
-
end
|
5
|
+
require_relative "active_record/railtie" if defined?(::Rails)
|
8
6
|
|
9
|
-
require_relative
|
10
|
-
require_relative
|
7
|
+
require_relative "active_record/middleware"
|
8
|
+
require_relative "active_record/handler"
|
11
9
|
|
12
10
|
module RailsFailover
|
13
11
|
module ActiveRecord
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
class << self
|
13
|
+
def config
|
14
|
+
::ActiveRecord::Base.connection_db_config.configuration_hash
|
15
|
+
end
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
def logger=(logger)
|
18
|
+
@logger = logger
|
19
|
+
end
|
21
20
|
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
def logger
|
22
|
+
@logger || Rails.logger
|
23
|
+
end
|
25
24
|
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
def verify_primary_frequency_seconds=(seconds)
|
26
|
+
@verify_primary_frequency_seconds = seconds
|
27
|
+
end
|
29
28
|
|
30
|
-
|
29
|
+
def verify_primary_frequency_seconds
|
30
|
+
@verify_primary_frequency_seconds || 5
|
31
|
+
end
|
31
32
|
|
32
|
-
|
33
|
+
def establish_reading_connection(handler, config, role: reading_role)
|
34
|
+
return unless config[:replica_host] && config[:replica_port]
|
33
35
|
replica_config = config.dup
|
34
36
|
replica_config[:host] = replica_config.delete(:replica_host)
|
35
37
|
replica_config[:port] = replica_config.delete(:replica_port)
|
36
38
|
replica_config[:replica] = true
|
37
|
-
handler.establish_connection(replica_config)
|
39
|
+
handler.establish_connection(replica_config, role: role)
|
38
40
|
end
|
39
|
-
end
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
|
42
|
+
def register_force_reading_role_callback(&block)
|
43
|
+
Middleware.force_reading_role_callback = block
|
44
|
+
end
|
44
45
|
|
45
|
-
|
46
|
-
|
47
|
-
|
46
|
+
def on_failover(&block)
|
47
|
+
@on_failover_callback = block
|
48
|
+
end
|
48
49
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
50
|
+
def on_failover_callback!(key)
|
51
|
+
@on_failover_callback&.call(key)
|
52
|
+
rescue => e
|
53
|
+
logger.warn(
|
54
|
+
"RailsFailover::ActiveRecord.on_failover failed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}",
|
55
|
+
)
|
56
|
+
end
|
54
57
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
+
def on_fallback(&block)
|
59
|
+
@on_fallback_callback = block
|
60
|
+
end
|
61
|
+
|
62
|
+
def on_fallback_callback!(key)
|
63
|
+
@on_fallback_callback&.call(key)
|
64
|
+
rescue => e
|
65
|
+
logger.warn(
|
66
|
+
"RailsFailover::ActiveRecord.on_fallback failed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}",
|
67
|
+
)
|
68
|
+
end
|
58
69
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
70
|
+
def reading_role
|
71
|
+
::ActiveRecord.try(:reading_role) || ::ActiveRecord::Base.reading_role
|
72
|
+
end
|
73
|
+
|
74
|
+
def writing_role
|
75
|
+
::ActiveRecord.try(:writing_role) || ::ActiveRecord::Base.writing_role
|
76
|
+
end
|
63
77
|
end
|
64
78
|
end
|
65
79
|
end
|
@@ -1,22 +1,21 @@
|
|
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
7
|
class Connector < ::Redis::Client::Connector
|
8
8
|
def initialize(options)
|
9
|
-
|
9
|
+
original_driver = options[:driver]
|
10
10
|
options[:primary_host] = options[:host]
|
11
11
|
options[:primary_port] = options[:port]
|
12
12
|
|
13
13
|
options[:driver] = Class.new(options[:driver]) do
|
14
14
|
def self.connect(options)
|
15
|
-
is_primary =
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
end
|
15
|
+
is_primary =
|
16
|
+
(options[:host] == options[:primary_host]) &&
|
17
|
+
(options[:port] == options[:primary_port])
|
18
|
+
super(options).tap { |conn| conn.rails_failover_role = is_primary ? PRIMARY : REPLICA }
|
20
19
|
rescue ::Redis::TimeoutError,
|
21
20
|
SocketError,
|
22
21
|
Errno::EADDRNOTAVAIL,
|
@@ -27,7 +26,6 @@ module RailsFailover
|
|
27
26
|
Errno::ENOENT,
|
28
27
|
Errno::ETIMEDOUT,
|
29
28
|
Errno::EINVAL => e
|
30
|
-
|
31
29
|
Handler.instance.verify_primary(options) if is_primary
|
32
30
|
raise e
|
33
31
|
end
|
@@ -40,7 +38,7 @@ module RailsFailover
|
|
40
38
|
end
|
41
39
|
end
|
42
40
|
|
43
|
-
options[:original_driver] =
|
41
|
+
options[:original_driver] = original_driver
|
44
42
|
options.delete(:connector)
|
45
43
|
options[:id] ||= "#{options[:host]}:#{options[:port]}"
|
46
44
|
@replica_options = replica_options(options)
|
@@ -48,11 +46,7 @@ module RailsFailover
|
|
48
46
|
end
|
49
47
|
|
50
48
|
def resolve
|
51
|
-
|
52
|
-
@replica_options
|
53
|
-
else
|
54
|
-
@options
|
55
|
-
end
|
49
|
+
Handler.instance.primary_down?(@options) ? @replica_options : @options
|
56
50
|
end
|
57
51
|
|
58
52
|
def check(client)
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
3
|
+
require "monitor"
|
4
|
+
require "singleton"
|
5
|
+
require "concurrent"
|
6
6
|
|
7
7
|
module RailsFailover
|
8
8
|
class Redis
|
@@ -12,7 +12,7 @@ module RailsFailover
|
|
12
12
|
|
13
13
|
PRIMARY_ROLE_STATUS = "role:master"
|
14
14
|
PRIMARY_LOADED_STATUS = "loading:0"
|
15
|
-
|
15
|
+
VERIFY_FREQUENCY_BUFFER_PERCENT = 20
|
16
16
|
SOFT_DISCONNECT_TIMEOUT_SECONDS = 1
|
17
17
|
SOFT_DISCONNECT_POLL_SECONDS = 0.05
|
18
18
|
|
@@ -67,7 +67,7 @@ module RailsFailover
|
|
67
67
|
|
68
68
|
def try_fallback_to_primary
|
69
69
|
frequency = RailsFailover::Redis.verify_primary_frequency_seconds
|
70
|
-
sleep(frequency * ((rand(
|
70
|
+
sleep(frequency * ((rand(VERIFY_FREQUENCY_BUFFER_PERCENT) + 100) / 100.0))
|
71
71
|
|
72
72
|
active_primaries_keys = {}
|
73
73
|
|
@@ -114,10 +114,11 @@ module RailsFailover
|
|
114
114
|
|
115
115
|
def primaries_down
|
116
116
|
ancestor_pids = nil
|
117
|
-
value =
|
118
|
-
|
119
|
-
|
120
|
-
|
117
|
+
value =
|
118
|
+
@primaries_down.compute_if_absent(Process.pid) do
|
119
|
+
ancestor_pids = @primaries_down.keys
|
120
|
+
@primaries_down.values.first || Concurrent::Map.new
|
121
|
+
end
|
121
122
|
|
122
123
|
ancestor_pids&.each do |pid|
|
123
124
|
@primaries_down.delete(pid)&.each { |id, options| verify_primary(options) }
|
@@ -132,10 +133,11 @@ module RailsFailover
|
|
132
133
|
|
133
134
|
def clients
|
134
135
|
ancestor_pids = nil
|
135
|
-
clients_for_pid =
|
136
|
-
|
137
|
-
|
138
|
-
|
136
|
+
clients_for_pid =
|
137
|
+
@clients.compute_if_absent(Process.pid) do
|
138
|
+
ancestor_pids = @clients.keys
|
139
|
+
Concurrent::Map.new
|
140
|
+
end
|
139
141
|
ancestor_pids&.each { |k| @clients.delete(k) }
|
140
142
|
clients_for_pid
|
141
143
|
end
|
@@ -149,9 +151,8 @@ module RailsFailover
|
|
149
151
|
def disconnect_clients(options, role)
|
150
152
|
id = options[:id]
|
151
153
|
|
152
|
-
matched_clients =
|
153
|
-
&.filter { |c| c.connection.rails_failover_role == role }
|
154
|
-
&.to_set
|
154
|
+
matched_clients =
|
155
|
+
clients_for_id(id)&.keys&.filter { |c| c.connection.rails_failover_role == role }&.to_set
|
155
156
|
|
156
157
|
return if matched_clients.nil? || matched_clients.empty?
|
157
158
|
|
data/lib/rails_failover/redis.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "redis"
|
4
4
|
|
5
|
-
supported_version =
|
5
|
+
supported_version = "4"
|
6
6
|
|
7
7
|
if Gem::Version.new(Redis::VERSION) < Gem::Version.new(supported_version)
|
8
8
|
raise "redis #{Redis::VERSION} is not supported. Please upgrade to Redis #{supported_version}."
|
9
9
|
end
|
10
10
|
|
11
11
|
require_relative "../redis/patches/client"
|
12
|
-
require_relative
|
12
|
+
require_relative "redis/connector"
|
13
13
|
|
14
14
|
module RailsFailover
|
15
15
|
class Redis
|
@@ -43,7 +43,9 @@ module RailsFailover
|
|
43
43
|
def self.on_failover_callback!(key)
|
44
44
|
@on_failover_callback&.call(key)
|
45
45
|
rescue => e
|
46
|
-
logger.warn(
|
46
|
+
logger.warn(
|
47
|
+
"RailsFailover::Redis.on_failover failed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}",
|
48
|
+
)
|
47
49
|
end
|
48
50
|
|
49
51
|
def self.on_fallback(&block)
|
@@ -53,7 +55,9 @@ module RailsFailover
|
|
53
55
|
def self.on_fallback_callback!(key)
|
54
56
|
@on_fallback_callback&.call(key)
|
55
57
|
rescue => e
|
56
|
-
logger.warn(
|
58
|
+
logger.warn(
|
59
|
+
"RailsFailover::Redis.on_fallback failed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}",
|
60
|
+
)
|
57
61
|
end
|
58
62
|
|
59
63
|
# For testing
|
data/lib/rails_failover.rb
CHANGED
data/lib/redis/patches/client.rb
CHANGED
data/makefile
CHANGED
@@ -9,13 +9,13 @@ test_active_record:
|
|
9
9
|
@ACTIVE_RECORD=1 bundle exec rspec --tag type:active_record ${RSPEC_PATH}
|
10
10
|
|
11
11
|
setup_dummy_rails_server:
|
12
|
-
@cd spec/support/dummy_app && bundle install --quiet && yarn install && RAILS_ENV=production $(BUNDLER_BIN) exec rails db:create db:migrate db:seed
|
12
|
+
@cd spec/support/dummy_app && BUNDLE_GEMFILE=Gemfile bundle install --quiet && yarn install && BUNDLE_GEMFILE=Gemfile 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
|
15
|
+
@cd spec/support/dummy_app && BUNDLE_GEMFILE=Gemfile 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)
|
19
19
|
|
20
20
|
teardown_dummy_rails_server:
|
21
|
-
@cd spec/support/dummy_app && (! (bundle check > /dev/null 2>&1) || DISABLE_DATABASE_ENVIRONMENT_CHECK=1 RAILS_ENV=production $(BUNDLER_BIN) exec rails db:drop)
|
21
|
+
@cd spec/support/dummy_app && (! (bundle check > /dev/null 2>&1) || BUNDLE_GEMFILE=Gemfile DISABLE_DATABASE_ENVIRONMENT_CHECK=1 RAILS_ENV=production $(BUNDLER_BIN) exec rails db:drop)
|
data/rails_failover.gemspec
CHANGED
@@ -1,29 +1,39 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative "lib/rails_failover/version"
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
|
-
spec.name
|
7
|
-
spec.version
|
8
|
-
spec.authors
|
9
|
-
spec.email
|
6
|
+
spec.name = "rails_failover"
|
7
|
+
spec.version = RailsFailover::VERSION
|
8
|
+
spec.authors = ["Alan Tan"]
|
9
|
+
spec.email = ["tgx@discourse.org"]
|
10
10
|
|
11
|
-
spec.summary
|
12
|
-
spec.homepage
|
13
|
-
spec.license
|
14
|
-
spec.required_ruby_version = Gem::Requirement.new(">=
|
11
|
+
spec.summary = "Failover for ActiveRecord and Redis"
|
12
|
+
spec.homepage = "https://github.com/discourse/rails_failover"
|
13
|
+
spec.license = "MIT"
|
14
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
|
15
15
|
|
16
16
|
# Specify which files should be added to the gem when it is released.
|
17
17
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
18
|
-
spec.files =
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
spec.
|
18
|
+
spec.files =
|
19
|
+
Dir.chdir(File.expand_path("..", __FILE__)) do
|
20
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
21
|
+
end
|
22
|
+
spec.bindir = "exe"
|
23
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
24
|
spec.require_paths = ["lib"]
|
24
25
|
|
25
|
-
spec.add_dependency "activerecord", "
|
26
|
-
spec.add_dependency "railties", "
|
27
|
-
|
26
|
+
spec.add_dependency "activerecord", ">= 6.1", "<= 7.1"
|
27
|
+
spec.add_dependency "railties", ">= 6.1", "<= 7.1"
|
28
28
|
spec.add_dependency "concurrent-ruby"
|
29
|
+
|
30
|
+
spec.add_development_dependency "rake", "~> 12.0"
|
31
|
+
spec.add_development_dependency "redis", "~> 4.1"
|
32
|
+
spec.add_development_dependency "pg", "~> 1.2"
|
33
|
+
spec.add_development_dependency "rack"
|
34
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
35
|
+
spec.add_development_dependency "byebug"
|
36
|
+
spec.add_development_dependency "rubocop-discourse"
|
37
|
+
spec.add_development_dependency "syntax_tree"
|
38
|
+
spec.add_development_dependency "syntax_tree-disable_ternary"
|
29
39
|
end
|
metadata
CHANGED
@@ -1,53 +1,53 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails_failover
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 2.0.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:
|
11
|
+
date: 2023-05-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '6.
|
20
|
-
- - "
|
19
|
+
version: '6.1'
|
20
|
+
- - "<="
|
21
21
|
- !ruby/object:Gem::Version
|
22
22
|
version: '7.1'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
|
-
- - "
|
27
|
+
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: '6.
|
30
|
-
- - "
|
29
|
+
version: '6.1'
|
30
|
+
- - "<="
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: '7.1'
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: railties
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
|
-
- - "
|
37
|
+
- - ">="
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version: '6.
|
40
|
-
- - "
|
39
|
+
version: '6.1'
|
40
|
+
- - "<="
|
41
41
|
- !ruby/object:Gem::Version
|
42
42
|
version: '7.1'
|
43
43
|
type: :runtime
|
44
44
|
prerelease: false
|
45
45
|
version_requirements: !ruby/object:Gem::Requirement
|
46
46
|
requirements:
|
47
|
-
- - "
|
47
|
+
- - ">="
|
48
48
|
- !ruby/object:Gem::Version
|
49
|
-
version: '6.
|
50
|
-
- - "
|
49
|
+
version: '6.1'
|
50
|
+
- - "<="
|
51
51
|
- !ruby/object:Gem::Version
|
52
52
|
version: '7.1'
|
53
53
|
- !ruby/object:Gem::Dependency
|
@@ -64,6 +64,132 @@ dependencies:
|
|
64
64
|
- - ">="
|
65
65
|
- !ruby/object:Gem::Version
|
66
66
|
version: '0'
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: rake
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - "~>"
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '12.0'
|
74
|
+
type: :development
|
75
|
+
prerelease: false
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - "~>"
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '12.0'
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: redis
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '4.1'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - "~>"
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '4.1'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: pg
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - "~>"
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '1.2'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - "~>"
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '1.2'
|
109
|
+
- !ruby/object:Gem::Dependency
|
110
|
+
name: rack
|
111
|
+
requirement: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
type: :development
|
117
|
+
prerelease: false
|
118
|
+
version_requirements: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
- !ruby/object:Gem::Dependency
|
124
|
+
name: rspec
|
125
|
+
requirement: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - "~>"
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '3.0'
|
130
|
+
type: :development
|
131
|
+
prerelease: false
|
132
|
+
version_requirements: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - "~>"
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '3.0'
|
137
|
+
- !ruby/object:Gem::Dependency
|
138
|
+
name: byebug
|
139
|
+
requirement: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0'
|
144
|
+
type: :development
|
145
|
+
prerelease: false
|
146
|
+
version_requirements: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - ">="
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '0'
|
151
|
+
- !ruby/object:Gem::Dependency
|
152
|
+
name: rubocop-discourse
|
153
|
+
requirement: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
type: :development
|
159
|
+
prerelease: false
|
160
|
+
version_requirements: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - ">="
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: '0'
|
165
|
+
- !ruby/object:Gem::Dependency
|
166
|
+
name: syntax_tree
|
167
|
+
requirement: !ruby/object:Gem::Requirement
|
168
|
+
requirements:
|
169
|
+
- - ">="
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
version: '0'
|
172
|
+
type: :development
|
173
|
+
prerelease: false
|
174
|
+
version_requirements: !ruby/object:Gem::Requirement
|
175
|
+
requirements:
|
176
|
+
- - ">="
|
177
|
+
- !ruby/object:Gem::Version
|
178
|
+
version: '0'
|
179
|
+
- !ruby/object:Gem::Dependency
|
180
|
+
name: syntax_tree-disable_ternary
|
181
|
+
requirement: !ruby/object:Gem::Requirement
|
182
|
+
requirements:
|
183
|
+
- - ">="
|
184
|
+
- !ruby/object:Gem::Version
|
185
|
+
version: '0'
|
186
|
+
type: :development
|
187
|
+
prerelease: false
|
188
|
+
version_requirements: !ruby/object:Gem::Requirement
|
189
|
+
requirements:
|
190
|
+
- - ">="
|
191
|
+
- !ruby/object:Gem::Version
|
192
|
+
version: '0'
|
67
193
|
description:
|
68
194
|
email:
|
69
195
|
- tgx@discourse.org
|
@@ -75,6 +201,7 @@ files:
|
|
75
201
|
- ".gitignore"
|
76
202
|
- ".rspec"
|
77
203
|
- ".rubocop.yml"
|
204
|
+
- ".streerc"
|
78
205
|
- CHANGELOG.md
|
79
206
|
- CODE_OF_CONDUCT.md
|
80
207
|
- Gemfile
|
@@ -110,14 +237,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
110
237
|
requirements:
|
111
238
|
- - ">="
|
112
239
|
- !ruby/object:Gem::Version
|
113
|
-
version:
|
240
|
+
version: 3.0.0
|
114
241
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
242
|
requirements:
|
116
243
|
- - ">="
|
117
244
|
- !ruby/object:Gem::Version
|
118
245
|
version: '0'
|
119
246
|
requirements: []
|
120
|
-
rubygems_version: 3.
|
247
|
+
rubygems_version: 3.4.10
|
121
248
|
signing_key:
|
122
249
|
specification_version: 4
|
123
250
|
summary: Failover for ActiveRecord and Redis
|