Skip to content

Commit 03af476

Browse files
committed
initial node manager / watcher implementation
0 parents  commit 03af476

19 files changed

+419
-0
lines changed

.gitignore

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
*.gem
2+
*.rbc
3+
.bundle
4+
.config
5+
.yardoc
6+
Gemfile.lock
7+
InstalledFiles
8+
_yardoc
9+
coverage
10+
doc/
11+
lib/bundler/man
12+
pkg
13+
rdoc
14+
spec/reports
15+
test/tmp
16+
test/version_tmp
17+
tmp

Gemfile

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
source 'https://rubygems.org'
2+
gemspec

LICENSE

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Copyright (c) 2012 Ryan LeCompte
2+
3+
MIT License
4+
5+
Permission is hereby granted, free of charge, to any person obtaining
6+
a copy of this software and associated documentation files (the
7+
"Software"), to deal in the Software without restriction, including
8+
without limitation the rights to use, copy, modify, merge, publish,
9+
distribute, sublicense, and/or sell copies of the Software, and to
10+
permit persons to whom the Software is furnished to do so, subject to
11+
the following conditions:
12+
13+
The above copyright notice and this permission notice shall be
14+
included in all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# RedisFailover
2+
3+
Redis Failover provides a full automatic master/slave failover solution for Ruby
4+
5+
## Installation
6+
7+
Add this line to your application's Gemfile:
8+
9+
gem 'redis_failover'
10+
11+
And then execute:
12+
13+
$ bundle
14+
15+
Or install it yourself as:
16+
17+
$ gem install redis_failover
18+
19+
## Usage
20+
21+
TODO: Write usage instructions here
22+
23+
## Contributing
24+
25+
1. Fork it
26+
2. Create your feature branch (`git checkout -b my-new-feature`)
27+
3. Commit your changes (`git commit -am 'Added some feature'`)
28+
4. Push to the branch (`git push origin my-new-feature`)
29+
5. Create new Pull Request

Rakefile

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env rake
2+
require "bundler/gem_tasks"
3+
require 'rspec/core/rake_task'
4+
5+
RSpec::Core::RakeTask.new(:spec) do |t|
6+
t.rspec_opts = %w(--format progress)
7+
end
8+
9+
task :default => [:spec]

bin/redis_failover_server

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env ruby
2+
3+
require 'redis_failover'
4+
5+
manager = RedisFailover::NodeManager.new(:host => 'localhost', :password => 'fu')
6+
manager.start
7+

lib/redis_failover.rb

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
require 'redis'
2+
require 'thread'
3+
require 'logger'
4+
require 'securerandom'
5+
6+
require 'redis_failover/cli'
7+
require 'redis_failover/util'
8+
require 'redis_failover/node'
9+
require 'redis_failover/errors'
10+
require 'redis_failover/client'
11+
require 'redis_failover/server'
12+
require 'redis_failover/version'
13+
require 'redis_failover/node_manager'
14+
require 'redis_failover/node_watcher'
15+
require 'redis_failover/configuration'

lib/redis_failover/cli.rb

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module RedisFailover
2+
class CLI
3+
end
4+
end

lib/redis_failover/client.rb

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module RedisFailover
2+
class Client
3+
end
4+
end

lib/redis_failover/configuration.rb

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module RedisFailover
2+
class Configuration
3+
end
4+
end

lib/redis_failover/errors.rb

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module RedisFailover
2+
class Error < StandardError; end
3+
4+
class InvalidNodeError < Error; end
5+
6+
class NodeUnreachableError < Error; end
7+
8+
class NoMasterError < Error; end
9+
end

lib/redis_failover/node.rb

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
module RedisFailover
2+
# Represents a redis node (master or slave).
3+
class Node
4+
include Util
5+
6+
attr_reader :host, :port
7+
8+
def initialize(manager, options = {})
9+
@host = options.fetch(:host) { raise InvalidNodeError, 'missing host'}
10+
@port = options.fetch(:port, 6379)
11+
@password = options[:password]
12+
@manager = manager
13+
end
14+
15+
def reachable?
16+
fetch_info
17+
true
18+
rescue
19+
false
20+
end
21+
22+
def unreachable?
23+
!reachable?
24+
end
25+
26+
def master?
27+
role == 'master'
28+
end
29+
30+
def slave?
31+
!master?
32+
end
33+
34+
def wait_until_unreachable
35+
redis.blpop(wait_key, 0)
36+
rescue
37+
unless reachable?
38+
raise NodeUnreachableError, 'failed while waiting'
39+
end
40+
end
41+
42+
def stop_waiting
43+
redis.lpush(wait_key, '1')
44+
end
45+
46+
def make_slave!
47+
master = @manager.current_master
48+
redis.slaveof(master.host, master.port)
49+
end
50+
51+
def make_master!
52+
redis.slaveof('no', 'one')
53+
end
54+
55+
def inspect
56+
"<RedisFailover::Node #{to_s}>"
57+
end
58+
59+
def to_s
60+
"#{@host}:#{@port}"
61+
end
62+
63+
def ==(other)
64+
return false unless other.is_a?(Node)
65+
return true if self.equal?(other)
66+
[host, port] == [other.host, other.port]
67+
end
68+
alias_method :eql?, :==
69+
70+
private
71+
72+
def role
73+
fetch_info[:role]
74+
end
75+
76+
def fetch_info
77+
symbolize_keys(redis.info)
78+
end
79+
80+
def wait_key
81+
@wait_key ||= SecureRandom.hex(32)
82+
end
83+
84+
def redis
85+
Redis.new(:host => @host, :password => @password, :port => @port)
86+
rescue
87+
raise NodeUnreachableError, 'failed to create redis client'
88+
end
89+
end
90+
end

lib/redis_failover/node_manager.rb

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
module RedisFailover
2+
# NodeManager manages a list of redis nodes.
3+
class NodeManager
4+
include Util
5+
6+
def initialize(*nodes)
7+
@master, @slaves = parse_nodes(nodes)
8+
@unreachable = []
9+
@queue = Queue.new
10+
@lock = Mutex.new
11+
end
12+
13+
def start
14+
trap_signals
15+
spawn_watchers
16+
17+
logger.info('Redis Failover Server started successfully.')
18+
while node = @queue.pop
19+
if node.unreachable?
20+
handle_unreachable(node)
21+
elsif node.reachable?
22+
handle_reachable(node)
23+
end
24+
end
25+
end
26+
27+
def notify_state_change(node)
28+
@queue << node
29+
end
30+
31+
def current_master
32+
@master
33+
end
34+
35+
def nodes
36+
@lock.synchronize do
37+
{
38+
:master => current_master.to_s,
39+
:slaves => @slaves.map(&:to_s)
40+
}
41+
end
42+
end
43+
44+
def shutdown
45+
logger.info('Shutting down ...')
46+
@watchers.each(&:shutdown)
47+
exit(0)
48+
end
49+
50+
private
51+
52+
def handle_unreachable(node)
53+
@lock.synchronize do
54+
# no-op if we already know about this node
55+
return if @unreachable.include?(node)
56+
logger.info("Handling unreachable node: #{node}")
57+
58+
# find a new master if this node was a master
59+
if node == @master
60+
logger.info("Demoting currently unreachable master #{node}.")
61+
promote_new_master
62+
end
63+
@unreachable << node
64+
end
65+
end
66+
67+
def handle_reachable(node)
68+
@lock.synchronize do
69+
# no-op if we already know about this node
70+
return if @master == node || @slaves.include?(node)
71+
logger.info("Handling reachable node: #{node}")
72+
73+
@unreachable.delete(node)
74+
@slaves << node
75+
if current_master
76+
# master already exists, make a slave
77+
node.make_slave!
78+
else
79+
# no master exists, make this the new master
80+
promote_new_master
81+
end
82+
end
83+
end
84+
85+
def promote_new_master
86+
@master = nil
87+
88+
if @slaves.empty?
89+
logger.error('Failed to promote a new master since no slaves available.')
90+
return
91+
else
92+
# make a slave the new master
93+
node = @slaves.pop
94+
node.make_master!
95+
@master = node
96+
logger.info("Successfully promoted #{@master} to master.")
97+
end
98+
end
99+
100+
def parse_nodes(nodes)
101+
nodes = nodes.map { |opts| Node.new(self, opts) }
102+
raise NoMasterError unless master = nodes.find(&:master?)
103+
[master, nodes - [master]]
104+
end
105+
106+
def spawn_watchers
107+
@watchers = [@master, *@slaves].map do |node|
108+
NodeWatcher.new(self, node)
109+
end
110+
@watchers.each(&:watch)
111+
end
112+
113+
def trap_signals
114+
%w(INT TERM).each do |signal|
115+
trap(signal) { shutdown }
116+
end
117+
end
118+
end
119+
end

lib/redis_failover/node_watcher.rb

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
module RedisFailover
2+
# Watches a specific redis node for its reachability.
3+
class NodeWatcher
4+
def initialize(manager, node)
5+
@manager = manager
6+
@node = node
7+
@monitor_thread = nil
8+
@done = false
9+
end
10+
11+
def watch
12+
@monitor_thread = Thread.new { monitor_node }
13+
end
14+
15+
def shutdown
16+
@done = true
17+
@node.stop_waiting
18+
@monitor_thread.join if @monitor_thread
19+
end
20+
21+
private
22+
23+
def monitor_node
24+
return if @done
25+
@manager.notify_state_change(@node) if @node.reachable?
26+
@node.wait_until_unreachable
27+
rescue
28+
@manager.notify_state_change(@node)
29+
relax && retry
30+
end
31+
32+
def relax
33+
sleep(5)
34+
end
35+
end
36+
end

lib/redis_failover/server.rb

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module RedisFailover
2+
class Server
3+
end
4+
end

0 commit comments

Comments
 (0)