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.
- data/.gitignore +17 -0
- data/Changes.md +4 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +106 -0
- data/Rakefile +9 -0
- data/bin/redis_failover_server +6 -0
- data/lib/redis_failover.rb +16 -0
- data/lib/redis_failover/cli.rb +47 -0
- data/lib/redis_failover/client.rb +198 -0
- data/lib/redis_failover/errors.rb +25 -0
- data/lib/redis_failover/node.rb +101 -0
- data/lib/redis_failover/node_manager.rb +151 -0
- data/lib/redis_failover/node_watcher.rb +46 -0
- data/lib/redis_failover/runner.rb +31 -0
- data/lib/redis_failover/server.rb +13 -0
- data/lib/redis_failover/util.rb +29 -0
- data/lib/redis_failover/version.rb +3 -0
- data/redis_failover.gemspec +25 -0
- data/spec/cli_spec.rb +39 -0
- data/spec/client_spec.rb +62 -0
- data/spec/node_manager_spec.rb +76 -0
- data/spec/node_spec.rb +76 -0
- data/spec/node_watcher_spec.rb +43 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/node_manager_stub.rb +43 -0
- data/spec/support/redis_stub.rb +82 -0
- data/spec/util_spec.rb +11 -0
- metadata +157 -0
data/spec/client_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
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
|