redis_failover 0.9.7.2 → 1.0.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.
- data/Changes.md +14 -0
- data/README.md +57 -20
- data/examples/config.yml +3 -0
- data/lib/redis_failover.rb +4 -1
- data/lib/redis_failover/cli.rb +25 -2
- data/lib/redis_failover/client.rb +25 -10
- data/lib/redis_failover/errors.rb +0 -4
- data/lib/redis_failover/failover_strategy.rb +25 -0
- data/lib/redis_failover/failover_strategy/latency.rb +21 -0
- data/lib/redis_failover/manual_failover.rb +16 -4
- data/lib/redis_failover/node.rb +2 -1
- data/lib/redis_failover/node_manager.rb +419 -144
- data/lib/redis_failover/node_snapshot.rb +81 -0
- data/lib/redis_failover/node_strategy.rb +34 -0
- data/lib/redis_failover/node_strategy/consensus.rb +18 -0
- data/lib/redis_failover/node_strategy/majority.rb +18 -0
- data/lib/redis_failover/node_strategy/single.rb +17 -0
- data/lib/redis_failover/node_watcher.rb +13 -13
- data/lib/redis_failover/util.rb +12 -4
- data/lib/redis_failover/version.rb +1 -1
- data/redis_failover.gemspec +1 -1
- data/spec/failover_strategy/latency_spec.rb +41 -0
- data/spec/failover_strategy_spec.rb +17 -0
- data/spec/node_snapshot_spec.rb +30 -0
- data/spec/node_strategy/consensus_spec.rb +30 -0
- data/spec/node_strategy/majority_spec.rb +22 -0
- data/spec/node_strategy/single_spec.rb +22 -0
- data/spec/node_strategy_spec.rb +22 -0
- data/spec/node_watcher_spec.rb +2 -2
- data/spec/spec_helper.rb +2 -1
- data/spec/support/node_manager_stub.rb +29 -8
- metadata +35 -8
data/lib/redis_failover/node.rb
CHANGED
@@ -22,7 +22,8 @@ module RedisFailover
|
|
22
22
|
# @option options [String] :host the host of the redis server
|
23
23
|
# @option options [String] :port the port of the redis server
|
24
24
|
def initialize(options = {})
|
25
|
-
@host = options
|
25
|
+
@host = options[:host]
|
26
|
+
raise InvalidNodeError, 'missing host' if @host.to_s.empty?
|
26
27
|
@port = Integer(options[:port] || 6379)
|
27
28
|
@password = options[:password]
|
28
29
|
end
|
@@ -3,21 +3,20 @@ module RedisFailover
|
|
3
3
|
# will discover the current redis master and slaves. Each redis node is
|
4
4
|
# monitored by a NodeWatcher instance. The NodeWatchers periodically
|
5
5
|
# report the current state of the redis node it's watching to the
|
6
|
-
# NodeManager
|
7
|
-
#
|
8
|
-
#
|
6
|
+
# NodeManager. The NodeManager processes the state reports and reacts
|
7
|
+
# appropriately by handling stale/dead nodes, and promoting a new redis master
|
8
|
+
# if it sees fit to do so.
|
9
9
|
class NodeManager
|
10
10
|
include Util
|
11
11
|
|
12
12
|
# Number of seconds to wait before retrying bootstrap process.
|
13
|
-
TIMEOUT =
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
].freeze
|
13
|
+
TIMEOUT = 5
|
14
|
+
# Number of seconds for checking node snapshots.
|
15
|
+
CHECK_INTERVAL = 5
|
16
|
+
# Number of max attempts to promote a master before releasing master lock.
|
17
|
+
MAX_PROMOTION_ATTEMPTS = 3
|
18
|
+
# Latency threshold for recording node state.
|
19
|
+
LATENCY_THRESHOLD = 0.5
|
21
20
|
|
22
21
|
# Errors that can happen during the node discovery process.
|
23
22
|
NODE_DISCOVERY_ERRORS = [
|
@@ -38,15 +37,16 @@ module RedisFailover
|
|
38
37
|
def initialize(options)
|
39
38
|
logger.info("Redis Node Manager v#{VERSION} starting (#{RUBY_DESCRIPTION})")
|
40
39
|
@options = options
|
41
|
-
@
|
42
|
-
@
|
43
|
-
@
|
40
|
+
@required_node_managers = options.fetch(:required_node_managers, 1)
|
41
|
+
@root_znode = options.fetch(:znode_path, Util::DEFAULT_ROOT_ZNODE_PATH)
|
42
|
+
@node_strategy = NodeStrategy.for(options.fetch(:node_strategy, :majority))
|
43
|
+
@failover_strategy = FailoverStrategy.for(options.fetch(:failover_strategy, :latency))
|
44
|
+
@nodes = Array(@options[:nodes]).map { |opts| Node.new(opts) }.uniq
|
45
|
+
@master_manager = false
|
46
|
+
@master_promotion_attempts = 0
|
47
|
+
@sufficient_node_managers = false
|
48
|
+
@lock = Monitor.new
|
44
49
|
@shutdown = false
|
45
|
-
@leader = false
|
46
|
-
@master = nil
|
47
|
-
@slaves = []
|
48
|
-
@unavailable = []
|
49
|
-
@lock_path = "#{@znode}_lock".freeze
|
50
50
|
end
|
51
51
|
|
52
52
|
# Starts the node manager.
|
@@ -54,21 +54,18 @@ module RedisFailover
|
|
54
54
|
# @note This method does not return until the manager terminates.
|
55
55
|
def start
|
56
56
|
return unless running?
|
57
|
-
@queue = Queue.new
|
58
57
|
setup_zk
|
59
|
-
|
60
|
-
|
61
|
-
@leader = true
|
62
|
-
logger.info('Acquired master Node Manager lock')
|
63
|
-
if discover_nodes
|
64
|
-
initialize_path
|
65
|
-
spawn_watchers
|
66
|
-
handle_state_reports
|
67
|
-
end
|
68
|
-
end
|
58
|
+
spawn_watchers
|
59
|
+
wait_until_master
|
69
60
|
rescue *ZK_ERRORS => ex
|
70
61
|
logger.error("ZK error while attempting to manage nodes: #{ex.inspect}")
|
71
62
|
reset
|
63
|
+
sleep(TIMEOUT)
|
64
|
+
retry
|
65
|
+
rescue NoMasterError
|
66
|
+
logger.error("Failed to promote a new master after #{MAX_PROMOTION_ATTEMPTS} attempts.")
|
67
|
+
reset
|
68
|
+
sleep(TIMEOUT)
|
72
69
|
retry
|
73
70
|
end
|
74
71
|
|
@@ -77,81 +74,58 @@ module RedisFailover
|
|
77
74
|
#
|
78
75
|
# @param [Node] node the node
|
79
76
|
# @param [Symbol] state the state
|
80
|
-
|
81
|
-
|
77
|
+
# @param [Integer] latency an optional latency
|
78
|
+
def notify_state(node, state, latency = nil)
|
79
|
+
@lock.synchronize do
|
80
|
+
if running?
|
81
|
+
update_current_state(node, state, latency)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
rescue => ex
|
85
|
+
logger.error("Error handling state report #{[node, state].inspect}: #{ex.inspect}")
|
86
|
+
logger.error(ex.backtrace.join("\n"))
|
82
87
|
end
|
83
88
|
|
84
89
|
# Performs a reset of the manager.
|
85
90
|
def reset
|
86
|
-
@
|
91
|
+
@master_manager = false
|
92
|
+
@master_promotion_attempts = 0
|
87
93
|
@watchers.each(&:shutdown) if @watchers
|
88
|
-
@queue.clear
|
89
|
-
@zk.close! if @zk
|
90
|
-
@zk_lock = nil
|
91
94
|
end
|
92
95
|
|
93
96
|
# Initiates a graceful shutdown.
|
94
97
|
def shutdown
|
95
98
|
logger.info('Shutting down ...')
|
96
|
-
@
|
99
|
+
@lock.synchronize do
|
97
100
|
@shutdown = true
|
98
|
-
unless @leader
|
99
|
-
reset
|
100
|
-
end
|
101
101
|
end
|
102
|
+
|
103
|
+
reset
|
104
|
+
exit
|
102
105
|
end
|
103
106
|
|
104
107
|
private
|
105
108
|
|
106
109
|
# Configures the ZooKeeper client.
|
107
110
|
def setup_zk
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
@zk.register(@manual_znode) do |event|
|
113
|
-
if event.node_created? || event.node_changed?
|
114
|
-
perform_manual_failover
|
111
|
+
unless @zk
|
112
|
+
@zk = ZK.new("#{@options[:zkservers]}#{@options[:chroot] || ''}")
|
113
|
+
@zk.register(manual_failover_path) do |event|
|
114
|
+
handle_manual_failover_update(event)
|
115
115
|
end
|
116
|
+
@zk.on_connected { @zk.stat(manual_failover_path, :watch => true) }
|
116
117
|
end
|
117
118
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
# Handles periodic state reports from {RedisFailover::NodeWatcher} instances.
|
123
|
-
def handle_state_reports
|
124
|
-
while running? && (state_report = @queue.pop)
|
125
|
-
begin
|
126
|
-
@mutex.synchronize do
|
127
|
-
return unless running?
|
128
|
-
@zk_lock.assert!
|
129
|
-
node, state = state_report
|
130
|
-
case state
|
131
|
-
when :unavailable then handle_unavailable(node)
|
132
|
-
when :available then handle_available(node)
|
133
|
-
when :syncing then handle_syncing(node)
|
134
|
-
when :zk_disconnected then raise ZKDisconnectedError
|
135
|
-
else raise InvalidNodeStateError.new(node, state)
|
136
|
-
end
|
137
|
-
|
138
|
-
# flush current state
|
139
|
-
write_state
|
140
|
-
end
|
141
|
-
rescue *ZK_ERRORS
|
142
|
-
# fail hard if this is a ZK connection-related error
|
143
|
-
raise
|
144
|
-
rescue => ex
|
145
|
-
logger.error("Error handling #{state_report.inspect}: #{ex.inspect}")
|
146
|
-
logger.error(ex.backtrace.join("\n"))
|
147
|
-
end
|
148
|
-
end
|
119
|
+
create_path(@root_znode)
|
120
|
+
create_path(current_state_root)
|
121
|
+
@zk.stat(manual_failover_path, :watch => true)
|
149
122
|
end
|
150
123
|
|
151
124
|
# Handles an unavailable node.
|
152
125
|
#
|
153
126
|
# @param [Node] node the unavailable node
|
154
|
-
|
127
|
+
# @param [Hash<Node, NodeSnapshot>] snapshots the current set of snapshots
|
128
|
+
def handle_unavailable(node, snapshots)
|
155
129
|
# no-op if we already know about this node
|
156
130
|
return if @unavailable.include?(node)
|
157
131
|
logger.info("Handling unavailable node: #{node}")
|
@@ -160,7 +134,7 @@ module RedisFailover
|
|
160
134
|
# find a new master if this node was a master
|
161
135
|
if node == @master
|
162
136
|
logger.info("Demoting currently unavailable master #{node}.")
|
163
|
-
promote_new_master
|
137
|
+
promote_new_master(snapshots)
|
164
138
|
else
|
165
139
|
@slaves.delete(node)
|
166
140
|
end
|
@@ -169,7 +143,8 @@ module RedisFailover
|
|
169
143
|
# Handles an available node.
|
170
144
|
#
|
171
145
|
# @param [Node] node the available node
|
172
|
-
|
146
|
+
# @param [Hash<Node, NodeSnapshot>] snapshots the current set of snapshots
|
147
|
+
def handle_available(node, snapshots)
|
173
148
|
reconcile(node)
|
174
149
|
|
175
150
|
# no-op if we already know about this node
|
@@ -182,7 +157,7 @@ module RedisFailover
|
|
182
157
|
@slaves << node
|
183
158
|
else
|
184
159
|
# no master exists, make this the new master
|
185
|
-
promote_new_master(node)
|
160
|
+
promote_new_master(snapshots, node)
|
186
161
|
end
|
187
162
|
|
188
163
|
@unavailable.delete(node)
|
@@ -191,74 +166,75 @@ module RedisFailover
|
|
191
166
|
# Handles a node that is currently syncing.
|
192
167
|
#
|
193
168
|
# @param [Node] node the syncing node
|
194
|
-
|
169
|
+
# @param [Hash<Node, NodeSnapshot>] snapshots the current set of snapshots
|
170
|
+
def handle_syncing(node, snapshots)
|
195
171
|
reconcile(node)
|
196
172
|
|
197
173
|
if node.syncing_with_master? && node.prohibits_stale_reads?
|
198
174
|
logger.info("Node #{node} not ready yet, still syncing with master.")
|
199
175
|
force_unavailable_slave(node)
|
200
|
-
|
176
|
+
else
|
177
|
+
# otherwise, we can use this node
|
178
|
+
handle_available(node, snapshots)
|
201
179
|
end
|
202
|
-
|
203
|
-
# otherwise, we can use this node
|
204
|
-
handle_available(node)
|
205
180
|
end
|
206
181
|
|
207
182
|
# Handles a manual failover request to the given node.
|
208
183
|
#
|
209
184
|
# @param [Node] node the candidate node for failover
|
210
|
-
|
185
|
+
# @param [Hash<Node, NodeSnapshot>] snapshots the current set of snapshots
|
186
|
+
def handle_manual_failover(node, snapshots)
|
211
187
|
# no-op if node to be failed over is already master
|
212
188
|
return if @master == node
|
213
189
|
logger.info("Handling manual failover")
|
214
190
|
|
191
|
+
# ensure we can talk to the node
|
192
|
+
node.ping
|
193
|
+
|
215
194
|
# make current master a slave, and promote new master
|
216
195
|
@slaves << @master if @master
|
217
196
|
@slaves.delete(node)
|
218
|
-
promote_new_master(node)
|
197
|
+
promote_new_master(snapshots, node)
|
219
198
|
end
|
220
199
|
|
221
200
|
# Promotes a new master.
|
222
201
|
#
|
202
|
+
# @param [Hash<Node, NodeSnapshot>] snapshots the current set of snapshots
|
223
203
|
# @param [Node] node the optional node to promote
|
224
|
-
|
225
|
-
|
226
|
-
delete_path
|
204
|
+
def promote_new_master(snapshots, node = nil)
|
205
|
+
delete_path(redis_nodes_path)
|
227
206
|
@master = nil
|
228
207
|
|
229
|
-
# make a specific node or
|
230
|
-
candidate = node ||
|
231
|
-
|
208
|
+
# make a specific node or selected candidate the new master
|
209
|
+
candidate = node || failover_strategy_candidate(snapshots)
|
210
|
+
|
211
|
+
if candidate.nil?
|
232
212
|
logger.error('Failed to promote a new master, no candidate available.')
|
233
|
-
|
213
|
+
else
|
214
|
+
@slaves.delete(candidate)
|
215
|
+
@unavailable.delete(candidate)
|
216
|
+
redirect_slaves_to(candidate)
|
217
|
+
candidate.make_master!
|
218
|
+
@master = candidate
|
219
|
+
write_current_redis_nodes
|
220
|
+
@master_promotion_attempts = 0
|
221
|
+
logger.info("Successfully promoted #{candidate} to master.")
|
234
222
|
end
|
235
|
-
|
236
|
-
redirect_slaves_to(candidate)
|
237
|
-
candidate.make_master!
|
238
|
-
@master = candidate
|
239
|
-
|
240
|
-
create_path
|
241
|
-
write_state
|
242
|
-
logger.info("Successfully promoted #{candidate} to master.")
|
243
223
|
end
|
244
224
|
|
245
225
|
# Discovers the current master and slave nodes.
|
246
226
|
# @return [Boolean] true if nodes successfully discovered, false otherwise
|
247
227
|
def discover_nodes
|
248
|
-
@
|
249
|
-
return
|
250
|
-
|
228
|
+
@lock.synchronize do
|
229
|
+
return unless running?
|
230
|
+
@slaves, @unavailable = [], []
|
251
231
|
if @master = find_existing_master
|
252
232
|
logger.info("Using master #{@master} from existing znode config.")
|
253
|
-
elsif @master = guess_master(nodes)
|
233
|
+
elsif @master = guess_master(@nodes)
|
254
234
|
logger.info("Guessed master #{@master} from known redis nodes.")
|
255
235
|
end
|
256
|
-
@slaves = nodes - [@master]
|
257
|
-
logger.info("Managing master (#{@master}) and slaves "
|
258
|
-
"(#{@slaves.map(&:to_s).join(', ')})")
|
259
|
-
# ensure that slaves are correctly pointing to this master
|
260
|
-
redirect_slaves_to(@master)
|
261
|
-
true
|
236
|
+
@slaves = @nodes - [@master]
|
237
|
+
logger.info("Managing master (#{@master}) and slaves #{stringify_nodes(@slaves)}")
|
262
238
|
end
|
263
239
|
rescue *NODE_DISCOVERY_ERRORS => ex
|
264
240
|
msg = <<-MSG.gsub(/\s+/, ' ')
|
@@ -276,7 +252,7 @@ module RedisFailover
|
|
276
252
|
|
277
253
|
# Seeds the initial node master from an existing znode config.
|
278
254
|
def find_existing_master
|
279
|
-
if data = @zk.get(
|
255
|
+
if data = @zk.get(redis_nodes_path).first
|
280
256
|
nodes = symbolize_keys(decode(data))
|
281
257
|
master = node_from(nodes[:master])
|
282
258
|
logger.info("Master from existing znode config: #{master || 'none'}")
|
@@ -305,10 +281,13 @@ module RedisFailover
|
|
305
281
|
|
306
282
|
# Spawns the {RedisFailover::NodeWatcher} instances for each managed node.
|
307
283
|
def spawn_watchers
|
308
|
-
@
|
309
|
-
|
284
|
+
@zk.delete(current_state_path, :ignore => :no_node)
|
285
|
+
@monitored_available, @monitored_unavailable = {}, []
|
286
|
+
@watchers = @nodes.map do |node|
|
287
|
+
NodeWatcher.new(self, node, @options.fetch(:max_failures, 3))
|
310
288
|
end
|
311
289
|
@watchers.each(&:watch)
|
290
|
+
logger.info("Monitoring redis nodes at #{stringify_nodes(@nodes)}")
|
312
291
|
end
|
313
292
|
|
314
293
|
# Searches for the master node.
|
@@ -376,39 +355,278 @@ module RedisFailover
|
|
376
355
|
}
|
377
356
|
end
|
378
357
|
|
358
|
+
# @return [Hash] the set of currently available/unavailable nodes as
|
359
|
+
# seen by this node manager instance
|
360
|
+
def node_availability_state
|
361
|
+
{
|
362
|
+
:available => Hash[@monitored_available.map { |k, v| [k.to_s, v] }],
|
363
|
+
:unavailable => @monitored_unavailable.map(&:to_s)
|
364
|
+
}
|
365
|
+
end
|
366
|
+
|
379
367
|
# Deletes the znode path containing the redis nodes.
|
380
|
-
|
381
|
-
|
382
|
-
|
368
|
+
#
|
369
|
+
# @param [String] path the znode path to delete
|
370
|
+
def delete_path(path)
|
371
|
+
@zk.delete(path)
|
372
|
+
logger.info("Deleted ZK node #{path}")
|
383
373
|
rescue ZK::Exceptions::NoNode => ex
|
384
374
|
logger.info("Tried to delete missing znode: #{ex.inspect}")
|
385
375
|
end
|
386
376
|
|
387
|
-
# Creates
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
377
|
+
# Creates a znode path.
|
378
|
+
#
|
379
|
+
# @param [String] path the znode path to create
|
380
|
+
# @param [Hash] options the options used to create the path
|
381
|
+
# @option options [String] :initial_value an initial value for the znode
|
382
|
+
# @option options [Boolean] :ephemeral true if node is ephemeral, false otherwise
|
383
|
+
def create_path(path, options = {})
|
384
|
+
unless @zk.exists?(path)
|
385
|
+
@zk.create(path,
|
386
|
+
options[:initial_value],
|
387
|
+
:ephemeral => options.fetch(:ephemeral, false))
|
388
|
+
logger.info("Created ZK node #{path}")
|
392
389
|
end
|
393
390
|
rescue ZK::Exceptions::NodeExists
|
394
391
|
# best effort
|
395
392
|
end
|
396
393
|
|
397
|
-
#
|
398
|
-
|
399
|
-
|
400
|
-
|
394
|
+
# Writes state to a particular znode path.
|
395
|
+
#
|
396
|
+
# @param [String] path the znode path that should be written to
|
397
|
+
# @param [String] value the value to write to the znode
|
398
|
+
# @param [Hash] options the default options to be used when creating the node
|
399
|
+
# @note the path will be created if it doesn't exist
|
400
|
+
def write_state(path, value, options = {})
|
401
|
+
create_path(path, options.merge(:initial_value => value))
|
402
|
+
@zk.set(path, value)
|
401
403
|
end
|
402
404
|
|
403
|
-
#
|
404
|
-
|
405
|
-
|
406
|
-
|
405
|
+
# Handles a manual failover znode update.
|
406
|
+
#
|
407
|
+
# @param [ZK::Event] event the ZK event to handle
|
408
|
+
def handle_manual_failover_update(event)
|
409
|
+
if event.node_created? || event.node_changed?
|
410
|
+
perform_manual_failover
|
411
|
+
end
|
412
|
+
rescue => ex
|
413
|
+
logger.error("Error scheduling a manual failover: #{ex.inspect}")
|
414
|
+
logger.error(ex.backtrace.join("\n"))
|
415
|
+
ensure
|
416
|
+
@zk.stat(manual_failover_path, :watch => true)
|
417
|
+
end
|
418
|
+
|
419
|
+
# Produces a FQDN id for this Node Manager.
|
420
|
+
#
|
421
|
+
# @return [String] the FQDN for this Node Manager
|
422
|
+
def manager_id
|
423
|
+
@manager_id ||= [
|
424
|
+
Socket.gethostbyname(Socket.gethostname)[0],
|
425
|
+
Process.pid
|
426
|
+
].join('-')
|
427
|
+
end
|
428
|
+
|
429
|
+
# Writes the current master list of redis nodes. This method is only invoked
|
430
|
+
# if this node manager instance is the master/primary manager.
|
431
|
+
def write_current_redis_nodes
|
432
|
+
write_state(redis_nodes_path, encode(current_nodes))
|
433
|
+
end
|
434
|
+
|
435
|
+
# Writes the current monitored list of redis nodes. This method is always
|
436
|
+
# invoked by all running node managers.
|
437
|
+
def write_current_monitored_state
|
438
|
+
write_state(current_state_path, encode(node_availability_state), :ephemeral => true)
|
439
|
+
end
|
440
|
+
|
441
|
+
# @return [String] root path for current node manager state
|
442
|
+
def current_state_root
|
443
|
+
"#{@root_znode}/manager_node_state"
|
444
|
+
end
|
445
|
+
|
446
|
+
# @return [String] the znode path for this node manager's view
|
447
|
+
# of available nodes
|
448
|
+
def current_state_path
|
449
|
+
"#{current_state_root}/#{manager_id}"
|
450
|
+
end
|
451
|
+
|
452
|
+
# @return [String] the znode path for the master redis nodes config
|
453
|
+
def redis_nodes_path
|
454
|
+
"#{@root_znode}/nodes"
|
455
|
+
end
|
456
|
+
|
457
|
+
# @return [String] the znode path used for performing manual failovers
|
458
|
+
def manual_failover_path
|
459
|
+
ManualFailover.path(@root_znode)
|
460
|
+
end
|
461
|
+
|
462
|
+
# @return [Boolean] true if this node manager is the master, false otherwise
|
463
|
+
def master_manager?
|
464
|
+
@master_manager
|
465
|
+
end
|
466
|
+
|
467
|
+
# Used to update the master node manager state. These states are only handled if
|
468
|
+
# this node manager instance is serving as the master manager.
|
469
|
+
#
|
470
|
+
# @param [Node] node the node to handle
|
471
|
+
# @param [Hash<Node, NodeSnapshot>] snapshots the current set of snapshots
|
472
|
+
def update_master_state(node, snapshots)
|
473
|
+
state = @node_strategy.determine_state(node, snapshots)
|
474
|
+
case state
|
475
|
+
when :unavailable
|
476
|
+
handle_unavailable(node, snapshots)
|
477
|
+
when :available
|
478
|
+
if node.syncing_with_master?
|
479
|
+
handle_syncing(node, snapshots)
|
480
|
+
else
|
481
|
+
handle_available(node, snapshots)
|
482
|
+
end
|
483
|
+
else
|
484
|
+
raise InvalidNodeStateError.new(node, state)
|
485
|
+
end
|
486
|
+
rescue *ZK_ERRORS
|
487
|
+
# fail hard if this is a ZK connection-related error
|
488
|
+
raise
|
489
|
+
rescue => ex
|
490
|
+
logger.error("Error handling state report for #{[node, state].inspect}: #{ex.inspect}")
|
491
|
+
end
|
492
|
+
|
493
|
+
# Updates the current view of the world for this particular node
|
494
|
+
# manager instance. All node managers write this state regardless
|
495
|
+
# of whether they are the master manager or not.
|
496
|
+
#
|
497
|
+
# @param [Node] node the node to handle
|
498
|
+
# @param [Symbol] state the node state
|
499
|
+
# @param [Integer] latency an optional latency
|
500
|
+
def update_current_state(node, state, latency = nil)
|
501
|
+
old_unavailable = @monitored_unavailable.dup
|
502
|
+
old_available = @monitored_available.dup
|
503
|
+
|
504
|
+
case state
|
505
|
+
when :unavailable
|
506
|
+
unless @monitored_unavailable.include?(node)
|
507
|
+
@monitored_unavailable << node
|
508
|
+
@monitored_available.delete(node)
|
509
|
+
write_current_monitored_state
|
510
|
+
end
|
511
|
+
when :available
|
512
|
+
last_latency = @monitored_available[node]
|
513
|
+
if last_latency.nil? || (latency - last_latency) > LATENCY_THRESHOLD
|
514
|
+
@monitored_available[node] = latency
|
515
|
+
@monitored_unavailable.delete(node)
|
516
|
+
write_current_monitored_state
|
517
|
+
end
|
518
|
+
else
|
519
|
+
raise InvalidNodeStateError.new(node, state)
|
520
|
+
end
|
521
|
+
rescue => ex
|
522
|
+
# if an error occurs, make sure that we rollback to the old state
|
523
|
+
@monitored_unavailable = old_unavailable
|
524
|
+
@monitored_available = old_available
|
525
|
+
raise
|
526
|
+
end
|
527
|
+
|
528
|
+
# Fetches each currently running node manager's view of the
|
529
|
+
# world in terms of which nodes they think are available/unavailable.
|
530
|
+
#
|
531
|
+
# @return [Hash<String, Array>] a hash of node manager to host states
|
532
|
+
def fetch_node_manager_states
|
533
|
+
states = {}
|
534
|
+
@zk.children(current_state_root).each do |child|
|
535
|
+
full_path = "#{current_state_root}/#{child}"
|
536
|
+
begin
|
537
|
+
states[child] = symbolize_keys(decode(@zk.get(full_path).first))
|
538
|
+
rescue ZK::Exceptions::NoNode
|
539
|
+
# ignore, this is an edge case that can happen when a node manager
|
540
|
+
# process dies while fetching its state
|
541
|
+
rescue => ex
|
542
|
+
logger.error("Failed to fetch states for #{full_path}: #{ex.inspect}")
|
543
|
+
end
|
544
|
+
end
|
545
|
+
states
|
546
|
+
end
|
547
|
+
|
548
|
+
# Builds current snapshots of nodes across all running node managers.
|
549
|
+
#
|
550
|
+
# @return [Hash<Node, NodeSnapshot>] the snapshots for all nodes
|
551
|
+
def current_node_snapshots
|
552
|
+
nodes = {}
|
553
|
+
snapshots = Hash.new { |h, k| h[k] = NodeSnapshot.new(k) }
|
554
|
+
fetch_node_manager_states.each do |node_manager, states|
|
555
|
+
available, unavailable = states.values_at(:available, :unavailable)
|
556
|
+
available.each do |node_string, latency|
|
557
|
+
node = nodes[node_string] ||= node_from(node_string)
|
558
|
+
snapshots[node].viewable_by(node_manager, latency)
|
559
|
+
end
|
560
|
+
unavailable.each do |node_string|
|
561
|
+
node = nodes[node_string] ||= node_from(node_string)
|
562
|
+
snapshots[node].unviewable_by(node_manager)
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
snapshots
|
567
|
+
end
|
568
|
+
|
569
|
+
# Waits until this node manager becomes the master.
|
570
|
+
def wait_until_master
|
571
|
+
logger.info('Waiting to become master Node Manager ...')
|
572
|
+
|
573
|
+
with_lock do
|
574
|
+
@master_manager = true
|
575
|
+
logger.info('Acquired master Node Manager lock.')
|
576
|
+
logger.info("Configured node strategy #{@node_strategy.class}")
|
577
|
+
logger.info("Configured failover strategy #{@failover_strategy.class}")
|
578
|
+
logger.info("Required Node Managers to make a decision: #{@required_node_managers}")
|
579
|
+
manage_nodes
|
580
|
+
end
|
581
|
+
end
|
582
|
+
|
583
|
+
# Manages the redis nodes by periodically processing snapshots.
|
584
|
+
def manage_nodes
|
585
|
+
# Re-discover nodes, since the state of the world may have been changed
|
586
|
+
# by the time we've become the primary node manager.
|
587
|
+
discover_nodes
|
588
|
+
|
589
|
+
# ensure that slaves are correctly pointing to this master
|
590
|
+
redirect_slaves_to(@master)
|
591
|
+
|
592
|
+
# Periodically update master config state.
|
593
|
+
while running? && master_manager?
|
594
|
+
@zk_lock.assert!
|
595
|
+
sleep(CHECK_INTERVAL)
|
596
|
+
|
597
|
+
@lock.synchronize do
|
598
|
+
snapshots = current_node_snapshots
|
599
|
+
if ensure_sufficient_node_managers(snapshots)
|
600
|
+
snapshots.each_key do |node|
|
601
|
+
update_master_state(node, snapshots)
|
602
|
+
end
|
603
|
+
|
604
|
+
# flush current master state
|
605
|
+
write_current_redis_nodes
|
606
|
+
|
607
|
+
# check if we've exhausted our attempts to promote a master
|
608
|
+
unless @master
|
609
|
+
@master_promotion_attempts += 1
|
610
|
+
raise NoMasterError if @master_promotion_attempts > MAX_PROMOTION_ATTEMPTS
|
611
|
+
end
|
612
|
+
end
|
613
|
+
end
|
614
|
+
end
|
615
|
+
end
|
616
|
+
|
617
|
+
# Creates a Node instance from a string.
|
618
|
+
#
|
619
|
+
# @param [String] node_string a string representation of a node (e.g., host:port)
|
620
|
+
# @return [Node] the Node representation
|
621
|
+
def node_from(node_string)
|
622
|
+
return if node_string.nil?
|
623
|
+
host, port = node_string.split(':', 2)
|
624
|
+
Node.new(:host => host, :port => port, :password => @options[:password])
|
407
625
|
end
|
408
626
|
|
409
627
|
# Executes a block wrapped in a ZK exclusive lock.
|
410
628
|
def with_lock
|
411
|
-
@zk_lock
|
629
|
+
@zk_lock ||= @zk.locker('master_redis_node_manager_lock')
|
412
630
|
|
413
631
|
begin
|
414
632
|
@zk_lock.lock!(true)
|
@@ -418,39 +636,96 @@ module RedisFailover
|
|
418
636
|
end
|
419
637
|
|
420
638
|
if running?
|
639
|
+
@zk_lock.assert!
|
421
640
|
yield
|
422
641
|
end
|
423
642
|
ensure
|
424
|
-
|
643
|
+
if @zk_lock
|
644
|
+
begin
|
645
|
+
@zk_lock.unlock!
|
646
|
+
rescue => ex
|
647
|
+
logger.warn("Failed to release lock: #{ex.inspect}")
|
648
|
+
end
|
649
|
+
end
|
425
650
|
end
|
426
651
|
|
427
652
|
# Perform a manual failover to a redis node.
|
428
653
|
def perform_manual_failover
|
429
|
-
@
|
430
|
-
return unless running? && @
|
654
|
+
@lock.synchronize do
|
655
|
+
return unless running? && @master_manager && @zk_lock
|
431
656
|
@zk_lock.assert!
|
432
|
-
new_master = @zk.get(
|
657
|
+
new_master = @zk.get(manual_failover_path, :watch => true).first
|
433
658
|
return unless new_master && new_master.size > 0
|
434
659
|
logger.info("Received manual failover request for: #{new_master}")
|
435
660
|
logger.info("Current nodes: #{current_nodes.inspect}")
|
436
|
-
|
437
|
-
|
661
|
+
snapshots = current_node_snapshots
|
662
|
+
|
663
|
+
node = if new_master == ManualFailover::ANY_SLAVE
|
664
|
+
failover_strategy_candidate(snapshots)
|
665
|
+
else
|
666
|
+
node_from(new_master)
|
667
|
+
end
|
668
|
+
|
438
669
|
if node
|
439
|
-
handle_manual_failover(node)
|
670
|
+
handle_manual_failover(node, snapshots)
|
440
671
|
else
|
441
672
|
logger.error('Failed to perform manual failover, no candidate found.')
|
442
673
|
end
|
443
674
|
end
|
444
675
|
rescue => ex
|
445
|
-
logger.error("Error handling
|
676
|
+
logger.error("Error handling manual failover: #{ex.inspect}")
|
446
677
|
logger.error(ex.backtrace.join("\n"))
|
447
678
|
ensure
|
448
|
-
@zk.stat(
|
679
|
+
@zk.stat(manual_failover_path, :watch => true)
|
449
680
|
end
|
450
681
|
|
451
682
|
# @return [Boolean] true if running, false otherwise
|
452
683
|
def running?
|
453
|
-
!@shutdown
|
684
|
+
@lock.synchronize { !@shutdown }
|
685
|
+
end
|
686
|
+
|
687
|
+
# @return [String] a stringified version of redis nodes
|
688
|
+
def stringify_nodes(nodes)
|
689
|
+
"(#{nodes.map(&:to_s).join(', ')})"
|
690
|
+
end
|
691
|
+
|
692
|
+
# Determines if each snapshot has a sufficient number of node managers.
|
693
|
+
#
|
694
|
+
# @param [Hash<Node, Snapshot>] snapshots the current snapshots
|
695
|
+
# @return [Boolean] true if sufficient, false otherwise
|
696
|
+
def ensure_sufficient_node_managers(snapshots)
|
697
|
+
currently_sufficient = true
|
698
|
+
snapshots.each do |node, snapshot|
|
699
|
+
node_managers = snapshot.node_managers
|
700
|
+
if node_managers.size < @required_node_managers
|
701
|
+
logger.error("Not enough Node Managers in snapshot for node #{node}. " +
|
702
|
+
"Required: #{@required_node_managers}, " +
|
703
|
+
"Available: #{node_managers.size} #{node_managers}")
|
704
|
+
currently_sufficient = false
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
if currently_sufficient && !@sufficient_node_managers
|
709
|
+
logger.info("Required Node Managers are visible: #{@required_node_managers}")
|
710
|
+
end
|
711
|
+
|
712
|
+
@sufficient_node_managers = currently_sufficient
|
713
|
+
@sufficient_node_managers
|
714
|
+
end
|
715
|
+
|
716
|
+
# Invokes the configured failover strategy.
|
717
|
+
#
|
718
|
+
# @param [Hash<Node, NodeSnapshot>] snapshots the node snapshots
|
719
|
+
# @return [Node] a failover candidate
|
720
|
+
def failover_strategy_candidate(snapshots)
|
721
|
+
# only include nodes that this master Node Manager can see
|
722
|
+
filtered_snapshots = snapshots.select do |node, snapshot|
|
723
|
+
snapshot.viewable_by?(manager_id)
|
724
|
+
end
|
725
|
+
|
726
|
+
logger.info('Attempting to find candidate from snapshots:')
|
727
|
+
logger.info("\n" + filtered_snapshots.values.join("\n"))
|
728
|
+
@failover_strategy.find_candidate(filtered_snapshots)
|
454
729
|
end
|
455
730
|
end
|
456
731
|
end
|