redis_failover 0.9.7.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.fetch(:host) { raise InvalidNodeError, 'missing host'}
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 via an asynchronous queue. The NodeManager processes the
7
- # state reports and reacts appropriately by handling stale/dead nodes,
8
- # and promoting a new redis master if it sees fit to do so.
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 = 3
14
-
15
- # ZK Errors that the Node Manager cares about.
16
- ZK_ERRORS = [
17
- ZK::Exceptions::LockAssertionFailedError,
18
- ZK::Exceptions::InterruptedSession,
19
- ZKDisconnectedError
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
- @znode = @options[:znode_path] || Util::DEFAULT_ZNODE_PATH
42
- @manual_znode = ManualFailover::ZNODE_PATH
43
- @mutex = Mutex.new
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
- logger.info('Waiting to become master Node Manager ...')
60
- with_lock do
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
- def notify_state(node, state)
81
- @queue << [node, state]
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
- @leader = false
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
- @mutex.synchronize do
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
- @zk.close! if @zk
109
- @zk = ZK.new("#{@options[:zkservers]}#{@options[:chroot] || ''}")
110
- @zk.on_expired_session { notify_state(:zk_disconnected, nil) }
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
- @zk.on_connected { @zk.stat(@manual_znode, :watch => true) }
119
- @zk.stat(@manual_znode, :watch => true)
120
- end
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
- def handle_unavailable(node)
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
- def handle_available(node)
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
- def handle_syncing(node)
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
- return
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
- def handle_manual_failover(node)
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
- # @note if no node is specified, a random slave will be used
225
- def promote_new_master(node = nil)
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 slave the new master
230
- candidate = node || @slaves.pop
231
- unless candidate
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
- return
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
- @mutex.synchronize do
249
- return false unless running?
250
- nodes = @options[:nodes].map { |opts| Node.new(opts) }.uniq
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(@znode).first
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
- @watchers = [@master, @slaves, @unavailable].flatten.compact.map do |node|
309
- NodeWatcher.new(self, node, @options[:max_failures] || 3)
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
- def delete_path
381
- @zk.delete(@znode)
382
- logger.info("Deleted ZooKeeper node #{@znode}")
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 the znode path containing the redis nodes.
388
- def create_path
389
- unless @zk.exists?(@znode)
390
- @zk.create(@znode, encode(current_nodes))
391
- logger.info("Created ZooKeeper node #{@znode}")
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
- # Initializes the znode path containing the redis nodes.
398
- def initialize_path
399
- create_path
400
- write_state
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
- # Writes the current redis nodes state to the znode path.
404
- def write_state
405
- create_path
406
- @zk.set(@znode, encode(current_nodes))
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 = @zk.locker(@lock_path)
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
- @zk_lock.unlock! if @zk_lock
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
- @mutex.synchronize do
430
- return unless running? && @leader && @zk_lock
654
+ @lock.synchronize do
655
+ return unless running? && @master_manager && @zk_lock
431
656
  @zk_lock.assert!
432
- new_master = @zk.get(@manual_znode, :watch => true).first
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
- node = new_master == ManualFailover::ANY_SLAVE ?
437
- @slaves.shuffle.first : node_from(new_master)
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 a manual failover: #{ex.inspect}")
676
+ logger.error("Error handling manual failover: #{ex.inspect}")
446
677
  logger.error(ex.backtrace.join("\n"))
447
678
  ensure
448
- @zk.stat(@manual_znode, :watch => true)
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