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