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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd1e8f697f988b96e0c149599da43cd69946dd19b06cbe8be89727f6d0775776
4
- data.tar.gz: 1119bda19483587f9dc3a7c3a41017b30d66f266342c7e818d1fe9e16a14c997
3
+ metadata.gz: 152daf08d747b0015a4dddd514ab1478c7fe56e7cfde31900ddbcc7132a32aa8
4
+ data.tar.gz: 3d64f9b3bad32c9d941e6cd9439e91eab1f4ccc9f3277cd392821eb67d85d9fa
5
5
  SHA512:
6
- metadata.gz: 4a1f151de2cb49d0728d01de7e62b569f80a025e21401b083a5662e8f967b29c1a6df8b283f02da047b5ce39c190363768b892144d6c28e9dffc2aad2c69d26f
7
- data.tar.gz: 9b48b8054f3ae13ec473b7a0869bdbb09ff7f05c6149e9602d1cd2c131383b0be3701ee607aeac2d5cd013e5a59f187a009799e948c4e9bcb7598763ef8932d6
6
+ metadata.gz: 296bef2bacd7b7ae81f6d707faaaec9ff8fe0cbcd252fd3151e35fe3465d16841687e8c8ad151c1b7ea39f914fe7b95da3f5ced9677e5b2678af87f04bb22d73
7
+ data.tar.gz: c6780fbf53a99acf790bd425d1856c8b3075ccb83433e577189ccdd674837d41f4b845294f0a822886e2141e37960c21ecde350a7100a0a59242ee714fd162e8
@@ -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
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rails_failover (0.6.1)
4
+ rails_failover (0.6.2)
5
5
  activerecord (~> 6.0)
6
+ concurrent-ruby
6
7
  railties (~> 6.0)
7
8
 
8
9
  GEM
@@ -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
- @thread = Thread.new do
29
- loop do
30
- initiate_fallback_to_primary
40
+ def loop_until_all_up
41
+ loop do
42
+ initiate_fallback_to_primary
31
43
 
32
- if all_primaries_up
33
- logger.warn "Fallback to primary for ActiveRecord has been completed."
34
- break
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
- mon_synchronize do
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 = false
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 = mon_synchronize do
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
- process_pid = Process.pid
114
- return @primaries_down[process_pid] if @primaries_down[process_pid]
115
-
116
- mon_synchronize do
117
- if !@primaries_down[process_pid]
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
- @primaries_down[process_pid]
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
- primary_down(options)
29
- ensure_failover_thread_running
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 ensure_failover_thread_running
34
- return if @thread&.alive?
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
- logger&.warn "Failover for Redis has been initiated"
41
+ def deregister_client(client)
42
+ id = client.options[:id]
43
+ clients_for_id(id).delete(client)
44
+ end
37
45
 
38
- @thread = Thread.new do
39
- loop do
40
- ensure_primary_clients_disconnected
41
- try_fallback_to_primary
46
+ def primary_down?(options)
47
+ primaries_down[options[:id]]
48
+ end
42
49
 
43
- if all_primaries_up
44
- logger&.warn "Fallback to primary for Redis has been completed."
45
- break
46
- end
47
- end
48
- end
50
+ def primaries_down_count
51
+ primaries_down.size
49
52
  end
50
53
 
51
- def ensure_primary_clients_disconnected
52
- mon_synchronize { primaries_down.dup }.each do |key, options|
53
- disconnect_clients(options, RailsFailover::Redis::PRIMARY)
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
- mon_synchronize { primaries_down.dup }.each do |key, options|
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
- mon_synchronize { primaries_down.empty? }
102
+ primaries_down.empty?
129
103
  end
130
104
 
131
105
  def primary_up(options)
132
- already_up = mon_synchronize do
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 = false
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 clients
148
- process_pid = Process.pid
149
- return @clients[process_pid] if @clients[process_pid]
150
-
151
- mon_synchronize do
152
- if !@clients[process_pid]
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
- @clients[process_pid]
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 primaries_down
165
- process_pid = Process.pid
166
- return @primaries_down[process_pid] if @primaries_down[process_pid]
129
+ def clients_for_id(id)
130
+ clients.compute_if_absent(id) { Concurrent::Map.new }
131
+ end
167
132
 
168
- mon_synchronize do
169
- if !@primaries_down[process_pid]
170
- @primaries_down[process_pid] = @primaries_down[@ancestor_pid] || {}
171
-
172
- if process_pid != @ancestor_pid
173
- @primaries_down.delete(@ancestor_pid)&.each do |id, options|
174
- verify_primary(options)
175
- end
176
- end
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
- @primaries_down[process_pid]
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
- key = options[:id]
150
+ id = options[:id]
185
151
 
186
- matched_clients = mon_synchronize { clients[key].dup }
152
+ matched_clients = clients_for_id(id)&.keys
187
153
  &.filter { |c| c.connection.rails_failover_role == role }
188
154
  &.to_set
189
155
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsFailover
4
- VERSION = "0.6.1"
4
+ VERSION = "0.6.2"
5
5
  end
@@ -24,4 +24,5 @@ Gem::Specification.new do |spec|
24
24
  ["activerecord", "railties"].each do |gem_name|
25
25
  spec.add_dependency gem_name, "~> 6.0"
26
26
  end
27
+ spec.add_dependency "concurrent-ruby"
27
28
  end
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.1
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-20 00:00:00.000000000 Z
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