spbtv_redis_failover 1.0.2.1
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.
- 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
|