spbtv_redis_failover 1.0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.travis.yml +7 -0
  4. data/.yardopts +6 -0
  5. data/Changes.md +191 -0
  6. data/Gemfile +2 -0
  7. data/LICENSE +22 -0
  8. data/README.md +240 -0
  9. data/Rakefile +9 -0
  10. data/bin/redis_node_manager +7 -0
  11. data/examples/config.yml +17 -0
  12. data/examples/multiple_environments_config.yml +15 -0
  13. data/lib/redis_failover.rb +25 -0
  14. data/lib/redis_failover/cli.rb +142 -0
  15. data/lib/redis_failover/client.rb +517 -0
  16. data/lib/redis_failover/errors.rb +54 -0
  17. data/lib/redis_failover/failover_strategy.rb +25 -0
  18. data/lib/redis_failover/failover_strategy/latency.rb +21 -0
  19. data/lib/redis_failover/manual_failover.rb +52 -0
  20. data/lib/redis_failover/node.rb +190 -0
  21. data/lib/redis_failover/node_manager.rb +741 -0
  22. data/lib/redis_failover/node_snapshot.rb +81 -0
  23. data/lib/redis_failover/node_strategy.rb +34 -0
  24. data/lib/redis_failover/node_strategy/consensus.rb +18 -0
  25. data/lib/redis_failover/node_strategy/majority.rb +18 -0
  26. data/lib/redis_failover/node_strategy/single.rb +17 -0
  27. data/lib/redis_failover/node_watcher.rb +83 -0
  28. data/lib/redis_failover/runner.rb +27 -0
  29. data/lib/redis_failover/util.rb +137 -0
  30. data/lib/redis_failover/version.rb +3 -0
  31. data/misc/redis_failover.png +0 -0
  32. data/spbtv_redis_failover.gemspec +26 -0
  33. data/spec/cli_spec.rb +75 -0
  34. data/spec/client_spec.rb +153 -0
  35. data/spec/failover_strategy/latency_spec.rb +41 -0
  36. data/spec/failover_strategy_spec.rb +17 -0
  37. data/spec/node_manager_spec.rb +136 -0
  38. data/spec/node_snapshot_spec.rb +30 -0
  39. data/spec/node_spec.rb +84 -0
  40. data/spec/node_strategy/consensus_spec.rb +30 -0
  41. data/spec/node_strategy/majority_spec.rb +22 -0
  42. data/spec/node_strategy/single_spec.rb +22 -0
  43. data/spec/node_strategy_spec.rb +22 -0
  44. data/spec/node_watcher_spec.rb +58 -0
  45. data/spec/spec_helper.rb +21 -0
  46. data/spec/support/config/multiple_environments.yml +15 -0
  47. data/spec/support/config/multiple_environments_with_chroot.yml +17 -0
  48. data/spec/support/config/single_environment.yml +7 -0
  49. data/spec/support/config/single_environment_with_chroot.yml +8 -0
  50. data/spec/support/node_manager_stub.rb +87 -0
  51. data/spec/support/redis_stub.rb +105 -0
  52. data/spec/util_spec.rb +21 -0
  53. 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
@@ -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