spbtv_redis_failover 1.0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.travis.yml +7 -0
- data/.yardopts +6 -0
- data/Changes.md +191 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +240 -0
- data/Rakefile +9 -0
- data/bin/redis_node_manager +7 -0
- data/examples/config.yml +17 -0
- data/examples/multiple_environments_config.yml +15 -0
- data/lib/redis_failover.rb +25 -0
- data/lib/redis_failover/cli.rb +142 -0
- data/lib/redis_failover/client.rb +517 -0
- data/lib/redis_failover/errors.rb +54 -0
- 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 +52 -0
- data/lib/redis_failover/node.rb +190 -0
- data/lib/redis_failover/node_manager.rb +741 -0
- 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 +83 -0
- data/lib/redis_failover/runner.rb +27 -0
- data/lib/redis_failover/util.rb +137 -0
- data/lib/redis_failover/version.rb +3 -0
- data/misc/redis_failover.png +0 -0
- data/spbtv_redis_failover.gemspec +26 -0
- data/spec/cli_spec.rb +75 -0
- data/spec/client_spec.rb +153 -0
- data/spec/failover_strategy/latency_spec.rb +41 -0
- data/spec/failover_strategy_spec.rb +17 -0
- data/spec/node_manager_spec.rb +136 -0
- data/spec/node_snapshot_spec.rb +30 -0
- data/spec/node_spec.rb +84 -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 +58 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/config/multiple_environments.yml +15 -0
- data/spec/support/config/multiple_environments_with_chroot.yml +17 -0
- data/spec/support/config/single_environment.yml +7 -0
- data/spec/support/config/single_environment_with_chroot.yml +8 -0
- data/spec/support/node_manager_stub.rb +87 -0
- data/spec/support/redis_stub.rb +105 -0
- data/spec/util_spec.rb +21 -0
- metadata +207 -0
data/spec/cli_spec.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module RedisFailover
|
4
|
+
describe CLI do
|
5
|
+
describe '.parse' do
|
6
|
+
it 'properly parses redis nodes' do
|
7
|
+
opts = CLI.parse(['-n host1:1,host2:2,host3:3', '-z localhost:1111'])
|
8
|
+
opts[:nodes].should == [
|
9
|
+
{:host => 'host1', :port => '1'},
|
10
|
+
{:host => 'host2', :port => '2'},
|
11
|
+
{:host => 'host3', :port => '3'}
|
12
|
+
]
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'properly parses ZooKeeper servers' do
|
16
|
+
opts = CLI.parse(['-n host1:1,host2:2,host3:3', '-z localhost:1111'])
|
17
|
+
opts[:zkservers].should == 'localhost:1111'
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'properly parses a redis password' do
|
21
|
+
opts = CLI.parse(['-n host:port', '-z localhost:1111', '-p redis_pass'])
|
22
|
+
opts[:nodes].should == [{
|
23
|
+
:host => 'host',
|
24
|
+
:port => 'port',
|
25
|
+
:password => 'redis_pass'
|
26
|
+
}]
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'properly parses max node failures' do
|
30
|
+
opts = CLI.parse([
|
31
|
+
'-n host:port',
|
32
|
+
'-z localhost:1111',
|
33
|
+
'-p redis_pass',
|
34
|
+
'--max-failures',
|
35
|
+
'1'])
|
36
|
+
opts[:max_failures].should == 1
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'properly parses a chroot' do
|
40
|
+
opts = CLI.parse(['-n host:port', '-z localhost:1111', '--with-chroot', '/with/chroot/from/command/line'])
|
41
|
+
opts[:nodes].should == [{
|
42
|
+
:host => 'host',
|
43
|
+
:port => 'port',
|
44
|
+
}]
|
45
|
+
|
46
|
+
opts[:chroot].should == '/with/chroot/from/command/line'
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'properly parses the config file' do
|
50
|
+
opts = CLI.parse(['-C', "#{File.dirname(__FILE__)}/support/config/single_environment.yml"])
|
51
|
+
opts[:zkservers].should == 'zk01:2181,zk02:2181,zk03:2181'
|
52
|
+
|
53
|
+
opts = CLI.parse(['-C', "#{File.dirname(__FILE__)}/support/config/multiple_environments.yml", '-E', 'development'])
|
54
|
+
opts[:zkservers].should == 'localhost:2181'
|
55
|
+
|
56
|
+
opts = CLI.parse(['-C', "#{File.dirname(__FILE__)}/support/config/multiple_environments.yml", '-E', 'staging'])
|
57
|
+
opts[:zkservers].should == 'zk01:2181,zk02:2181,zk03:2181'
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'properly parses the config file that include chroot' do
|
61
|
+
opts = CLI.parse(['-C', "#{File.dirname(__FILE__)}/support/config/single_environment_with_chroot.yml"])
|
62
|
+
opts[:zkservers].should == 'zk01:2181,zk02:2181,zk03:2181'
|
63
|
+
opts[:chroot].should == '/with/chroot'
|
64
|
+
|
65
|
+
opts = CLI.parse(['-C', "#{File.dirname(__FILE__)}/support/config/multiple_environments_with_chroot.yml", '-E', 'development'])
|
66
|
+
opts[:zkservers].should == 'localhost:2181'
|
67
|
+
opts[:chroot].should == '/with/chroot_development'
|
68
|
+
|
69
|
+
opts = CLI.parse(['-C', "#{File.dirname(__FILE__)}/support/config/multiple_environments_with_chroot.yml", '-E', 'staging'])
|
70
|
+
opts[:zkservers].should == 'zk01:2181,zk02:2181,zk03:2181'
|
71
|
+
opts[:chroot].should == '/with/chroot_staging'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module RedisFailover
|
4
|
+
Client::Redis = RedisStub
|
5
|
+
Client::Redis::Client = Redis::Client
|
6
|
+
class ClientStub < Client
|
7
|
+
def current_master
|
8
|
+
@master
|
9
|
+
end
|
10
|
+
|
11
|
+
def current_slaves
|
12
|
+
@slaves
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch_nodes
|
16
|
+
{
|
17
|
+
:master => 'localhost:6379',
|
18
|
+
:slaves => ['localhost:1111'],
|
19
|
+
:unavailable => []
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def setup_zk
|
24
|
+
@zk = NullObject.new
|
25
|
+
update_znode_timestamp
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe Client do
|
30
|
+
let(:client) { ClientStub.new(:zkservers => 'localhost:9281', :safe_mode => true) }
|
31
|
+
|
32
|
+
describe '#build_clients' do
|
33
|
+
it 'properly parses master' do
|
34
|
+
client.current_master.to_s.should == 'localhost:6379'
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'properly parses slaves' do
|
38
|
+
client.current_slaves.first.to_s.should == 'localhost:1111'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#client' do
|
43
|
+
it 'should return itself as a delegate' do
|
44
|
+
client.client.should == client
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#dispatch' do
|
49
|
+
it 'routes write operations to master' do
|
50
|
+
called = false
|
51
|
+
client.current_master.define_singleton_method(:del) do |*args|
|
52
|
+
called = true
|
53
|
+
end
|
54
|
+
client.del('foo')
|
55
|
+
called.should be_true
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#inspect' do
|
59
|
+
it 'should always include db' do
|
60
|
+
opts = {:zkservers => 'localhost:1234'}
|
61
|
+
client = ClientStub.new(opts)
|
62
|
+
client.inspect.should match('<RedisFailover::Client \(db: 0,')
|
63
|
+
db = '5'
|
64
|
+
opts.merge!(:db => db)
|
65
|
+
client = ClientStub.new(opts)
|
66
|
+
client.inspect.should match("<RedisFailover::Client \\(db: #{db},")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#call' do
|
71
|
+
it 'should dispatch :call messages to correct method' do
|
72
|
+
client.should_receive(:dispatch).with(:foo, *['key'])
|
73
|
+
client.call([:foo, 'key'])
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'with :master_only false' do
|
78
|
+
it 'routes read operations to a slave' do
|
79
|
+
called = false
|
80
|
+
client.current_slaves.first.change_role_to('slave')
|
81
|
+
client.current_slaves.first.define_singleton_method(:get) do |*args|
|
82
|
+
called = true
|
83
|
+
end
|
84
|
+
client.get('foo')
|
85
|
+
called.should be_true
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'with :master_only true' do
|
90
|
+
it 'routes read operations to master' do
|
91
|
+
client = ClientStub.new(:zkservers => 'localhost:9281', :master_only => true)
|
92
|
+
called = false
|
93
|
+
client.current_master.define_singleton_method(:get) do |*args|
|
94
|
+
called = true
|
95
|
+
end
|
96
|
+
client.get('foo')
|
97
|
+
called.should be_true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'reconnects when node is unavailable' do
|
102
|
+
class << client
|
103
|
+
attr_reader :reconnected
|
104
|
+
def build_clients
|
105
|
+
@reconnected = true
|
106
|
+
super
|
107
|
+
end
|
108
|
+
|
109
|
+
def fetch_nodes
|
110
|
+
@calls ||= 0
|
111
|
+
{
|
112
|
+
:master => "localhost:222#{@calls += 1}",
|
113
|
+
:slaves => ['localhost:1111'],
|
114
|
+
:unavailable => []
|
115
|
+
}
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
client.current_master.make_unavailable!
|
120
|
+
client.del('foo')
|
121
|
+
client.reconnected.should be_true
|
122
|
+
end
|
123
|
+
|
124
|
+
context 'with :verify_role true' do
|
125
|
+
it 'properly detects when a node has changed roles' do
|
126
|
+
client.current_master.change_role_to('slave')
|
127
|
+
expect { client.send(:master) }.to raise_error(InvalidNodeRoleError)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'raises error for unsupported operations' do
|
132
|
+
expect { client.select }.to raise_error(UnsupportedOperationError)
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'with :safe_mode enabled' do
|
136
|
+
it 'rebuilds clients when no communication from Node Manager within certain time window' do
|
137
|
+
client.instance_variable_set(:@last_znode_timestamp, Time.at(0))
|
138
|
+
client.should_receive(:build_clients)
|
139
|
+
client.del('foo')
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
context 'with :safe_mode disabled' do
|
144
|
+
it 'does not rebuild clients when no communication from Node Manager within certain time window' do
|
145
|
+
client = ClientStub.new(:zkservers => 'localhost:9281', :safe_mode => false)
|
146
|
+
client.instance_variable_set(:@last_znode_timestamp, Time.at(0))
|
147
|
+
client.should_not_receive(:build_clients)
|
148
|
+
client.del('foo')
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -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,136 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module RedisFailover
|
4
|
+
describe NodeManager do
|
5
|
+
let(:manager) { NodeManagerStub.new({}) }
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
manager.discover_nodes
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#nodes' do
|
12
|
+
it 'returns current master and slave nodes' do
|
13
|
+
manager.current_nodes.should == {
|
14
|
+
:master => 'master:6379',
|
15
|
+
:slaves => ['slave:6379'],
|
16
|
+
:unavailable => []
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#handle_unavailable' do
|
22
|
+
context 'slave dies' do
|
23
|
+
it 'moves slave to unavailable list' do
|
24
|
+
slave = manager.slaves.first
|
25
|
+
manager.force_unavailable(slave)
|
26
|
+
manager.current_nodes[:unavailable].should include(slave.to_s)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'master dies' do
|
31
|
+
before(:each) do
|
32
|
+
@slave = manager.slaves.first
|
33
|
+
@master = manager.master
|
34
|
+
manager.force_unavailable(@master)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'promotes slave to master' do
|
38
|
+
manager.master.should == @slave
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'moves master to unavailable list' do
|
42
|
+
manager.current_nodes[:unavailable].should include(@master.to_s)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#handle_available' do
|
48
|
+
before(:each) do
|
49
|
+
# force to be unavailable first
|
50
|
+
@slave = manager.slaves.first
|
51
|
+
manager.force_unavailable(@slave)
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'slave node with a master present' do
|
55
|
+
it 'removes slave from unavailable list' do
|
56
|
+
manager.force_available(@slave)
|
57
|
+
manager.current_nodes[:unavailable].should be_empty
|
58
|
+
manager.current_nodes[:slaves].should include(@slave.to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'makes node a slave of new master' do
|
62
|
+
manager.master = Node.new(:host => 'foo', :port => '7892')
|
63
|
+
manager.force_available(@slave)
|
64
|
+
@slave.fetch_info.should == {
|
65
|
+
:role => 'slave',
|
66
|
+
:master_host => 'foo',
|
67
|
+
:master_port => '7892'}
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'does not invoke slaveof operation if master has not changed' do
|
71
|
+
@slave.redis.should_not_receive(:slaveof)
|
72
|
+
manager.force_available(@slave)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'slave node with no master present' do
|
77
|
+
before(:each) do
|
78
|
+
@master = manager.master
|
79
|
+
manager.force_unavailable(@master)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'promotes slave to master' do
|
83
|
+
manager.force_available(@slave)
|
84
|
+
manager.master.should == @slave
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'slaves list remains empty' do
|
88
|
+
manager.current_nodes[:slaves].should be_empty
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe '#handle_syncing' do
|
94
|
+
context 'prohibits stale reads' do
|
95
|
+
it 'adds node to unavailable list' do
|
96
|
+
slave = manager.slaves.first
|
97
|
+
manager.force_syncing(slave, false)
|
98
|
+
manager.current_nodes[:unavailable].should include(slave.to_s)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'allows stale reads' do
|
103
|
+
it 'makes node available' do
|
104
|
+
slave = manager.slaves.first
|
105
|
+
manager.force_syncing(slave, true)
|
106
|
+
manager.current_nodes[:unavailable].should_not include(slave.to_s)
|
107
|
+
manager.current_nodes[:slaves].should include(slave.to_s)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe '#guess_master' do
|
113
|
+
let(:node1) { Node.new(:host => 'node1').extend(RedisStubSupport) }
|
114
|
+
let(:node2) { Node.new(:host => 'node2').extend(RedisStubSupport) }
|
115
|
+
let(:node3) { Node.new(:host => 'node3').extend(RedisStubSupport) }
|
116
|
+
|
117
|
+
it 'raises error when no master is found' do
|
118
|
+
node1.make_slave!(node3)
|
119
|
+
node2.make_slave!(node3)
|
120
|
+
expect { manager.guess_master([node1, node2]) }.to raise_error(NoMasterError)
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'raises error when multiple masters found' do
|
124
|
+
node1.make_master!
|
125
|
+
node2.make_master!
|
126
|
+
expect { manager.guess_master([node1, node2]) }.to raise_error(MultipleMastersError)
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'raises error when a node can not be reached' do
|
130
|
+
node1.make_master!
|
131
|
+
node2.redis.make_unavailable!
|
132
|
+
expect { manager.guess_master([node1, node2]) }.to raise_error(NodeUnavailableError)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
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
|