rails_failover 0.6.1 → 0.6.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|