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