rails_failover 2.1.0 → 2.2.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/.github/workflows/ci.yml +28 -14
- data/.gitignore +2 -1
- data/.rspec +1 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +2 -0
- data/README.md +21 -2
- data/lib/rails_failover/active_record/handler.rb +5 -4
- data/lib/rails_failover/active_record/railtie.rb +1 -1
- data/lib/rails_failover/redis/{connector.rb → compat_4x/connector.rb} +5 -5
- data/lib/rails_failover/redis/compat_4x/handler.rb +26 -0
- data/lib/{redis → rails_failover/redis/compat_4x}/patches/client.rb +3 -0
- data/lib/rails_failover/redis/compat_5x/client.rb +21 -0
- data/lib/rails_failover/redis/compat_5x/config.rb +59 -0
- data/lib/rails_failover/redis/compat_5x/handler.rb +22 -0
- data/lib/rails_failover/redis/compat_5x/patches/client.rb +27 -0
- data/lib/rails_failover/redis/{handler.rb → handler_base.rb} +31 -41
- data/lib/rails_failover/redis.rb +8 -2
- data/lib/rails_failover/version.rb +1 -1
- data/makefile +2 -13
- data/postgresql.mk +30 -10
- data/rails_failover.gemspec +4 -4
- metadata +31 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1246e6e2f88b79171c60042aef00f64877c7292c316bd8aa7cb218a7c663c817
|
4
|
+
data.tar.gz: 1ca0033ff058c31d97b29635d7cdcbde05cf4e09a544cde00ca9f6c3b7e0322a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b51f968031ce4b1313f8ae9a299b3e179689bad66121094085955a2fc9d75d24d4a2fcfafbbd6b6783e363a1329ab6747ddd6d9749bf98b327b304940bb13c73
|
7
|
+
data.tar.gz: 77037a16dc321fadb5866cf11a04e220702c1f8e911986da711943c5565b97589d71f21bbf3fb8ca6c8e41d4cd89e090a44fa391b61cc0c10441fe4ba631982f
|
data/.github/workflows/ci.yml
CHANGED
@@ -16,7 +16,7 @@ jobs:
|
|
16
16
|
- name: Setup ruby
|
17
17
|
uses: ruby/setup-ruby@v1
|
18
18
|
with:
|
19
|
-
ruby-version:
|
19
|
+
ruby-version: "3.2"
|
20
20
|
bundler-cache: true
|
21
21
|
|
22
22
|
- name: Rubocop
|
@@ -29,13 +29,14 @@ jobs:
|
|
29
29
|
bundle exec stree check Gemfile rails_failover.gemspec $(git ls-files '*.rb')
|
30
30
|
|
31
31
|
redis:
|
32
|
-
name:
|
32
|
+
name: "Redis (Redis gem ~> ${{ matrix.redis_gem }}, Ruby ${{ matrix.ruby }})"
|
33
33
|
runs-on: ubuntu-latest
|
34
34
|
|
35
35
|
strategy:
|
36
36
|
fail-fast: false
|
37
37
|
matrix:
|
38
|
-
ruby: [
|
38
|
+
ruby: ["3.4", "3.3", "3.2", "3.1"]
|
39
|
+
redis_gem: ["4.8", "5.3"]
|
39
40
|
|
40
41
|
steps:
|
41
42
|
- uses: actions/checkout@v3
|
@@ -44,31 +45,37 @@ jobs:
|
|
44
45
|
uses: ruby/setup-ruby@v1
|
45
46
|
with:
|
46
47
|
ruby-version: ${{ matrix.ruby }}
|
47
|
-
|
48
|
+
|
49
|
+
- name: Setup gems
|
50
|
+
env:
|
51
|
+
REDIS_GEM_VERSION: ${{ matrix.redis_gem }}
|
52
|
+
run: bundle install
|
48
53
|
|
49
54
|
- name: Setup redis
|
50
55
|
run: sudo apt-get install redis-server
|
51
56
|
|
52
57
|
- name: Redis specs
|
58
|
+
env:
|
59
|
+
REDIS_GEM_VERSION: ${{ matrix.redis_gem }}
|
53
60
|
run: bin/rspec redis
|
54
61
|
|
55
62
|
active_record:
|
56
63
|
runs-on: ubuntu-latest
|
57
|
-
name:
|
64
|
+
name: "ActiveRecord ~>${{ matrix.rails }} (Ruby ${{ matrix.ruby }})"
|
58
65
|
|
59
66
|
strategy:
|
60
67
|
fail-fast: false
|
61
68
|
matrix:
|
62
|
-
ruby: [
|
63
|
-
rails: [
|
69
|
+
ruby: ["3.4", "3.3", "3.2", "3.1"]
|
70
|
+
rails: ["7.1.0", "7.0.0"]
|
64
71
|
include:
|
65
|
-
- ruby:
|
66
|
-
rails:
|
72
|
+
- ruby: "3.2"
|
73
|
+
rails: "6.1.0"
|
67
74
|
exclude:
|
68
|
-
- ruby:
|
69
|
-
rails:
|
70
|
-
- ruby:
|
71
|
-
rails:
|
75
|
+
- ruby: "3.4"
|
76
|
+
rails: "7.0.0"
|
77
|
+
- ruby: "3.3"
|
78
|
+
rails: "7.0.0"
|
72
79
|
|
73
80
|
steps:
|
74
81
|
- uses: actions/checkout@v3
|
@@ -84,13 +91,20 @@ jobs:
|
|
84
91
|
- name: Setup postgres
|
85
92
|
run: |
|
86
93
|
make setup_pg
|
87
|
-
make start_pg
|
88
94
|
|
89
95
|
- name: ActiveRecord specs
|
90
96
|
env:
|
91
97
|
RAILS_VERSION: ${{ matrix.rails }}
|
92
98
|
run: bin/rspec active_record
|
93
99
|
|
100
|
+
- name: Dump Unicorn STDERR logs
|
101
|
+
if: ${{ failure() }}
|
102
|
+
run: cat spec/support/dummy_app/log/unicorn.stderr.log
|
103
|
+
|
104
|
+
- name: Dump Rails logs
|
105
|
+
if: ${{ failure() }}
|
106
|
+
run: cat spec/support/dummy_app/log/production.log
|
107
|
+
|
94
108
|
publish:
|
95
109
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
96
110
|
needs: [lint, redis, active_record]
|
data/.gitignore
CHANGED
data/.rspec
CHANGED
data/CHANGELOG.md
CHANGED
@@ -5,7 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [2.2.0] - 2025-01-29
|
9
|
+
|
10
|
+
- DEV: Add compatibility with the Redis gem version 5.x
|
11
|
+
|
12
|
+
## [2.1.1] - 2024-07-02
|
13
|
+
|
14
|
+
- FIX: Falling back to primary PG server not reliable on Rails 7.1
|
15
|
+
|
8
16
|
## [2.1.0] - 2024-05-29
|
17
|
+
|
9
18
|
- DEV: Update dependencies to officially support Rails 7.1
|
10
19
|
|
11
20
|
## [2.0.1] - 2023-05-30
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -79,7 +79,25 @@ connects_to database: { writing: :primary, second_database_writing: :second_data
|
|
79
79
|
Add `require 'rails_failover/redis'` before creating a `Redis` instance.
|
80
80
|
|
81
81
|
```ruby
|
82
|
-
Redis
|
82
|
+
# Redis/RedisClient 4.x
|
83
|
+
Redis.new(
|
84
|
+
host: "127.0.0.1",
|
85
|
+
port: 6379,
|
86
|
+
replica_host: "127.0.0.1",
|
87
|
+
replica_port: 6380,
|
88
|
+
connector: RailsFailover::Redis::Connector,
|
89
|
+
)
|
90
|
+
|
91
|
+
# Redis/RedisClient 5.x
|
92
|
+
Redis.new(
|
93
|
+
host: "127.0.0.1",
|
94
|
+
port: 6379,
|
95
|
+
client_implementation: RailsFailover::Redis::Client,
|
96
|
+
custom: {
|
97
|
+
replica_host: "127.0.0.1",
|
98
|
+
replica_port: 6380,
|
99
|
+
}
|
100
|
+
)
|
83
101
|
```
|
84
102
|
|
85
103
|
Callbacks can be registered when the primary connection is down and when it is up.
|
@@ -94,6 +112,8 @@ RailsFailover::Redis.on_fallback_callback do
|
|
94
112
|
end
|
95
113
|
```
|
96
114
|
|
115
|
+
> ⚠️ If you’re using Sidekiq, don’t provide it with the replica configuration as it won’t work. RailsFailover works with a replica in read-only mode, meaning Sidekiq wouldn’t work properly anyway as it needs to write to Redis.
|
116
|
+
|
97
117
|
## Development
|
98
118
|
|
99
119
|
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.
|
@@ -107,7 +127,6 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
107
127
|
The ActiveRecord failover tests are run against a dummy Rails server. Run the following commands to run the test:
|
108
128
|
|
109
129
|
1. `make setup_pg`
|
110
|
-
2. `make start_pg`
|
111
130
|
3. `bin/rspec active_record`. You may also run the tests with more unicorn workers by adding the `UNICORN_WORKERS` env variable.
|
112
131
|
|
113
132
|
#### Redis
|
@@ -60,10 +60,11 @@ module RailsFailover
|
|
60
60
|
|
61
61
|
begin
|
62
62
|
connection =
|
63
|
-
::ActiveRecord::Base
|
64
|
-
|
65
|
-
role: handler_key
|
66
|
-
|
63
|
+
::ActiveRecord::Base
|
64
|
+
.connection_handler
|
65
|
+
.retrieve_connection(spec_name, role: handler_key)
|
66
|
+
.tap(&:verify!)
|
67
|
+
|
67
68
|
connection_active = connection.active?
|
68
69
|
rescue => e
|
69
70
|
logger.debug "#{Process.pid} Connection to server for '#{handler_key} #{spec_name}' failed with '#{e.message}'"
|
@@ -11,7 +11,7 @@ module RailsFailover
|
|
11
11
|
app.config.active_record_rails_failover = true
|
12
12
|
::ActiveSupport.on_load(:active_record) do
|
13
13
|
begin
|
14
|
-
::ActiveRecord::Base.connection
|
14
|
+
::ActiveRecord::Base.connection.verify!
|
15
15
|
rescue ::ActiveRecord::NoDatabaseError
|
16
16
|
# Do nothing since database hasn't been created
|
17
17
|
rescue ::PG::Error, ::ActiveRecord::ConnectionNotEstablished
|
@@ -9,6 +9,7 @@ module RailsFailover
|
|
9
9
|
original_driver = options[:driver]
|
10
10
|
options[:primary_host] = options[:host]
|
11
11
|
options[:primary_port] = options[:port]
|
12
|
+
options[:id] ||= "#{options[:host]}:#{options[:port]}"
|
12
13
|
|
13
14
|
options[:driver] = Class.new(options[:driver]) do
|
14
15
|
def self.connect(options)
|
@@ -40,25 +41,24 @@ module RailsFailover
|
|
40
41
|
|
41
42
|
options[:original_driver] = original_driver
|
42
43
|
options.delete(:connector)
|
43
|
-
options[:id] ||= "#{options[:host]}:#{options[:port]}"
|
44
44
|
@replica_options = replica_options(options)
|
45
45
|
@options = options.dup
|
46
46
|
end
|
47
47
|
|
48
48
|
def resolve
|
49
|
-
Handler.instance.primary_down?(@options) ? @replica_options : @options
|
49
|
+
Handler.instance.primary_down?(@options[:id]) ? @replica_options : @options
|
50
50
|
end
|
51
51
|
|
52
52
|
def check(client)
|
53
|
-
Handler.instance.register_client(client)
|
54
|
-
expected_role = Handler.instance.primary_down?(@options) ? REPLICA : PRIMARY
|
53
|
+
Handler.instance.register_client(client, client.options[:id])
|
54
|
+
expected_role = Handler.instance.primary_down?(@options[:id]) ? REPLICA : PRIMARY
|
55
55
|
if client.connection.rails_failover_role != expected_role
|
56
56
|
raise ::Redis::CannotConnectError, "Opened with unexpected failover role"
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
60
60
|
def on_disconnect(client)
|
61
|
-
Handler.instance.deregister_client(client)
|
61
|
+
Handler.instance.deregister_client(client, client.options[:id])
|
62
62
|
end
|
63
63
|
|
64
64
|
private
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../handler_base"
|
4
|
+
|
5
|
+
module RailsFailover
|
6
|
+
class Redis
|
7
|
+
class Handler < HandlerBase
|
8
|
+
def new_primary_client(options)
|
9
|
+
options[:driver] = options[:original_driver]
|
10
|
+
::Redis::Client.new(options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def primary_client_info(client)
|
14
|
+
client.call([:info])
|
15
|
+
end
|
16
|
+
|
17
|
+
def soft_disconnect_original_client(matched_clients, redis, role)
|
18
|
+
# When subscribed, Redis#_client is not a Redis::Client
|
19
|
+
# Instance variable is the only reliable way
|
20
|
+
client = redis.instance_variable_get(:@original_client)
|
21
|
+
return if !matched_clients.include?(client)
|
22
|
+
soft_disconnect(redis, client, role)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,9 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "redis"
|
4
|
+
require "active_support/core_ext/module/delegation"
|
4
5
|
|
5
6
|
# See https://github.com/redis/redis-rb/pull/908
|
6
7
|
class Redis::Client
|
8
|
+
delegate :rails_failover_role, :shutdown_socket, to: :connection, allow_nil: true
|
9
|
+
|
7
10
|
def disconnect
|
8
11
|
if connected?
|
9
12
|
result = connection.disconnect
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsFailover
|
4
|
+
class Redis
|
5
|
+
class Client < ::Redis::Client
|
6
|
+
def initialize(config, **kwargs)
|
7
|
+
super
|
8
|
+
@config = RailsFailover::Redis::Config.new(config)
|
9
|
+
end
|
10
|
+
|
11
|
+
def connect
|
12
|
+
Handler.instance.register_client(self, id)
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_disconnect
|
17
|
+
Handler.instance.deregister_client(self, id)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "delegate"
|
4
|
+
require_relative "handler"
|
5
|
+
|
6
|
+
module RailsFailover
|
7
|
+
class Redis
|
8
|
+
class Config < SimpleDelegator
|
9
|
+
attr_reader :driver, :primary_host, :primary_port, :id
|
10
|
+
attr_accessor :rails_failover_role
|
11
|
+
|
12
|
+
def initialize(object)
|
13
|
+
super
|
14
|
+
@primary_host = object.host
|
15
|
+
@primary_port = object.port
|
16
|
+
@id ||= "#{object.host}:#{object.port}"
|
17
|
+
@driver =
|
18
|
+
Class.new(object.driver) do
|
19
|
+
def connect
|
20
|
+
is_primary =
|
21
|
+
(config.host == config.primary_host) && (config.port == config.primary_port)
|
22
|
+
super.tap { config.rails_failover_role = is_primary ? PRIMARY : REPLICA }
|
23
|
+
rescue ::Redis::TimeoutError,
|
24
|
+
RedisClient::CannotConnectError,
|
25
|
+
SocketError,
|
26
|
+
Errno::EADDRNOTAVAIL,
|
27
|
+
Errno::ECONNREFUSED,
|
28
|
+
Errno::EHOSTDOWN,
|
29
|
+
Errno::EHOSTUNREACH,
|
30
|
+
Errno::ENETUNREACH,
|
31
|
+
Errno::ENOENT,
|
32
|
+
Errno::ETIMEDOUT,
|
33
|
+
Errno::EINVAL => e
|
34
|
+
Handler.instance.verify_primary(config) if is_primary
|
35
|
+
raise e
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def host
|
41
|
+
return super unless Handler.instance.primary_down?(id)
|
42
|
+
custom[:replica_host]
|
43
|
+
end
|
44
|
+
|
45
|
+
def port
|
46
|
+
return super unless Handler.instance.primary_down?(id)
|
47
|
+
custom[:replica_port]
|
48
|
+
end
|
49
|
+
|
50
|
+
def new_primary_client
|
51
|
+
::Redis::Client.new(__getobj__)
|
52
|
+
end
|
53
|
+
|
54
|
+
def [](key)
|
55
|
+
instance_variable_get("@#{key}")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../handler_base"
|
4
|
+
|
5
|
+
module RailsFailover
|
6
|
+
class Redis
|
7
|
+
class Handler < HandlerBase
|
8
|
+
def new_primary_client(config)
|
9
|
+
config.new_primary_client
|
10
|
+
end
|
11
|
+
|
12
|
+
def primary_client_info(client)
|
13
|
+
client.call_v([:info])
|
14
|
+
end
|
15
|
+
|
16
|
+
def soft_disconnect_original_client(matched_clients, redis, role)
|
17
|
+
return if !matched_clients.include?(redis._client)
|
18
|
+
soft_disconnect(redis, redis._client, role)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redis"
|
4
|
+
require "active_support/core_ext/module/delegation"
|
5
|
+
|
6
|
+
class Redis::Client
|
7
|
+
delegate :rails_failover_role, to: :config, allow_nil: true
|
8
|
+
|
9
|
+
alias shutdown_socket close
|
10
|
+
|
11
|
+
def disconnect
|
12
|
+
close
|
13
|
+
on_disconnect
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def on_disconnect
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Redis::SubscribedClient
|
22
|
+
delegate :connected?, to: :@client
|
23
|
+
end
|
24
|
+
|
25
|
+
class RedisClient::PubSub
|
26
|
+
delegate :connected?, to: :@raw_connection, allow_nil: true
|
27
|
+
end
|
@@ -6,7 +6,7 @@ require "concurrent"
|
|
6
6
|
|
7
7
|
module RailsFailover
|
8
8
|
class Redis
|
9
|
-
class
|
9
|
+
class HandlerBase
|
10
10
|
include Singleton
|
11
11
|
include MonitorMixin
|
12
12
|
|
@@ -23,8 +23,8 @@ module RailsFailover
|
|
23
23
|
super() # Monitor#initialize
|
24
24
|
end
|
25
25
|
|
26
|
-
def verify_primary(
|
27
|
-
primary_down(
|
26
|
+
def verify_primary(config)
|
27
|
+
primary_down(config)
|
28
28
|
|
29
29
|
mon_synchronize do
|
30
30
|
return if @thread&.alive?
|
@@ -33,18 +33,16 @@ module RailsFailover
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
def register_client(client)
|
37
|
-
id = client.options[:id]
|
36
|
+
def register_client(client, id)
|
38
37
|
clients_for_id(id).put_if_absent(client, true)
|
39
38
|
end
|
40
39
|
|
41
|
-
def deregister_client(client)
|
42
|
-
id = client.options[:id]
|
40
|
+
def deregister_client(client, id)
|
43
41
|
clients_for_id(id).delete(client)
|
44
42
|
end
|
45
43
|
|
46
|
-
def primary_down?(
|
47
|
-
primaries_down[
|
44
|
+
def primary_down?(id)
|
45
|
+
primaries_down[id]
|
48
46
|
end
|
49
47
|
|
50
48
|
def primaries_down_count
|
@@ -71,15 +69,14 @@ module RailsFailover
|
|
71
69
|
|
72
70
|
active_primaries_keys = {}
|
73
71
|
|
74
|
-
primaries_down.each do |key,
|
72
|
+
primaries_down.each do |key, config|
|
75
73
|
info = nil
|
76
|
-
|
74
|
+
config = config.dup
|
77
75
|
|
78
76
|
begin
|
79
|
-
|
80
|
-
primary_client = ::Redis::Client.new(options)
|
77
|
+
primary_client = new_primary_client(config)
|
81
78
|
logger&.debug "Checking connection to primary server (#{key})"
|
82
|
-
info = primary_client
|
79
|
+
info = primary_client_info(primary_client)
|
83
80
|
rescue => e
|
84
81
|
logger&.debug "Connection to primary server (#{key}) failed with '#{e.message}'"
|
85
82
|
ensure
|
@@ -87,14 +84,14 @@ module RailsFailover
|
|
87
84
|
end
|
88
85
|
|
89
86
|
if info && info.include?(PRIMARY_LOADED_STATUS) && info.include?(PRIMARY_ROLE_STATUS)
|
90
|
-
active_primaries_keys[key] =
|
87
|
+
active_primaries_keys[key] = config
|
91
88
|
logger&.debug "Primary server (#{key}) is active, disconnecting clients from replica"
|
92
89
|
end
|
93
90
|
end
|
94
91
|
|
95
|
-
active_primaries_keys.each do |key,
|
96
|
-
primary_up(
|
97
|
-
disconnect_clients(
|
92
|
+
active_primaries_keys.each do |key, config|
|
93
|
+
primary_up(config)
|
94
|
+
disconnect_clients(config[:id], RailsFailover::Redis::REPLICA)
|
98
95
|
end
|
99
96
|
end
|
100
97
|
|
@@ -102,14 +99,14 @@ module RailsFailover
|
|
102
99
|
primaries_down.empty?
|
103
100
|
end
|
104
101
|
|
105
|
-
def primary_up(
|
106
|
-
already_up = !primaries_down.delete(
|
107
|
-
RailsFailover::Redis.on_fallback_callback!(
|
102
|
+
def primary_up(config)
|
103
|
+
already_up = !primaries_down.delete(config[:id])
|
104
|
+
RailsFailover::Redis.on_fallback_callback!(config[:id]) if !already_up
|
108
105
|
end
|
109
106
|
|
110
|
-
def primary_down(
|
111
|
-
already_down = primaries_down.put_if_absent(
|
112
|
-
RailsFailover::Redis.on_failover_callback!(
|
107
|
+
def primary_down(config)
|
108
|
+
already_down = primaries_down.put_if_absent(config[:id], config.dup)
|
109
|
+
RailsFailover::Redis.on_failover_callback!(config[:id]) if !already_down
|
113
110
|
end
|
114
111
|
|
115
112
|
def primaries_down
|
@@ -121,7 +118,7 @@ module RailsFailover
|
|
121
118
|
end
|
122
119
|
|
123
120
|
ancestor_pids&.each do |pid|
|
124
|
-
@primaries_down.delete(pid)&.each { |id,
|
121
|
+
@primaries_down.delete(pid)&.each { |id, config| verify_primary(config) }
|
125
122
|
end
|
126
123
|
|
127
124
|
value
|
@@ -143,27 +140,21 @@ module RailsFailover
|
|
143
140
|
end
|
144
141
|
|
145
142
|
def ensure_primary_clients_disconnected
|
146
|
-
primaries_down.each do |key,
|
147
|
-
disconnect_clients(
|
143
|
+
primaries_down.each do |key, config|
|
144
|
+
disconnect_clients(config[:id], RailsFailover::Redis::PRIMARY)
|
148
145
|
end
|
149
146
|
end
|
150
147
|
|
151
|
-
def disconnect_clients(
|
152
|
-
id = options[:id]
|
153
|
-
|
148
|
+
def disconnect_clients(id, role)
|
154
149
|
matched_clients =
|
155
|
-
clients_for_id(id)&.keys&.
|
150
|
+
clients_for_id(id)&.keys&.select { _1.rails_failover_role == role }&.to_set
|
156
151
|
|
157
152
|
return if matched_clients.nil? || matched_clients.empty?
|
158
153
|
|
159
154
|
# This is not ideal, but the mutex we need is contained
|
160
155
|
# in the ::Redis instance, not the Redis::Client
|
161
156
|
ObjectSpace.each_object(::Redis) do |redis|
|
162
|
-
|
163
|
-
# Instance variable is the only reliable way
|
164
|
-
client = redis.instance_variable_get(:@original_client)
|
165
|
-
next if !matched_clients.include?(client)
|
166
|
-
soft_disconnect(redis, client, role)
|
157
|
+
soft_disconnect_original_client(matched_clients, redis, role)
|
167
158
|
end
|
168
159
|
end
|
169
160
|
|
@@ -174,7 +165,7 @@ module RailsFailover
|
|
174
165
|
|
175
166
|
if !has_lock
|
176
167
|
begin
|
177
|
-
client.
|
168
|
+
client.shutdown_socket
|
178
169
|
rescue => e
|
179
170
|
logger&.warn "Redis shutdown_socket for (#{role}) failed with #{e.class} '#{e.message}'"
|
180
171
|
end
|
@@ -182,15 +173,14 @@ module RailsFailover
|
|
182
173
|
waiting_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
183
174
|
loop do # Keep trying
|
184
175
|
break if has_lock = redis_mon_try_enter(redis)
|
185
|
-
break if !client.
|
186
|
-
break if client.
|
176
|
+
break if !client.connected? # Disconnected by other thread
|
177
|
+
break if client.rails_failover_role != role # Reconnected by other thread
|
187
178
|
time_now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
188
179
|
break if time_now > waiting_since + SOFT_DISCONNECT_TIMEOUT_SECONDS
|
189
180
|
sleep SOFT_DISCONNECT_POLL_SECONDS
|
190
181
|
end
|
191
182
|
end
|
192
|
-
|
193
|
-
client.disconnect if client.connection&.rails_failover_role == role
|
183
|
+
client.disconnect if client.rails_failover_role == role
|
194
184
|
ensure
|
195
185
|
redis_mon_exit(redis) if has_lock
|
196
186
|
end
|
data/lib/rails_failover/redis.rb
CHANGED
@@ -8,8 +8,14 @@ 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
|
-
|
12
|
-
require_relative "redis/
|
11
|
+
if Redis::VERSION >= "5"
|
12
|
+
require_relative "redis/compat_5x/patches/client"
|
13
|
+
require_relative "redis/compat_5x/config"
|
14
|
+
require_relative "redis/compat_5x/client"
|
15
|
+
else
|
16
|
+
require_relative "redis/compat_4x/patches/client"
|
17
|
+
require_relative "redis/compat_4x/connector"
|
18
|
+
end
|
13
19
|
|
14
20
|
module RailsFailover
|
15
21
|
class Redis
|
data/makefile
CHANGED
@@ -3,19 +3,8 @@ include redis.mk
|
|
3
3
|
|
4
4
|
all: redis
|
5
5
|
|
6
|
-
active_record:
|
6
|
+
active_record: start_pg_primary create_test_database stop_pg test_active_record
|
7
7
|
|
8
8
|
test_active_record:
|
9
|
+
@BUNDLE_GEMFILE=./spec/support/dummy_app/Gemfile bundle install --quiet
|
9
10
|
@ACTIVE_RECORD=1 bundle exec rspec --tag type:active_record ${RSPEC_PATH}
|
10
|
-
|
11
|
-
setup_dummy_rails_server:
|
12
|
-
@cd spec/support/dummy_app && BUNDLE_GEMFILE=Gemfile bundle install --quiet && BUNDLE_GEMFILE=Gemfile RAILS_ENV=production $(BUNDLER_BIN) exec rails db:create db:migrate db:seed
|
13
|
-
|
14
|
-
start_dummy_rails_server:
|
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
|
-
|
17
|
-
stop_dummy_rails_server:
|
18
|
-
@kill -TERM $(shell cat spec/support/dummy_app/tmp/pids/unicorn.pid)
|
19
|
-
|
20
|
-
teardown_dummy_rails_server:
|
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/postgresql.mk
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
PG_BIN_DIR := $(shell pg_config --bindir)
|
2
2
|
PWD := $(shell pwd)
|
3
|
-
PG_PRIMARY_DIR :=
|
3
|
+
PG_PRIMARY_DIR := /tmp/primary
|
4
4
|
PG_PRIMARY_DATA_DIR := $(PG_PRIMARY_DIR)/data
|
5
5
|
PG_PRIMARY_RUN_DIR := $(PG_PRIMARY_DIR)/run
|
6
|
-
PG_REPLICA_DIR :=
|
6
|
+
PG_REPLICA_DIR := /tmp/replica
|
7
7
|
PG_REPLICA_DATA_DIR := $(PG_REPLICA_DIR)/data
|
8
8
|
PG_REPLICA_RUN_DIR := $(PG_REPLICA_DIR)/run
|
9
9
|
PG_PRIMARY_PORT := 5434
|
@@ -14,11 +14,14 @@ PG_REPLICATION_SLOT_NAME := replication
|
|
14
14
|
|
15
15
|
setup_pg: init_primary start_pg_primary setup_primary init_replica stop_pg_primary
|
16
16
|
|
17
|
+
create_test_database:
|
18
|
+
@$(PG_BIN_DIR)/psql -p $(PG_PRIMARY_PORT) -h $(PG_PRIMARY_RUN_DIR) -d postgres --quiet -c "DROP DATABASE IF EXISTS test;"
|
19
|
+
@$(PG_BIN_DIR)/psql -p $(PG_PRIMARY_PORT) -h $(PG_PRIMARY_RUN_DIR) -d postgres --quiet -c "CREATE DATABASE test;"
|
20
|
+
|
17
21
|
setup_primary:
|
18
22
|
@$(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
23
|
@$(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
24
|
@$(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
25
|
|
23
26
|
start_pg: start_pg_primary start_pg_replica
|
24
27
|
|
@@ -36,19 +39,36 @@ init_replica:
|
|
36
39
|
@chmod 0700 $(PG_REPLICA_DATA_DIR)
|
37
40
|
|
38
41
|
start_pg_primary:
|
39
|
-
|
42
|
+
@if [ ! -d "$(PG_PRIMARY_DATA_DIR)" ] || ! $(PG_BIN_DIR)/pg_ctl status -D $(PG_PRIMARY_DATA_DIR) > /dev/null 2>&1; then \
|
43
|
+
$(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; \
|
44
|
+
while ! $(PG_BIN_DIR)/pg_ctl status -D $(PG_PRIMARY_DATA_DIR) > /dev/null 2>&1; do \
|
45
|
+
sleep 1; \
|
46
|
+
done; \
|
47
|
+
fi
|
40
48
|
|
41
49
|
start_pg_replica:
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
50
|
+
@if [ ! -d "$(PG_REPLICA_DATA_DIR)" ] || ! $(PG_BIN_DIR)/pg_ctl status -D $(PG_REPLICA_DATA_DIR) > /dev/null 2>&1; then \
|
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)" start; \
|
52
|
+
while ! $(PG_BIN_DIR)/pg_ctl status -D $(PG_REPLICA_DATA_DIR) > /dev/null 2>&1; do \
|
53
|
+
sleep 1; \
|
54
|
+
done; \
|
55
|
+
fi
|
46
56
|
|
47
57
|
stop_pg_primary:
|
48
|
-
|
58
|
+
@if [ -d "$(PG_PRIMARY_DATA_DIR)" ] && $(PG_BIN_DIR)/pg_ctl status -D $(PG_PRIMARY_DATA_DIR) > /dev/null 2>&1; then \
|
59
|
+
$(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; \
|
60
|
+
while $(PG_BIN_DIR)/pg_ctl status -D $(PG_PRIMARY_DATA_DIR) > /dev/null 2>&1; do \
|
61
|
+
sleep 1; \
|
62
|
+
done; \
|
63
|
+
fi
|
49
64
|
|
50
65
|
stop_pg_replica:
|
51
|
-
|
66
|
+
@if [ -d "$(PG_REPLICA_DATA_DIR)" ] && $(PG_BIN_DIR)/pg_ctl status -D $(PG_REPLICA_DATA_DIR) > /dev/null 2>&1; then \
|
67
|
+
$(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; \
|
68
|
+
while $(PG_BIN_DIR)/pg_ctl status -D $(PG_REPLICA_DATA_DIR) > /dev/null 2>&1; do \
|
69
|
+
sleep 1; \
|
70
|
+
done; \
|
71
|
+
fi
|
52
72
|
|
53
73
|
cleanup_pg:
|
54
74
|
@rm -rf $(PG_PRIMARY_DIR) $(PG_REPLICA_DIR)
|
data/rails_failover.gemspec
CHANGED
@@ -27,11 +27,11 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_dependency "railties", ">= 6.1", "< 8.0"
|
28
28
|
spec.add_dependency "concurrent-ruby"
|
29
29
|
|
30
|
-
spec.add_development_dependency "rake"
|
31
|
-
spec.add_development_dependency "redis", "
|
32
|
-
spec.add_development_dependency "pg"
|
30
|
+
spec.add_development_dependency "rake"
|
31
|
+
spec.add_development_dependency "redis", ">= 4.1", "< 6.0"
|
32
|
+
spec.add_development_dependency "pg"
|
33
33
|
spec.add_development_dependency "rack"
|
34
|
-
spec.add_development_dependency "rspec"
|
34
|
+
spec.add_development_dependency "rspec"
|
35
35
|
spec.add_development_dependency "byebug"
|
36
36
|
spec.add_development_dependency "rubocop-discourse"
|
37
37
|
spec.add_development_dependency "syntax_tree"
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails_failover
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alan Tan
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-01-29 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: activerecord
|
@@ -68,44 +67,50 @@ dependencies:
|
|
68
67
|
name: rake
|
69
68
|
requirement: !ruby/object:Gem::Requirement
|
70
69
|
requirements:
|
71
|
-
- - "
|
70
|
+
- - ">="
|
72
71
|
- !ruby/object:Gem::Version
|
73
|
-
version: '
|
72
|
+
version: '0'
|
74
73
|
type: :development
|
75
74
|
prerelease: false
|
76
75
|
version_requirements: !ruby/object:Gem::Requirement
|
77
76
|
requirements:
|
78
|
-
- - "
|
77
|
+
- - ">="
|
79
78
|
- !ruby/object:Gem::Version
|
80
|
-
version: '
|
79
|
+
version: '0'
|
81
80
|
- !ruby/object:Gem::Dependency
|
82
81
|
name: redis
|
83
82
|
requirement: !ruby/object:Gem::Requirement
|
84
83
|
requirements:
|
85
|
-
- - "
|
84
|
+
- - ">="
|
86
85
|
- !ruby/object:Gem::Version
|
87
86
|
version: '4.1'
|
87
|
+
- - "<"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '6.0'
|
88
90
|
type: :development
|
89
91
|
prerelease: false
|
90
92
|
version_requirements: !ruby/object:Gem::Requirement
|
91
93
|
requirements:
|
92
|
-
- - "
|
94
|
+
- - ">="
|
93
95
|
- !ruby/object:Gem::Version
|
94
96
|
version: '4.1'
|
97
|
+
- - "<"
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '6.0'
|
95
100
|
- !ruby/object:Gem::Dependency
|
96
101
|
name: pg
|
97
102
|
requirement: !ruby/object:Gem::Requirement
|
98
103
|
requirements:
|
99
|
-
- - "
|
104
|
+
- - ">="
|
100
105
|
- !ruby/object:Gem::Version
|
101
|
-
version: '
|
106
|
+
version: '0'
|
102
107
|
type: :development
|
103
108
|
prerelease: false
|
104
109
|
version_requirements: !ruby/object:Gem::Requirement
|
105
110
|
requirements:
|
106
|
-
- - "
|
111
|
+
- - ">="
|
107
112
|
- !ruby/object:Gem::Version
|
108
|
-
version: '
|
113
|
+
version: '0'
|
109
114
|
- !ruby/object:Gem::Dependency
|
110
115
|
name: rack
|
111
116
|
requirement: !ruby/object:Gem::Requirement
|
@@ -124,16 +129,16 @@ dependencies:
|
|
124
129
|
name: rspec
|
125
130
|
requirement: !ruby/object:Gem::Requirement
|
126
131
|
requirements:
|
127
|
-
- - "
|
132
|
+
- - ">="
|
128
133
|
- !ruby/object:Gem::Version
|
129
|
-
version: '
|
134
|
+
version: '0'
|
130
135
|
type: :development
|
131
136
|
prerelease: false
|
132
137
|
version_requirements: !ruby/object:Gem::Requirement
|
133
138
|
requirements:
|
134
|
-
- - "
|
139
|
+
- - ">="
|
135
140
|
- !ruby/object:Gem::Version
|
136
|
-
version: '
|
141
|
+
version: '0'
|
137
142
|
- !ruby/object:Gem::Dependency
|
138
143
|
name: byebug
|
139
144
|
requirement: !ruby/object:Gem::Requirement
|
@@ -190,7 +195,6 @@ dependencies:
|
|
190
195
|
- - ">="
|
191
196
|
- !ruby/object:Gem::Version
|
192
197
|
version: '0'
|
193
|
-
description:
|
194
198
|
email:
|
195
199
|
- tgx@discourse.org
|
196
200
|
executables: []
|
@@ -217,10 +221,15 @@ files:
|
|
217
221
|
- lib/rails_failover/active_record/middleware.rb
|
218
222
|
- lib/rails_failover/active_record/railtie.rb
|
219
223
|
- lib/rails_failover/redis.rb
|
220
|
-
- lib/rails_failover/redis/connector.rb
|
221
|
-
- lib/rails_failover/redis/handler.rb
|
224
|
+
- lib/rails_failover/redis/compat_4x/connector.rb
|
225
|
+
- lib/rails_failover/redis/compat_4x/handler.rb
|
226
|
+
- lib/rails_failover/redis/compat_4x/patches/client.rb
|
227
|
+
- lib/rails_failover/redis/compat_5x/client.rb
|
228
|
+
- lib/rails_failover/redis/compat_5x/config.rb
|
229
|
+
- lib/rails_failover/redis/compat_5x/handler.rb
|
230
|
+
- lib/rails_failover/redis/compat_5x/patches/client.rb
|
231
|
+
- lib/rails_failover/redis/handler_base.rb
|
222
232
|
- lib/rails_failover/version.rb
|
223
|
-
- lib/redis/patches/client.rb
|
224
233
|
- makefile
|
225
234
|
- postgresql.mk
|
226
235
|
- rails_failover.gemspec
|
@@ -229,7 +238,6 @@ homepage: https://github.com/discourse/rails_failover
|
|
229
238
|
licenses:
|
230
239
|
- MIT
|
231
240
|
metadata: {}
|
232
|
-
post_install_message:
|
233
241
|
rdoc_options: []
|
234
242
|
require_paths:
|
235
243
|
- lib
|
@@ -244,8 +252,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
244
252
|
- !ruby/object:Gem::Version
|
245
253
|
version: '0'
|
246
254
|
requirements: []
|
247
|
-
rubygems_version: 3.
|
248
|
-
signing_key:
|
255
|
+
rubygems_version: 3.6.2
|
249
256
|
specification_version: 4
|
250
257
|
summary: Failover for ActiveRecord and Redis
|
251
258
|
test_files: []
|