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 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