redis_failover 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ module RedisFailover
4
+ Client::Redis = RedisStub
5
+ class ClientStub < Client
6
+ def current_master
7
+ @master
8
+ end
9
+
10
+ def current_slaves
11
+ @slaves
12
+ end
13
+
14
+ def fetch_redis_servers
15
+ {
16
+ :master => 'localhost:6379',
17
+ :slaves => ['localhost:1111'],
18
+ :unreachable => []
19
+ }
20
+ end
21
+ end
22
+
23
+ describe Client do
24
+ let(:client) { ClientStub.new(:host => 'localhost', :port => 3000) }
25
+
26
+ describe '#build_clients' do
27
+ it 'properly parses master' do
28
+ client.current_master.to_s.should == 'localhost:6379'
29
+ end
30
+
31
+ it 'properly parses slaves' do
32
+ client.current_slaves.first.to_s.should == 'localhost:1111'
33
+ end
34
+ end
35
+
36
+ describe '#dispatch' do
37
+ it 'routes write operations to master' do
38
+ client.current_master.should_receive(:del)
39
+ client.del('foo')
40
+ end
41
+
42
+ it 'routes read operations to a slave' do
43
+ client.current_slaves.first.should_receive(:get)
44
+ client.get('foo')
45
+ end
46
+
47
+ it 'reconnects with redis failover server when node is unreachable' do
48
+ class << client
49
+ attr_reader :reconnected
50
+ def build_clients
51
+ @reconnected = true
52
+ super
53
+ end
54
+ end
55
+
56
+ client.current_master.make_unreachable!
57
+ client.del('foo')
58
+ client.reconnected.should be_true
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ module RedisFailover
4
+ describe NodeManager do
5
+ let(:manager) { NodeManagerStub.new({}) }
6
+
7
+ describe '#nodes' do
8
+ it 'returns current master and slave nodes' do
9
+ manager.nodes.should == {
10
+ :master => 'master:6379',
11
+ :slaves => ['slave:6379'],
12
+ :unreachable => []
13
+ }
14
+ end
15
+ end
16
+
17
+ describe '#handle_unreachable' do
18
+ context 'slave dies' do
19
+ it 'moves slave to unreachable list' do
20
+ slave = manager.slaves.first
21
+ manager.force_unreachable(slave)
22
+ manager.nodes[:unreachable].should include(slave.to_s)
23
+ end
24
+ end
25
+
26
+ context 'master dies' do
27
+ before(:each) do
28
+ @slave = manager.slaves.first
29
+ @master = manager.master
30
+ manager.force_unreachable(@master)
31
+ end
32
+
33
+ it 'promotes slave to master' do
34
+ manager.master.should == @slave
35
+ end
36
+
37
+ it 'moves master to unreachable list' do
38
+ manager.nodes[:unreachable].should include(@master.to_s)
39
+ end
40
+ end
41
+ end
42
+
43
+ describe '#handle_reachable' do
44
+ before(:each) do
45
+ # force to be unreachable first
46
+ @slave = manager.slaves.first
47
+ manager.force_unreachable(@slave)
48
+ end
49
+
50
+ context 'slave node with a master present' do
51
+ it 'removes slave from unreachable list' do
52
+ manager.force_reachable(@slave)
53
+ manager.nodes[:unreachable].should be_empty
54
+ manager.nodes[:slaves].should include(@slave.to_s)
55
+ end
56
+ end
57
+
58
+ context 'slave node with no master present' do
59
+ before(:each) do
60
+ @master = manager.master
61
+ manager.force_unreachable(@master)
62
+ end
63
+
64
+ it 'promotes slave to master' do
65
+ manager.master.should be_nil
66
+ manager.force_reachable(@slave)
67
+ manager.master.should == @slave
68
+ end
69
+
70
+ it 'slaves list remains empty' do
71
+ manager.nodes[:slaves].should be_empty
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
data/spec/node_spec.rb ADDED
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ module RedisFailover
4
+ describe Node do
5
+ let(:node) { Node.new(:host => 'localhost', :port => '123') }
6
+
7
+ before(:each) do
8
+ node.extend(RedisStubSupport)
9
+ end
10
+
11
+ describe '#initialize' do
12
+ it 'creates a new instance' do
13
+ node.host.should == 'localhost'
14
+ node.port.should == 123
15
+ end
16
+
17
+ it 'reports error if host missing' do
18
+ expect { Node.new }.to raise_error(InvalidNodeError)
19
+ end
20
+ end
21
+
22
+ describe '#ping' do
23
+ it 'responds properly if node is reachable' do
24
+ expect { node.ping }.to_not raise_error
25
+ end
26
+
27
+ it 'responds properly if node is unreachable' do
28
+ node.redis.make_unreachable!
29
+ expect { node.ping }.to raise_error(NodeUnreachableError)
30
+ end
31
+ end
32
+
33
+ describe '#master?' do
34
+ it 'responds properly if node is master' do
35
+ node.should be_master
36
+ end
37
+
38
+ it 'responds properly if node is not master' do
39
+ node.make_slave!(Node.new(:host => 'masterhost'))
40
+ node.should_not be_master
41
+ end
42
+ end
43
+
44
+
45
+ describe '#slave?' do
46
+ it 'responds properly if node is slave' do
47
+ node.should_not be_slave
48
+ end
49
+
50
+ it 'responds properly if node is not slave' do
51
+ node.make_master!
52
+ node.should be_master
53
+ end
54
+ end
55
+
56
+ describe '#wait_until_unreachable' do
57
+ it 'should wait until node dies' do
58
+ thread = Thread.new { node.wait_until_unreachable }
59
+ thread.should be_alive
60
+ node.redis.make_unreachable!
61
+ expect { thread.value }.to raise_error
62
+ end
63
+ end
64
+
65
+ describe '#stop_waiting' do
66
+ it 'should gracefully stop waiting' do
67
+ thread = Thread.new { node.wait_until_unreachable }
68
+ thread.should be_alive
69
+ node.stop_waiting
70
+ sleep 0.2
71
+ thread.should_not be_alive
72
+ thread.value.should be_nil
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ module RedisFailover
4
+ class LightNodeManager
5
+ def initialize
6
+ @node_states = {}
7
+ end
8
+
9
+ def notify_state_change(node, state)
10
+ @node_states[node] = state
11
+ end
12
+
13
+ def state_for(node)
14
+ @node_states[node]
15
+ end
16
+ end
17
+
18
+ describe NodeWatcher do
19
+ let(:node_manager) { LightNodeManager.new }
20
+ let(:node) { Node.new(:host => 'host', :port => 123).extend(RedisStubSupport) }
21
+
22
+ describe '#watch' do
23
+ it 'properly informs manager of unreachable node' do
24
+ watcher = NodeWatcher.new(node_manager, node, 1)
25
+ watcher.watch
26
+ sleep(3)
27
+ node.redis.make_unreachable!
28
+ sleep(3)
29
+ watcher.shutdown
30
+ node_manager.state_for(node).should == :unreachable
31
+ end
32
+
33
+ it 'properly informs manager of reachable node' do
34
+ node_manager.notify_state_change(node, :unreachable)
35
+ watcher = NodeWatcher.new(node_manager, node, 1)
36
+ watcher.watch
37
+ sleep(3)
38
+ watcher.shutdown
39
+ node_manager.state_for(node).should == :reachable
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ require 'rspec'
2
+ require 'redis_failover'
3
+
4
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
5
+
6
+ class NullObject
7
+ def method_missing(method, *args, &block)
8
+ self
9
+ end
10
+ end
11
+
12
+ RedisFailover::Util.logger = NullObject.new
13
+
14
+ RSpec.configure do |config|
15
+ end
@@ -0,0 +1,43 @@
1
+ module RedisFailover
2
+ class NodeManagerStub < NodeManager
3
+ def parse_nodes
4
+ master = Node.new(:host => 'master')
5
+ slave = Node.new(:host => 'slave')
6
+ [master, slave].each { |node| node.extend(RedisStubSupport) }
7
+ master.make_master!
8
+ slave.make_slave!(master)
9
+ [master, [slave]]
10
+ end
11
+
12
+ def master
13
+ @master
14
+ end
15
+
16
+ def slaves
17
+ @slaves
18
+ end
19
+
20
+ def start_processing
21
+ @thread = Thread.new { start }
22
+ end
23
+
24
+ def stop_processing
25
+ @queue << nil
26
+ @thread.value
27
+ end
28
+
29
+ def force_unreachable(node)
30
+ start_processing
31
+ node.redis.make_unreachable!
32
+ notify_state_change(node, :unreachable)
33
+ stop_processing
34
+ end
35
+
36
+ def force_reachable(node)
37
+ start_processing
38
+ node.redis.make_reachable!
39
+ notify_state_change(node, :reachable)
40
+ stop_processing
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,82 @@
1
+ require 'ostruct'
2
+
3
+ module RedisFailover
4
+ # Test stub for Redis.
5
+ class RedisStub
6
+ class Proxy
7
+ def initialize(queue, opts = {})
8
+ @info = {:role => 'master'}
9
+ @queue = queue
10
+ end
11
+
12
+ def blpop(*args)
13
+ @queue.pop.tap do |value|
14
+ raise value if value
15
+ end
16
+ end
17
+
18
+ def del(*args)
19
+ end
20
+
21
+ def lpush(*args)
22
+ @queue << nil
23
+ end
24
+
25
+ def slaveof(host, port)
26
+ if host == 'no' && port == 'one'
27
+ @info[:role] = 'master'
28
+ else
29
+ @info[:role] = 'slave'
30
+ end
31
+ end
32
+
33
+ def info
34
+ @info.dup
35
+ end
36
+
37
+ def ping
38
+ 'pong'
39
+ end
40
+ end
41
+
42
+ attr_reader :host, :port, :reachable
43
+ def initialize(opts = {})
44
+ @host = opts[:host]
45
+ @port = Integer(opts[:port])
46
+ @queue = Queue.new
47
+ @proxy = Proxy.new(@queue, opts)
48
+ @reachable = true
49
+ end
50
+
51
+ def method_missing(method, *args, &block)
52
+ if @reachable
53
+ @proxy.send(method, *args, &block)
54
+ else
55
+ raise Errno::ECONNREFUSED
56
+ end
57
+ end
58
+
59
+ def make_reachable!
60
+ @reachable = true
61
+ end
62
+
63
+ def make_unreachable!
64
+ @queue << Errno::ECONNREFUSED
65
+ @reachable = false
66
+ end
67
+
68
+ def to_s
69
+ "#{@host}:#{@port}"
70
+ end
71
+
72
+ def client
73
+ OpenStruct.new(:host => @host, :port => @port)
74
+ end
75
+ end
76
+
77
+ module RedisStubSupport
78
+ def redis
79
+ @redis ||= RedisStub.new(:host => @host, :port => @port)
80
+ end
81
+ end
82
+ end
data/spec/util_spec.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ module RedisFailover
4
+ describe Util do
5
+ describe '.symbolize_keys' do
6
+ it 'converts hash keys to symbols' do
7
+ Util.symbolize_keys('a' => 1, 'b' => 2).should == {:a => 1, :b => 2}
8
+ end
9
+ end
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis_failover
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ryan LeCompte
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-04-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: &70199893303420 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70199893303420
25
+ - !ruby/object:Gem::Dependency
26
+ name: redis-namespace
27
+ requirement: &70199893302980 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70199893302980
36
+ - !ruby/object:Gem::Dependency
37
+ name: multi_json
38
+ requirement: &70199893302560 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70199893302560
47
+ - !ruby/object:Gem::Dependency
48
+ name: sinatra
49
+ requirement: &70199893302140 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70199893302140
58
+ - !ruby/object:Gem::Dependency
59
+ name: rake
60
+ requirement: &70199893301720 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70199893301720
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: &70199893301300 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70199893301300
80
+ description: Redis Failover provides a full automatic master/slave failover solution
81
+ for Ruby
82
+ email:
83
+ - lecompte@gmail.com
84
+ executables:
85
+ - redis_failover_server
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - .gitignore
90
+ - Changes.md
91
+ - Gemfile
92
+ - LICENSE
93
+ - README.md
94
+ - Rakefile
95
+ - bin/redis_failover_server
96
+ - lib/redis_failover.rb
97
+ - lib/redis_failover/cli.rb
98
+ - lib/redis_failover/client.rb
99
+ - lib/redis_failover/errors.rb
100
+ - lib/redis_failover/node.rb
101
+ - lib/redis_failover/node_manager.rb
102
+ - lib/redis_failover/node_watcher.rb
103
+ - lib/redis_failover/runner.rb
104
+ - lib/redis_failover/server.rb
105
+ - lib/redis_failover/util.rb
106
+ - lib/redis_failover/version.rb
107
+ - redis_failover.gemspec
108
+ - spec/cli_spec.rb
109
+ - spec/client_spec.rb
110
+ - spec/node_manager_spec.rb
111
+ - spec/node_spec.rb
112
+ - spec/node_watcher_spec.rb
113
+ - spec/spec_helper.rb
114
+ - spec/support/node_manager_stub.rb
115
+ - spec/support/redis_stub.rb
116
+ - spec/util_spec.rb
117
+ homepage: http://github.com/ryanlecompte/redis_failover
118
+ licenses: []
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ none: false
125
+ requirements:
126
+ - - ! '>='
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ segments:
130
+ - 0
131
+ hash: -1217445009113181940
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ none: false
134
+ requirements:
135
+ - - ! '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ segments:
139
+ - 0
140
+ hash: -1217445009113181940
141
+ requirements: []
142
+ rubyforge_project:
143
+ rubygems_version: 1.8.16
144
+ signing_key:
145
+ specification_version: 3
146
+ summary: Redis Failover provides a full automatic master/slave failover solution for
147
+ Ruby
148
+ test_files:
149
+ - spec/cli_spec.rb
150
+ - spec/client_spec.rb
151
+ - spec/node_manager_spec.rb
152
+ - spec/node_spec.rb
153
+ - spec/node_watcher_spec.rb
154
+ - spec/spec_helper.rb
155
+ - spec/support/node_manager_stub.rb
156
+ - spec/support/redis_stub.rb
157
+ - spec/util_spec.rb