rails_failover 0.6.1 → 0.6.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 +4 -4
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +2 -1
- data/lib/rails_failover/active_record/handler.rb +33 -50
- data/lib/rails_failover/redis/handler.rb +63 -97
- data/lib/rails_failover/version.rb +1 -1
- data/rails_failover.gemspec +1 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 152daf08d747b0015a4dddd514ab1478c7fe56e7cfde31900ddbcc7132a32aa8
|
4
|
+
data.tar.gz: 3d64f9b3bad32c9d941e6cd9439e91eab1f4ccc9f3277cd392821eb67d85d9fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 296bef2bacd7b7ae81f6d707faaaec9ff8fe0cbcd252fd3151e35fe3465d16841687e8c8ad151c1b7ea39f914fe7b95da3f5ced9677e5b2678af87f04bb22d73
|
7
|
+
data.tar.gz: c6780fbf53a99acf790bd425d1856c8b3075ccb83433e577189ccdd674837d41f4b845294f0a822886e2141e37960c21ecde350a7100a0a59242ee714fd162e8
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
+
## [0.6.2] - 2020-11-19
|
10
|
+
|
11
|
+
- FIX: Use concurrent-ruby maps to simplify concurrency logic. Resolves a number of possible concurrency issues
|
12
|
+
|
9
13
|
## [0.6.1] - 2020-11-19
|
10
14
|
|
11
15
|
- FIX: Recover correctly if both the primary and replica go offline
|
data/Gemfile.lock
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require 'singleton'
|
3
3
|
require 'monitor'
|
4
|
+
require 'concurrent'
|
4
5
|
|
5
6
|
module RailsFailover
|
6
7
|
module ActiveRecord
|
@@ -11,8 +12,7 @@ module RailsFailover
|
|
11
12
|
VERIFY_FREQUENCY_BUFFER_PRECENT = 20
|
12
13
|
|
13
14
|
def initialize
|
14
|
-
@primaries_down =
|
15
|
-
@ancestor_pid = Process.pid
|
15
|
+
@primaries_down = Concurrent::Map.new
|
16
16
|
|
17
17
|
super() # Monitor#initialize
|
18
18
|
end
|
@@ -22,18 +22,28 @@ module RailsFailover
|
|
22
22
|
|
23
23
|
mon_synchronize do
|
24
24
|
return if @thread&.alive?
|
25
|
-
|
26
25
|
logger.warn "Failover for ActiveRecord has been initiated"
|
26
|
+
@thread = Thread.new { loop_until_all_up }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def primary_down?(handler_key)
|
31
|
+
primaries_down[handler_key]
|
32
|
+
end
|
33
|
+
|
34
|
+
def primaries_down_count
|
35
|
+
primaries_down.size
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
27
39
|
|
28
|
-
|
29
|
-
|
30
|
-
|
40
|
+
def loop_until_all_up
|
41
|
+
loop do
|
42
|
+
initiate_fallback_to_primary
|
31
43
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
end
|
36
|
-
end
|
44
|
+
if all_primaries_up
|
45
|
+
logger.warn "Fallback to primary for ActiveRecord has been completed."
|
46
|
+
break
|
37
47
|
end
|
38
48
|
end
|
39
49
|
end
|
@@ -71,37 +81,17 @@ module RailsFailover
|
|
71
81
|
end
|
72
82
|
end
|
73
83
|
|
74
|
-
def primary_down?(handler_key)
|
75
|
-
primaries_down[handler_key]
|
76
|
-
end
|
77
|
-
|
78
|
-
def primaries_down_count
|
79
|
-
mon_synchronize do
|
80
|
-
primaries_down.count
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
private
|
85
|
-
|
86
84
|
def all_primaries_up
|
87
|
-
|
88
|
-
primaries_down.empty?
|
89
|
-
end
|
85
|
+
primaries_down.empty?
|
90
86
|
end
|
91
87
|
|
92
88
|
def primary_down(handler_key)
|
93
|
-
already_down =
|
94
|
-
mon_synchronize do
|
95
|
-
already_down = !!primaries_down[handler_key]
|
96
|
-
primaries_down[handler_key] = true
|
97
|
-
end
|
89
|
+
already_down = primaries_down.put_if_absent(handler_key, true)
|
98
90
|
RailsFailover::ActiveRecord.on_failover_callback!(handler_key) if !already_down
|
99
91
|
end
|
100
92
|
|
101
93
|
def primary_up(handler_key)
|
102
|
-
already_up =
|
103
|
-
!primaries_down.delete(handler_key)
|
104
|
-
end
|
94
|
+
already_up = !primaries_down.delete(handler_key)
|
105
95
|
RailsFailover::ActiveRecord.on_fallback_callback!(handler_key) if !already_up
|
106
96
|
end
|
107
97
|
|
@@ -110,24 +100,17 @@ module RailsFailover
|
|
110
100
|
end
|
111
101
|
|
112
102
|
def primaries_down
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
@primaries_down[process_pid] = @primaries_down[@ancestor_pid] || {}
|
119
|
-
|
120
|
-
if process_pid != @ancestor_pid
|
121
|
-
@primaries_down.delete(@ancestor_pid)
|
122
|
-
|
123
|
-
@primaries_down[process_pid].each_key do |handler_key|
|
124
|
-
verify_primary(handler_key)
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
103
|
+
ancestor_pids = nil
|
104
|
+
value = @primaries_down.compute_if_absent(Process.pid) do
|
105
|
+
ancestor_pids = @primaries_down.keys
|
106
|
+
@primaries_down.values.first || Concurrent::Map.new
|
107
|
+
end
|
128
108
|
|
129
|
-
|
109
|
+
ancestor_pids&.each do |pid|
|
110
|
+
@primaries_down.delete(pid)&.each_key { |key| verify_primary(key) }
|
130
111
|
end
|
112
|
+
|
113
|
+
value
|
131
114
|
end
|
132
115
|
|
133
116
|
def logger
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'monitor'
|
4
4
|
require 'singleton'
|
5
|
+
require 'concurrent'
|
5
6
|
|
6
7
|
module RailsFailover
|
7
8
|
class Redis
|
@@ -16,41 +17,51 @@ module RailsFailover
|
|
16
17
|
SOFT_DISCONNECT_POLL_SECONDS = 0.05
|
17
18
|
|
18
19
|
def initialize
|
19
|
-
@primaries_down =
|
20
|
-
@clients =
|
21
|
-
@ancestor_pid = Process.pid
|
20
|
+
@primaries_down = Concurrent::Map.new
|
21
|
+
@clients = Concurrent::Map.new
|
22
22
|
|
23
23
|
super() # Monitor#initialize
|
24
24
|
end
|
25
25
|
|
26
26
|
def verify_primary(options)
|
27
|
+
primary_down(options)
|
28
|
+
|
27
29
|
mon_synchronize do
|
28
|
-
|
29
|
-
|
30
|
+
return if @thread&.alive?
|
31
|
+
logger&.warn "Failover for Redis has been initiated"
|
32
|
+
@thread = Thread.new { loop_until_all_up }
|
30
33
|
end
|
31
34
|
end
|
32
35
|
|
33
|
-
def
|
34
|
-
|
36
|
+
def register_client(client)
|
37
|
+
id = client.options[:id]
|
38
|
+
clients_for_id(id).put_if_absent(client, true)
|
39
|
+
end
|
35
40
|
|
36
|
-
|
41
|
+
def deregister_client(client)
|
42
|
+
id = client.options[:id]
|
43
|
+
clients_for_id(id).delete(client)
|
44
|
+
end
|
37
45
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
try_fallback_to_primary
|
46
|
+
def primary_down?(options)
|
47
|
+
primaries_down[options[:id]]
|
48
|
+
end
|
42
49
|
|
43
|
-
|
44
|
-
|
45
|
-
break
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
50
|
+
def primaries_down_count
|
51
|
+
primaries_down.size
|
49
52
|
end
|
50
53
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
+
private
|
55
|
+
|
56
|
+
def loop_until_all_up
|
57
|
+
loop do
|
58
|
+
ensure_primary_clients_disconnected
|
59
|
+
try_fallback_to_primary
|
60
|
+
|
61
|
+
if all_primaries_up
|
62
|
+
logger&.warn "Fallback to primary for Redis has been completed."
|
63
|
+
break
|
64
|
+
end
|
54
65
|
end
|
55
66
|
end
|
56
67
|
|
@@ -60,7 +71,7 @@ module RailsFailover
|
|
60
71
|
|
61
72
|
active_primaries_keys = {}
|
62
73
|
|
63
|
-
|
74
|
+
primaries_down.each do |key, options|
|
64
75
|
info = nil
|
65
76
|
options = options.dup
|
66
77
|
|
@@ -87,103 +98,58 @@ module RailsFailover
|
|
87
98
|
end
|
88
99
|
end
|
89
100
|
|
90
|
-
def register_client(client)
|
91
|
-
key = client.options[:id]
|
92
|
-
|
93
|
-
mon_synchronize do
|
94
|
-
clients[key] ||= []
|
95
|
-
clients[key] << client
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def deregister_client(client)
|
100
|
-
key = client.options[:id]
|
101
|
-
|
102
|
-
mon_synchronize do
|
103
|
-
if clients[key]
|
104
|
-
clients[key].delete(client)
|
105
|
-
|
106
|
-
if clients[key].empty?
|
107
|
-
clients.delete(key)
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
def primary_down?(options)
|
114
|
-
mon_synchronize do
|
115
|
-
primaries_down[options[:id]]
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
def primaries_down_count
|
120
|
-
mon_synchronize do
|
121
|
-
primaries_down.count
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
private
|
126
|
-
|
127
101
|
def all_primaries_up
|
128
|
-
|
102
|
+
primaries_down.empty?
|
129
103
|
end
|
130
104
|
|
131
105
|
def primary_up(options)
|
132
|
-
already_up =
|
133
|
-
!primaries_down.delete(options[:id])
|
134
|
-
end
|
106
|
+
already_up = !primaries_down.delete(options[:id])
|
135
107
|
RailsFailover::Redis.on_fallback_callback!(options[:id]) if !already_up
|
136
108
|
end
|
137
109
|
|
138
110
|
def primary_down(options)
|
139
|
-
already_down =
|
140
|
-
mon_synchronize do
|
141
|
-
already_down = !!primaries_down[options[:id]]
|
142
|
-
primaries_down[options[:id]] = options.dup
|
143
|
-
end
|
111
|
+
already_down = primaries_down.put_if_absent(options[:id], options.dup)
|
144
112
|
RailsFailover::Redis.on_failover_callback!(options[:id]) if !already_down
|
145
113
|
end
|
146
114
|
|
147
|
-
def
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
@clients[process_pid] = {}
|
154
|
-
|
155
|
-
if process_pid != @ancestor_pid
|
156
|
-
@clients.delete(@ancestor_pid)
|
157
|
-
end
|
158
|
-
end
|
115
|
+
def primaries_down
|
116
|
+
ancestor_pids = nil
|
117
|
+
value = @primaries_down.compute_if_absent(Process.pid) do
|
118
|
+
ancestor_pids = @primaries_down.keys
|
119
|
+
@primaries_down.values.first || Concurrent::Map.new
|
120
|
+
end
|
159
121
|
|
160
|
-
|
122
|
+
ancestor_pids&.each do |pid|
|
123
|
+
@primaries_down.delete(pid)&.each { |id, options| verify_primary(options) }
|
161
124
|
end
|
125
|
+
|
126
|
+
value
|
162
127
|
end
|
163
128
|
|
164
|
-
def
|
165
|
-
|
166
|
-
|
129
|
+
def clients_for_id(id)
|
130
|
+
clients.compute_if_absent(id) { Concurrent::Map.new }
|
131
|
+
end
|
167
132
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
end
|
133
|
+
def clients
|
134
|
+
ancestor_pids = nil
|
135
|
+
clients_for_pid = @clients.compute_if_absent(Process.pid) do
|
136
|
+
ancestor_pids = @clients.keys
|
137
|
+
Concurrent::Map.new
|
138
|
+
end
|
139
|
+
ancestor_pids&.each { |k| @clients.delete(k) }
|
140
|
+
clients_for_pid
|
141
|
+
end
|
178
142
|
|
179
|
-
|
143
|
+
def ensure_primary_clients_disconnected
|
144
|
+
primaries_down.each do |key, options|
|
145
|
+
disconnect_clients(options, RailsFailover::Redis::PRIMARY)
|
180
146
|
end
|
181
147
|
end
|
182
148
|
|
183
149
|
def disconnect_clients(options, role)
|
184
|
-
|
150
|
+
id = options[:id]
|
185
151
|
|
186
|
-
matched_clients =
|
152
|
+
matched_clients = clients_for_id(id)&.keys
|
187
153
|
&.filter { |c| c.connection.rails_failover_role == role }
|
188
154
|
&.to_set
|
189
155
|
|
data/rails_failover.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails_failover
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.2
|
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-11-
|
11
|
+
date: 2020-11-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '6.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: concurrent-ruby
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
41
55
|
description:
|
42
56
|
email:
|
43
57
|
- tgx@discourse.org
|