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.
@@ -0,0 +1,81 @@
1
+ module RedisFailover
2
+ # Represents a snapshot of a particular redis node as seen by all currently running
3
+ # redis node managers.
4
+ class NodeSnapshot
5
+ # @return [String] the redis node
6
+ attr_reader :node
7
+
8
+ # Creates a new instance.
9
+ #
10
+ # @param [String] the redis node
11
+ # @see NodeManager#initialize
12
+ def initialize(node)
13
+ @node = node
14
+ @available = {}
15
+ @unavailable = []
16
+ end
17
+
18
+ # Declares this node available by the specified node manager.
19
+ #
20
+ # @param [String] node_manager the node manager id
21
+ # @param [Integer] latency the latency
22
+ def viewable_by(node_manager, latency)
23
+ @available[node_manager] = latency
24
+ end
25
+
26
+ # Determines if this node is viewable by a node manager.
27
+ #
28
+ # @param [String] node_manager the node manager id
29
+ def viewable_by?(node_manager)
30
+ @available.key?(node_manager)
31
+ end
32
+
33
+ # Declares this node unavailable by the specified node manager.
34
+ #
35
+ # @param [String] node_manager the node manager id
36
+ def unviewable_by(node_manager)
37
+ @unavailable << node_manager
38
+ end
39
+
40
+ # @return [Integer] the number of node managers saying
41
+ # this node is available
42
+ def available_count
43
+ @available.size
44
+ end
45
+
46
+ # @return [Integer] the number of node managers saying
47
+ # this node is unavailable
48
+ def unavailable_count
49
+ @unavailable.size
50
+ end
51
+
52
+ # @return [Integer] the average available latency
53
+ def avg_latency
54
+ return if @available.empty?
55
+ @available.values.inject(0) { |sum, n| sum + n } / @available.size
56
+ end
57
+
58
+ # @return [Array<String>] all node managers involved in this snapshot
59
+ def node_managers
60
+ (@available.keys + @unavailable).uniq
61
+ end
62
+
63
+ # @return [Boolean] true if all node managers indicated that this
64
+ # node was viewable
65
+ def all_available?
66
+ available_count > 0 && unavailable_count == 0
67
+ end
68
+
69
+ # @return [Boolean] true if all node managers indicated that this
70
+ # node was unviewable
71
+ def all_unavailable?
72
+ unavailable_count > 0 && available_count == 0
73
+ end
74
+
75
+ # @return [String] a friendly representation of this node snapshot
76
+ def to_s
77
+ 'Node %s available by %p, unavailable by %p (%d up, %d down)' %
78
+ [node, @available, @unavailable, available_count, unavailable_count]
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,34 @@
1
+ module RedisFailover
2
+ # Base class for strategies that determine node availability.
3
+ class NodeStrategy
4
+ include Util
5
+
6
+ # Loads a strategy based on the given name.
7
+ #
8
+ # @param [String, Symbol] name the strategy name
9
+ # @return [Object] a new strategy instance
10
+ def self.for(name)
11
+ require "redis_failover/node_strategy/#{name.downcase}"
12
+ const_get(name.capitalize).new
13
+ rescue LoadError, NameError
14
+ raise "Failed to find node strategy: #{name}"
15
+ end
16
+
17
+ # Returns the state determined by this strategy.
18
+ #
19
+ # @param [Node] the node to handle
20
+ # @param [Hash<Node, NodeSnapshot>] snapshots the current set of snapshots
21
+ # @return [Symbol] the status
22
+ def determine_state(node, snapshots)
23
+ raise NotImplementedError
24
+ end
25
+
26
+ # Logs a node as being unavailable.
27
+ #
28
+ # @param [Node] node the node
29
+ # @param [NodeSnapshot] snapshot the node snapshot
30
+ def log_unavailable(node, snapshot)
31
+ logger.info("#{self.class} marking #{node} as unavailable. Snapshot: #{snapshot}")
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ module RedisFailover
2
+ class NodeStrategy
3
+ # Consensus strategy only marks the node as unavailable if all members of the
4
+ # snapshot indicate that the node is down.
5
+ class Consensus < NodeStrategy
6
+ # @see RedisFailover::NodeStrategy#determine_state
7
+ def determine_state(node, snapshots)
8
+ snapshot = snapshots[node]
9
+ if snapshot.all_unavailable?
10
+ log_unavailable(node, snapshot)
11
+ :unavailable
12
+ else
13
+ :available
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module RedisFailover
2
+ class NodeStrategy
3
+ # Majority strategy only marks the node as unavailable if a majority of the
4
+ # snapshot indicates that the node is down.
5
+ class Majority < NodeStrategy
6
+ # @see RedisFailover::NodeStrategy#determine_state
7
+ def determine_state(node, snapshots)
8
+ snapshot = snapshots[node]
9
+ if snapshot.unavailable_count > snapshot.available_count
10
+ log_unavailable(node, snapshot)
11
+ :unavailable
12
+ else
13
+ :available
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ module RedisFailover
2
+ class NodeStrategy
3
+ # Marks the node as unavailable if any node manager reports the node as down.
4
+ class Single < NodeStrategy
5
+ # @see RedisFailover::NodeStrategy#determine_state
6
+ def determine_state(node, snapshots)
7
+ snapshot = snapshots[node]
8
+ if snapshot.unavailable_count > 0
9
+ log_unavailable(node, snapshot)
10
+ :unavailable
11
+ else
12
+ :available
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -33,8 +33,12 @@ module RedisFailover
33
33
  # Performs a graceful shutdown of this watcher.
34
34
  def shutdown
35
35
  @done = true
36
- @node.wakeup
37
- @monitor_thread.join if @monitor_thread
36
+ begin
37
+ @node.wakeup
38
+ rescue
39
+ # best effort
40
+ end
41
+ @monitor_thread.join
38
42
  rescue => ex
39
43
  logger.warn("Failed to gracefully shutdown watcher for #{@node}")
40
44
  end
@@ -48,17 +52,12 @@ module RedisFailover
48
52
 
49
53
  loop do
50
54
  begin
51
- return if @done
55
+ break if @done
52
56
  sleep(WATCHER_SLEEP_TIME)
53
- @node.ping
57
+ latency = Benchmark.realtime { @node.ping }
54
58
  failures = 0
55
-
56
- if @node.syncing_with_master?
57
- notify(:syncing)
58
- else
59
- notify(:available)
60
- @node.wait
61
- end
59
+ notify(:available, latency)
60
+ @node.wait
62
61
  rescue NodeUnavailableError => ex
63
62
  logger.debug("Failed to communicate with node #{@node}: #{ex.inspect}")
64
63
  failures += 1
@@ -76,8 +75,9 @@ module RedisFailover
76
75
  # Notifies the manager of a node's state.
77
76
  #
78
77
  # @param [Symbol] state the node's state
79
- def notify(state)
80
- @manager.notify_state(@node, state)
78
+ # @param [Integer] latency an optional latency
79
+ def notify(state, latency = nil)
80
+ @manager.notify_state(@node, state, latency)
81
81
  end
82
82
  end
83
83
  end
@@ -51,8 +51,8 @@ module RedisFailover
51
51
  # that abstracts the master/slave servers.
52
52
  UNSUPPORTED_OPS = Set[:select, :dbsize].freeze
53
53
 
54
- # Default node in ZK that contains the current list of available redis nodes.
55
- DEFAULT_ZNODE_PATH = '/redis_failover_nodes'.freeze
54
+ # Default root node in ZK used for redis_failover.
55
+ DEFAULT_ROOT_ZNODE_PATH = '/redis_failover'.freeze
56
56
 
57
57
  # Connectivity errors that the redis (<3.x) client raises.
58
58
  REDIS_ERRORS = Errno.constants.map { |c| Errno.const_get(c) }
@@ -61,11 +61,19 @@ module RedisFailover
61
61
  REDIS_ERRORS << Redis::BaseError if Redis.const_defined?('BaseError')
62
62
  REDIS_ERRORS.freeze
63
63
 
64
+ # ZK Errors
65
+ ZK_ERRORS = [
66
+ ZK::Exceptions::LockAssertionFailedError,
67
+ ZK::Exceptions::InterruptedSession,
68
+ ZK::Exceptions::Retryable,
69
+ Zookeeper::Exceptions::ContinuationTimeoutError
70
+ ].freeze
71
+
64
72
  # Full set of errors related to connectivity.
65
73
  CONNECTIVITY_ERRORS = [
66
74
  RedisFailover::Error,
67
- ZK::Exceptions::InterruptedSession,
68
- REDIS_ERRORS
75
+ REDIS_ERRORS,
76
+ ZK_ERRORS
69
77
  ].flatten.freeze
70
78
 
71
79
  # Symbolizes the keys of the specified hash.
@@ -1,3 +1,3 @@
1
1
  module RedisFailover
2
- VERSION = '0.9.7.2'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -18,7 +18,7 @@ Gem::Specification.new do |gem|
18
18
  gem.add_dependency('redis', ['>= 2.2', '< 4'])
19
19
  gem.add_dependency('redis-namespace')
20
20
  gem.add_dependency('multi_json', '~> 1')
21
- gem.add_dependency('zk', '~> 1.6')
21
+ gem.add_dependency('zk', ['>= 1.7.2', '< 1.8'])
22
22
 
23
23
  gem.add_development_dependency('rake')
24
24
  gem.add_development_dependency('rspec')
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ module RedisFailover
4
+ class FailoverStrategy
5
+ FailoverStrategy.for(:latency)
6
+
7
+ describe Latency do
8
+ describe '#find_candidate' do
9
+ it 'returns only candidates seen by all node managers' do
10
+ strategy = FailoverStrategy.for(:latency)
11
+ snapshot_1 = NodeSnapshot.new(Node.new(:host => 'localhost', :port => '123'))
12
+ snapshot_1.viewable_by('nm1', 0)
13
+ snapshot_1.unviewable_by('nm2')
14
+
15
+ snapshot_2 = NodeSnapshot.new(Node.new(:host => 'localhost', :port => '456'))
16
+ snapshot_2.viewable_by('nm2', 0)
17
+ snapshot_2.unviewable_by('nm1')
18
+
19
+ snapshots = {snapshot_1.node => snapshot_1, snapshot_2.node => snapshot_2}
20
+ strategy.find_candidate(snapshots).should be_nil
21
+ end
22
+
23
+ it 'returns the candidate with the lowest average latency' do
24
+ strategy = FailoverStrategy.for(:latency)
25
+ snapshot_1 = NodeSnapshot.new(Node.new(:host => 'localhost', :port => '123'))
26
+ snapshot_1.viewable_by('nm1', 5)
27
+ snapshot_1.viewable_by('nm2', 4)
28
+ snapshot_1.viewable_by('nm3', 3)
29
+
30
+ snapshot_2 = NodeSnapshot.new(Node.new(:host => 'localhost', :port => '456'))
31
+ snapshot_2.viewable_by('nm1', 1)
32
+ snapshot_2.viewable_by('nm2', 1)
33
+ snapshot_2.viewable_by('nm3', 2)
34
+
35
+ snapshots = {snapshot_1.node => snapshot_1, snapshot_2.node => snapshot_2}
36
+ strategy.find_candidate(snapshots).should == snapshot_2.node
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ module RedisFailover
4
+ describe FailoverStrategy do
5
+
6
+ describe '.for' do
7
+ it 'creates a new latency strategy instance' do
8
+ s = FailoverStrategy.for('latency')
9
+ s.should be_a RedisFailover::FailoverStrategy::Latency
10
+ end
11
+
12
+ it 'rejects unknown strategies' do
13
+ expect { FailoverStrategy.for('foobar') }.to raise_error(RuntimeError)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ module RedisFailover
4
+ describe NodeSnapshot do
5
+ let(:snapshot) { NodeSnapshot.new(Node.new(:host => 'localhost', :port => '123')) }
6
+
7
+ describe '#initialize' do
8
+ it 'creates a new empty snapshot' do
9
+ snapshot.available_count.should == 0
10
+ snapshot.unavailable_count.should == 0
11
+ end
12
+ end
13
+
14
+ describe '#viewable_by' do
15
+ it 'updates the availability count' do
16
+ snapshot.viewable_by('nm1', 0)
17
+ snapshot.viewable_by('nm2', 0)
18
+ snapshot.available_count.should == 2
19
+ end
20
+ end
21
+
22
+ describe '#unviewable_by' do
23
+ it 'updates the unavailability count' do
24
+ snapshot.unviewable_by('nm1')
25
+ snapshot.unviewable_by('nm2')
26
+ snapshot.unavailable_count.should == 2
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ module RedisFailover
4
+ class NodeStrategy
5
+ NodeStrategy.for(:consensus)
6
+
7
+ describe Consensus do
8
+ let(:node) { Node.new(:host => 'localhost', :port => '123') }
9
+ let(:snapshot) { NodeSnapshot.new(node) }
10
+
11
+ describe '#determine_state' do
12
+ it 'returns the unavailable state if unavailable by all node managers' do
13
+ strategy = NodeStrategy.for(:consensus)
14
+ snapshot.unviewable_by('nm1')
15
+ snapshot.unviewable_by('nm2')
16
+ snapshot.unviewable_by('nm3')
17
+ strategy.determine_state(node, node => snapshot).should == :unavailable
18
+ end
19
+
20
+ it 'returns the available state if unavailable by some node managers' do
21
+ strategy = NodeStrategy.for(:consensus)
22
+ snapshot.unviewable_by('nm1')
23
+ snapshot.unviewable_by('nm2')
24
+ snapshot.viewable_by('nm3', 0)
25
+ strategy.determine_state(node, node => snapshot).should == :available
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ module RedisFailover
4
+ class NodeStrategy
5
+ NodeStrategy.for(:majority)
6
+
7
+ describe Majority do
8
+ let(:node) { Node.new(:host => 'localhost', :port => '123') }
9
+ let(:snapshot) { NodeSnapshot.new(node) }
10
+
11
+ describe '#determine_state' do
12
+ it 'returns the unavailable state if unavailable by the majority of node managers' do
13
+ strategy = NodeStrategy.for(:majority)
14
+ snapshot.viewable_by('nm1', 0)
15
+ snapshot.unviewable_by('nm2')
16
+ snapshot.unviewable_by('nm3')
17
+ strategy.determine_state(node, node => snapshot).should == :unavailable
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ module RedisFailover
4
+ class NodeStrategy
5
+ NodeStrategy.for(:single)
6
+
7
+ describe Single do
8
+ let(:node) { Node.new(:host => 'localhost', :port => '123') }
9
+ let(:snapshot) { NodeSnapshot.new(node) }
10
+
11
+ describe '#determine_state' do
12
+ it 'returns the unavailable state if any node manager reports as down' do
13
+ strategy = NodeStrategy.for(:single)
14
+ snapshot.unviewable_by('nm1')
15
+ snapshot.viewable_by('nm2', 0)
16
+ snapshot.viewable_by('nm3', 0)
17
+ strategy.determine_state(node, node => snapshot).should == :unavailable
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ module RedisFailover
4
+ describe NodeStrategy do
5
+
6
+ describe '.for' do
7
+ it 'creates a new majority strategy instance' do
8
+ s = NodeStrategy.for('majority')
9
+ s.should be_a RedisFailover::NodeStrategy::Majority
10
+ end
11
+
12
+ it 'creates a new consensus strategy instance' do
13
+ s = NodeStrategy.for('consensus')
14
+ s.should be_a RedisFailover::NodeStrategy::Consensus
15
+ end
16
+
17
+ it 'rejects unknown strategies' do
18
+ expect { NodeStrategy.for('foobar') }.to raise_error(RuntimeError)
19
+ end
20
+ end
21
+ end
22
+ end