redis_failover 0.1.0

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