redis_failover 0.5.4 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +1 -0
- data/Changes.md +6 -0
- data/README.md +16 -15
- data/lib/redis_failover.rb +1 -0
- data/lib/redis_failover/cli.rb +26 -15
- data/lib/redis_failover/client.rb +44 -14
- data/lib/redis_failover/node.rb +6 -1
- data/lib/redis_failover/node_manager.rb +38 -11
- data/lib/redis_failover/node_watcher.rb +5 -2
- data/lib/redis_failover/runner.rb +0 -1
- data/lib/redis_failover/util.rb +6 -1
- data/lib/redis_failover/version.rb +1 -1
- data/lib/redis_failover/zk_client.rb +14 -4
- data/redis_failover.gemspec +2 -2
- data/spec/cli_spec.rb +13 -7
- data/spec/node_manager_spec.rb +4 -0
- data/spec/node_watcher_spec.rb +3 -3
- data/spec/spec_helper.rb +1 -0
- data/spec/support/node_manager_stub.rb +17 -3
- metadata +56 -20
data/.travis.yml
CHANGED
data/Changes.md
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
0.6.0
|
2
|
+
-----------
|
3
|
+
- Add support for running multiple Node Manager processes for added redundancy (#4)
|
4
|
+
- Add support for specifying a redis database in RedisFailover::Client (#5)
|
5
|
+
- Improved Node Manager command-line option parsing
|
6
|
+
|
1
7
|
0.5.4
|
2
8
|
-----------
|
3
9
|
- No longer use problematic ZK#reopen.
|
data/README.md
CHANGED
@@ -22,7 +22,8 @@ nodes. Note that detection of a node going down should be nearly instantaneous,
|
|
22
22
|
used to keep tabs on a node is via a blocking Redis BLPOP call (no polling). This call fails nearly
|
23
23
|
immediately when the node actually goes offline. To avoid false positives (i.e., intermittent flaky
|
24
24
|
network interruption), the Node Manager will only mark a node as unavailable if it fails to communicate with
|
25
|
-
it 3 times (this is configurable via --max-failures, see configuration options below).
|
25
|
+
it 3 times (this is configurable via --max-failures, see configuration options below). Note that you can
|
26
|
+
deploy multiple Node Manager daemons for added redundancy.
|
26
27
|
|
27
28
|
This gem provides a RedisFailover::Client wrapper that is master/slave aware. The client is configured
|
28
29
|
with a list of ZooKeeper servers. The client will automatically contact the ZooKeeper cluster to find out
|
@@ -56,12 +57,13 @@ The Node Manager is a simple process that should be run as a background daemon.
|
|
56
57
|
following options:
|
57
58
|
|
58
59
|
Usage: redis_node_manager [OPTIONS]
|
59
|
-
|
60
|
-
|
61
|
-
-
|
62
|
-
|
63
|
-
|
64
|
-
--
|
60
|
+
|
61
|
+
Specific options:
|
62
|
+
-n, --nodes NODES Comma-separated redis host:port pairs
|
63
|
+
-z, --zkservers SERVERS Comma-separated ZooKeeper host:port pairs
|
64
|
+
-p, --password [PASSWORD] Redis password
|
65
|
+
--znode-path [PATH] Znode path override for storing redis server list
|
66
|
+
--max-failures [COUNT] Max failures before manager marks node unavailable
|
65
67
|
-h, --help Display all options
|
66
68
|
|
67
69
|
To start the daemon for a simple master/slave configuration, use the following:
|
@@ -69,8 +71,11 @@ To start the daemon for a simple master/slave configuration, use the following:
|
|
69
71
|
redis_node_manager -n localhost:6379,localhost:6380 -z localhost:2181,localhost:2182,localhost:2183
|
70
72
|
|
71
73
|
The Node Manager will automatically discover the master/slaves upon startup. Note that it is
|
72
|
-
a good idea to
|
73
|
-
|
74
|
+
a good idea to run more than one instance of the Node Manager daemon in your environment. At
|
75
|
+
any moment, a single Node Manager process will be designated to monitor the redis servers. If
|
76
|
+
this Node Manager process dies or becomes partitioned from the network, another Node Manager
|
77
|
+
will be promoted as the primary monitor of redis servers. You can run as many Node Manager
|
78
|
+
processes as you'd like for added redundancy.
|
74
79
|
|
75
80
|
## Client Usage
|
76
81
|
|
@@ -88,6 +93,7 @@ The full set of options that can be passed to RedisFailover::Client are:
|
|
88
93
|
:zkservers - comma-separated ZooKeeper host:port pairs (required)
|
89
94
|
:znode_path - the Znode path override for redis server list (optional)
|
90
95
|
:password - password for redis nodes (optional)
|
96
|
+
:db - db to use for redis nodes (optional)
|
91
97
|
:namespace - namespace for redis nodes (optional)
|
92
98
|
:logger - logger override (optional)
|
93
99
|
:retry_failure - indicate if failures should be retried (default true)
|
@@ -95,7 +101,7 @@ The full set of options that can be passed to RedisFailover::Client are:
|
|
95
101
|
|
96
102
|
## Requirements
|
97
103
|
|
98
|
-
- redis_failover is actively tested against MRI 1.9.2/1.9.3. Other rubies may work, although I don't actively test against them. 1.8 is not supported.
|
104
|
+
- redis_failover is actively tested against MRI 1.9.2/1.9.3 and JRuby 1.6.7 (1.9 mode only). Other rubies may work, although I don't actively test against them. 1.8 is not supported.
|
99
105
|
- redis_failover requires a ZooKeeper service cluster to ensure reliability and data consistency. ZooKeeper is very simple and easy to get up and running. Please refer to this [Quick ZooKeeper Guide](https://github.com/ryanlecompte/redis_failover/wiki/Quick-ZooKeeper-Guide) to get up and running quickly if you don't already have ZooKeeper as a part of your environment.
|
100
106
|
|
101
107
|
## Considerations
|
@@ -104,11 +110,6 @@ The full set of options that can be passed to RedisFailover::Client are:
|
|
104
110
|
|
105
111
|
- Note that it's still possible for the RedisFailover::Client instances to see a stale list of servers for a very small window. In most cases this will not be the case due to how ZooKeeper handles distributed communication, but you should be aware that in the worst case the client could write to a "stale" master for a small period of time until the next watch event is received by the client via ZooKeeper.
|
106
112
|
|
107
|
-
## TODO
|
108
|
-
|
109
|
-
- Rework specs to work against a set of real Redis/ZooKeeper nodes as opposed to stubs.
|
110
|
-
- Add support for running more than one Node Manager.
|
111
|
-
|
112
113
|
## Resources
|
113
114
|
|
114
115
|
- To learn more about Redis master/slave replication, see the [Redis documentation](http://redis.io/topics/replication).
|
data/lib/redis_failover.rb
CHANGED
data/lib/redis_failover/cli.rb
CHANGED
@@ -2,36 +2,36 @@ module RedisFailover
|
|
2
2
|
# Parses server command-line arguments.
|
3
3
|
class CLI
|
4
4
|
def self.parse(source)
|
5
|
-
return {} if source.empty?
|
6
|
-
|
7
5
|
options = {}
|
8
6
|
parser = OptionParser.new do |opts|
|
9
7
|
opts.banner = "Usage: redis_node_manager [OPTIONS]"
|
8
|
+
opts.separator ""
|
9
|
+
opts.separator "Specific options:"
|
10
10
|
|
11
|
-
opts.on('-
|
12
|
-
|
13
|
-
end
|
14
|
-
|
15
|
-
opts.on('-n', '--nodes redis nodes',
|
16
|
-
'Comma-separated redis host:port pairs (required)') do |nodes|
|
11
|
+
opts.on('-n', '--nodes NODES',
|
12
|
+
'Comma-separated redis host:port pairs') do |nodes|
|
17
13
|
# turns 'host1:port,host2:port' => [{:host => host, :port => port}, ...]
|
18
14
|
options[:nodes] = nodes.split(',').map do |node|
|
19
15
|
Hash[[:host, :port].zip(node.strip.split(':'))]
|
20
16
|
end
|
21
17
|
end
|
22
18
|
|
23
|
-
opts.on('-z', '--zkservers
|
24
|
-
'Comma-separated ZooKeeper host:port pairs
|
25
|
-
options[:zkservers] = servers
|
19
|
+
opts.on('-z', '--zkservers SERVERS',
|
20
|
+
'Comma-separated ZooKeeper host:port pairs') do |servers|
|
21
|
+
options[:zkservers] = servers.strip
|
26
22
|
end
|
27
23
|
|
28
|
-
opts.on('
|
29
|
-
|
24
|
+
opts.on('-p', '--password [PASSWORD]', 'Redis password') do |password|
|
25
|
+
options[:password] = password.strip
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on('--znode-path [PATH]',
|
29
|
+
'Znode path override for storing redis server list') do |path|
|
30
30
|
options[:znode_path] = path
|
31
31
|
end
|
32
32
|
|
33
|
-
opts.on('--max-failures
|
34
|
-
'Max failures before manager marks node unavailable
|
33
|
+
opts.on('--max-failures [COUNT]',
|
34
|
+
'Max failures before manager marks node unavailable') do |max|
|
35
35
|
options[:max_failures] = Integer(max)
|
36
36
|
end
|
37
37
|
|
@@ -42,6 +42,11 @@ module RedisFailover
|
|
42
42
|
end
|
43
43
|
|
44
44
|
parser.parse(source)
|
45
|
+
if required_options_missing?(options)
|
46
|
+
puts parser
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
|
45
50
|
# assume password is same for all redis nodes
|
46
51
|
if password = options[:password]
|
47
52
|
options[:nodes].each { |opts| opts.update(:password => password) }
|
@@ -49,5 +54,11 @@ module RedisFailover
|
|
49
54
|
|
50
55
|
options
|
51
56
|
end
|
57
|
+
|
58
|
+
def self.required_options_missing?(options)
|
59
|
+
return true if options.empty?
|
60
|
+
return true unless options.values_at(:nodes, :zkservers).all?
|
61
|
+
false
|
62
|
+
end
|
52
63
|
end
|
53
64
|
end
|
@@ -1,12 +1,32 @@
|
|
1
|
-
require 'set'
|
2
|
-
|
3
1
|
module RedisFailover
|
4
|
-
# Redis failover-aware client.
|
2
|
+
# Redis failover-aware client. RedisFailover::Client is a wrapper over a set of underlying redis
|
3
|
+
# clients, which means all normal redis operations can be performed on an instance of this class.
|
4
|
+
# The class only requires a set of ZooKeeper server addresses to function properly. The client
|
5
|
+
# will automatically retry failed operations, and handle failover to a new master. The client
|
6
|
+
# registers and listens for watcher events from the Node Manager. When these events are received,
|
7
|
+
# the client fetches the latest set of redis nodes from ZooKeeper and rebuilds its internal
|
8
|
+
# Redis clients appropriately. RedisFailover::Client also directs write operations to the master,
|
9
|
+
# and all read operations to the slaves.
|
10
|
+
#
|
11
|
+
# Examples
|
12
|
+
#
|
13
|
+
# client = RedisFailover::Client.new(:zkservers => 'localhost:2181,localhost:2182,localhost:2183')
|
14
|
+
# client.set('foo', 1) # will be directed to master
|
15
|
+
# client.get('foo') # will be directed to a slave
|
16
|
+
#
|
5
17
|
class Client
|
6
18
|
include Util
|
7
19
|
|
20
|
+
# Maximum allowed elapsed time between notifications from the Node Manager.
|
21
|
+
# When this timeout is reached, the client will raise a NoNodeManagerError
|
22
|
+
# and purge its internal redis clients.
|
8
23
|
ZNODE_UPDATE_TIMEOUT = 9
|
24
|
+
|
25
|
+
# Amount of time to sleep before retrying a failed operation.
|
9
26
|
RETRY_WAIT_TIME = 3
|
27
|
+
|
28
|
+
# Redis read operations that are automatically dispatched to slaves. Any
|
29
|
+
# operation not listed here will be dispatched to the master.
|
10
30
|
REDIS_READ_OPS = Set[
|
11
31
|
:echo,
|
12
32
|
:exists,
|
@@ -47,6 +67,8 @@ module RedisFailover
|
|
47
67
|
:zscore
|
48
68
|
].freeze
|
49
69
|
|
70
|
+
# Unsupported Redis operations. These don't make sense in a client
|
71
|
+
# that abstracts the master/slave servers.
|
50
72
|
UNSUPPORTED_OPS = Set[
|
51
73
|
:select,
|
52
74
|
:ttl,
|
@@ -66,13 +88,14 @@ module RedisFailover
|
|
66
88
|
#
|
67
89
|
# Options:
|
68
90
|
#
|
69
|
-
# :zkservers
|
70
|
-
# :znode_path
|
71
|
-
# :password
|
72
|
-
# :
|
73
|
-
# :
|
91
|
+
# :zkservers - comma-separated ZooKeeper host:port pairs (required)
|
92
|
+
# :znode_path - the Znode path override for redis server list (optional)
|
93
|
+
# :password - password for redis nodes (optional)
|
94
|
+
# :db - db to use for redis nodes (optional)
|
95
|
+
# :namespace - namespace for redis nodes (optional)
|
96
|
+
# :logger - logger override (optional)
|
74
97
|
# :retry_failure - indicate if failures should be retried (default true)
|
75
|
-
# :max_retries
|
98
|
+
# :max_retries - max retries for a failure (default 3)
|
76
99
|
#
|
77
100
|
def initialize(options = {})
|
78
101
|
Util.logger = options[:logger] if options[:logger]
|
@@ -80,6 +103,7 @@ module RedisFailover
|
|
80
103
|
@znode = options[:znode_path] || Util::DEFAULT_ZNODE_PATH
|
81
104
|
@namespace = options[:namespace]
|
82
105
|
@password = options[:password]
|
106
|
+
@db = options[:db]
|
83
107
|
@retry = options[:retry_failure] || true
|
84
108
|
@max_retries = @retry ? options.fetch(:max_retries, 3) : 1
|
85
109
|
@master = nil
|
@@ -89,6 +113,7 @@ module RedisFailover
|
|
89
113
|
build_clients
|
90
114
|
end
|
91
115
|
|
116
|
+
# Dispatches redis operations to master/slaves.
|
92
117
|
def method_missing(method, *args, &block)
|
93
118
|
if redis_operation?(method)
|
94
119
|
dispatch(method, *args, &block)
|
@@ -102,7 +127,7 @@ module RedisFailover
|
|
102
127
|
end
|
103
128
|
|
104
129
|
def inspect
|
105
|
-
"#<RedisFailover::Client
|
130
|
+
"#<RedisFailover::Client (master: #{master_name}, slaves: #{slave_names})>"
|
106
131
|
end
|
107
132
|
alias_method :to_s, :inspect
|
108
133
|
|
@@ -163,13 +188,14 @@ module RedisFailover
|
|
163
188
|
master.send(method, *args, &block)
|
164
189
|
end
|
165
190
|
rescue *CONNECTIVITY_ERRORS => ex
|
166
|
-
logger.error("Error while handling
|
191
|
+
logger.error("Error while handling `#{method}` - #{ex.inspect}")
|
167
192
|
logger.error(ex.backtrace.join("\n"))
|
168
193
|
|
169
194
|
if tries < @max_retries
|
170
195
|
tries += 1
|
171
196
|
build_clients
|
172
|
-
sleep(RETRY_WAIT_TIME)
|
197
|
+
sleep(RETRY_WAIT_TIME)
|
198
|
+
retry
|
173
199
|
end
|
174
200
|
|
175
201
|
raise
|
@@ -214,7 +240,8 @@ module RedisFailover
|
|
214
240
|
|
215
241
|
if tries < @max_retries
|
216
242
|
tries += 1
|
217
|
-
sleep(RETRY_WAIT_TIME)
|
243
|
+
sleep(RETRY_WAIT_TIME)
|
244
|
+
retry
|
218
245
|
end
|
219
246
|
|
220
247
|
raise
|
@@ -233,7 +260,10 @@ module RedisFailover
|
|
233
260
|
def new_clients_for(*nodes)
|
234
261
|
nodes.map do |node|
|
235
262
|
host, port = node.split(':')
|
236
|
-
|
263
|
+
opts = {:host => host, :port => port}
|
264
|
+
opts.update(:db => @db) if @db
|
265
|
+
opts.update(:password => @password) if @password
|
266
|
+
client = Redis.new(opts)
|
237
267
|
if @namespace
|
238
268
|
client = Redis::Namespace.new(@namespace, :redis => client)
|
239
269
|
end
|
data/lib/redis_failover/node.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
module RedisFailover
|
2
|
-
# Represents a redis node (master or slave).
|
2
|
+
# Represents a redis node (master or slave). Instances of this class
|
3
|
+
# are used by the NodeManager and NodeWatcher to manipulate real redis
|
4
|
+
# servers.
|
3
5
|
class Node
|
4
6
|
include Util
|
5
7
|
|
8
|
+
# Maximum amount of time given for any redis operation to complete.
|
9
|
+
# If a redis operation doesn't complete in the alotted time, a
|
10
|
+
# NodeUnavailableError will be raised.
|
6
11
|
MAX_OP_WAIT_TIME = 5
|
7
12
|
|
8
13
|
attr_reader :host, :port
|
@@ -1,24 +1,51 @@
|
|
1
1
|
module RedisFailover
|
2
|
-
# NodeManager manages a list of redis nodes.
|
2
|
+
# NodeManager manages a list of redis nodes. Upon startup, the NodeManager
|
3
|
+
# will discover the current redis master and slaves. Each redis node is
|
4
|
+
# monitored by a NodeWatcher instance. The NodeWatchers periodically
|
5
|
+
# report the current state of the redis node it's watching to the
|
6
|
+
# NodeManager via an asynchronous queue. The NodeManager processes the
|
7
|
+
# state reports and reacts appropriately by handling stale/dead nodes,
|
8
|
+
# and promoting a new redis master if it sees fit to do so.
|
3
9
|
class NodeManager
|
4
10
|
include Util
|
5
11
|
|
12
|
+
# Name for the znode that handles exclusive locking between multiple
|
13
|
+
# Node Manager processes. Whoever holds the lock will be considered
|
14
|
+
# the "master" Node Manager, and will be responsible for monitoring
|
15
|
+
# the redis nodes. When a Node Manager that holds the lock disappears
|
16
|
+
# or fails, another Node Manager process will grab the lock and
|
17
|
+
# become the master.
|
18
|
+
LOCK_PATH = 'master_node_manager'
|
19
|
+
|
20
|
+
# Number of seconds to wait before retrying bootstrap process.
|
21
|
+
TIMEOUT = 3
|
22
|
+
|
6
23
|
def initialize(options)
|
24
|
+
logger.info("Redis Node Manager v#{VERSION} starting (#{RUBY_DESCRIPTION})")
|
7
25
|
@options = options
|
8
|
-
@zkclient = ZkClient.new(@options[:zkservers])
|
9
26
|
@znode = @options[:znode_path] || Util::DEFAULT_ZNODE_PATH
|
10
27
|
@unavailable = []
|
11
28
|
@queue = Queue.new
|
12
|
-
discover_nodes
|
13
29
|
end
|
14
30
|
|
15
31
|
def start
|
16
|
-
|
17
|
-
|
18
|
-
|
32
|
+
@zkclient = ZkClient.new(@options[:zkservers])
|
33
|
+
logger.info('Waiting to become master Node Manager ...')
|
34
|
+
@zkclient.with_lock(LOCK_PATH) do
|
35
|
+
logger.info('Acquired master Node Manager lock')
|
36
|
+
discover_nodes
|
37
|
+
initialize_path
|
38
|
+
spawn_watchers
|
39
|
+
handle_state_reports
|
40
|
+
end
|
41
|
+
rescue *CONNECTIVITY_ERRORS => ex
|
42
|
+
logger.error("Error while attempting to manage nodes: #{ex.inspect}")
|
43
|
+
logger.error(ex.backtrace.join("\n"))
|
44
|
+
sleep(TIMEOUT)
|
45
|
+
retry
|
19
46
|
end
|
20
47
|
|
21
|
-
def
|
48
|
+
def notify_state(node, state)
|
22
49
|
@queue << [node, state]
|
23
50
|
end
|
24
51
|
|
@@ -29,10 +56,10 @@ module RedisFailover
|
|
29
56
|
|
30
57
|
private
|
31
58
|
|
32
|
-
def
|
33
|
-
while
|
59
|
+
def handle_state_reports
|
60
|
+
while state_report = @queue.pop
|
34
61
|
begin
|
35
|
-
node, state =
|
62
|
+
node, state = state_report
|
36
63
|
case state
|
37
64
|
when :unavailable then handle_unavailable(node)
|
38
65
|
when :available then handle_available(node)
|
@@ -43,7 +70,7 @@ module RedisFailover
|
|
43
70
|
# flush current state
|
44
71
|
write_state
|
45
72
|
rescue StandardError, *CONNECTIVITY_ERRORS => ex
|
46
|
-
logger.error("Error
|
73
|
+
logger.error("Error handling #{state_report.inspect}: #{ex.inspect}")
|
47
74
|
logger.error(ex.backtrace.join("\n"))
|
48
75
|
end
|
49
76
|
end
|
@@ -1,8 +1,11 @@
|
|
1
1
|
module RedisFailover
|
2
|
-
#
|
2
|
+
# NodeWatcher periodically monitors a specific redis node for its availability.
|
3
|
+
# NodeWatcher instances periodically report a redis node's current state
|
4
|
+
# to the NodeManager for proper handling.
|
3
5
|
class NodeWatcher
|
4
6
|
include Util
|
5
7
|
|
8
|
+
# Time to sleep before checking on the monitored node's status.
|
6
9
|
WATCHER_SLEEP_TIME = 2
|
7
10
|
|
8
11
|
def initialize(manager, node, max_failures)
|
@@ -55,7 +58,7 @@ module RedisFailover
|
|
55
58
|
end
|
56
59
|
|
57
60
|
def notify(state)
|
58
|
-
@manager.
|
61
|
+
@manager.notify_state(@node, state)
|
59
62
|
end
|
60
63
|
end
|
61
64
|
end
|
@@ -6,7 +6,6 @@ module RedisFailover
|
|
6
6
|
@node_manager = NodeManager.new(options)
|
7
7
|
trap_signals
|
8
8
|
node_manager_thread = Thread.new { @node_manager.start }
|
9
|
-
Util.logger.info("Redis Node Manager v#{VERSION} successfully started (#{RUBY_DESCRIPTION})")
|
10
9
|
node_manager_thread.join
|
11
10
|
end
|
12
11
|
|
data/lib/redis_failover/util.rb
CHANGED
@@ -1,12 +1,17 @@
|
|
1
1
|
require 'redis_failover/errors'
|
2
2
|
|
3
3
|
module RedisFailover
|
4
|
-
# Common utiilty methods.
|
4
|
+
# Common utiilty methods and constants.
|
5
5
|
module Util
|
6
6
|
extend self
|
7
7
|
|
8
|
+
# Default node in ZooKeeper that contains the current list of available redis nodes.
|
8
9
|
DEFAULT_ZNODE_PATH = '/redis_failover_nodes'.freeze
|
10
|
+
|
11
|
+
# Connectivity errors that the redis client raises.
|
9
12
|
REDIS_ERRORS = Errno.constants.map { |c| Errno.const_get(c) }.freeze
|
13
|
+
|
14
|
+
# Full set of errors related to connectivity.
|
10
15
|
CONNECTIVITY_ERRORS = [
|
11
16
|
RedisFailover::Error,
|
12
17
|
ZK::Exceptions::KeeperException,
|
@@ -4,7 +4,13 @@ module RedisFailover
|
|
4
4
|
class ZkClient
|
5
5
|
include Util
|
6
6
|
|
7
|
+
# Time to sleep before retrying a failed operation.
|
8
|
+
TIMEOUT = 2
|
9
|
+
|
10
|
+
# Maximum reconnect attempts.
|
7
11
|
MAX_RECONNECTS = 3
|
12
|
+
|
13
|
+
# Errors that are candidates for rebuilding the underlying ZK client.
|
8
14
|
RECONNECTABLE_ERRORS = [
|
9
15
|
ZookeeperExceptions::ZookeeperException::SessionExpired,
|
10
16
|
ZookeeperExceptions::ZookeeperException::SystemError,
|
@@ -15,6 +21,8 @@ module RedisFailover
|
|
15
21
|
ZookeeperExceptions::ZookeeperException::ConnectionClosed,
|
16
22
|
ZookeeperExceptions::ZookeeperException::NotConnected
|
17
23
|
].freeze
|
24
|
+
|
25
|
+
# ZK methods that are wrapped with reconnect logic.
|
18
26
|
WRAPPED_ZK_METHODS = [
|
19
27
|
:get,
|
20
28
|
:set,
|
@@ -22,7 +30,8 @@ module RedisFailover
|
|
22
30
|
:event_handler,
|
23
31
|
:stat,
|
24
32
|
:create,
|
25
|
-
:delete
|
33
|
+
:delete,
|
34
|
+
:with_lock].freeze
|
26
35
|
|
27
36
|
def initialize(servers, &setup_block)
|
28
37
|
@servers = servers
|
@@ -42,7 +51,7 @@ module RedisFailover
|
|
42
51
|
end
|
43
52
|
|
44
53
|
WRAPPED_ZK_METHODS.each do |zk_method|
|
45
|
-
class_eval
|
54
|
+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
46
55
|
def #{zk_method}(*args, &block)
|
47
56
|
perform_with_reconnect do
|
48
57
|
@client.#{zk_method}(*args, &block)
|
@@ -58,14 +67,15 @@ module RedisFailover
|
|
58
67
|
begin
|
59
68
|
yield
|
60
69
|
rescue *RECONNECTABLE_ERRORS => ex
|
61
|
-
logger.error("ZooKeeper
|
70
|
+
logger.error("ZooKeeper connection error, rebuilding client: #{ex.inspect}")
|
62
71
|
logger.error(ex.backtrace.join("\n"))
|
63
72
|
if tries < MAX_RECONNECTS
|
64
73
|
tries += 1
|
65
74
|
@on_session_expiration.call if @on_session_expiration
|
66
75
|
build_client
|
67
76
|
@on_session_recovered.call if @on_session_recovered
|
68
|
-
sleep(
|
77
|
+
sleep(TIMEOUT)
|
78
|
+
retry
|
69
79
|
end
|
70
80
|
|
71
81
|
raise
|
data/redis_failover.gemspec
CHANGED
@@ -17,8 +17,8 @@ Gem::Specification.new do |gem|
|
|
17
17
|
|
18
18
|
gem.add_dependency('redis')
|
19
19
|
gem.add_dependency('redis-namespace')
|
20
|
-
gem.add_dependency('multi_json')
|
21
|
-
gem.add_dependency('zk')
|
20
|
+
gem.add_dependency('multi_json', '>= 1.0', '< 1.3')
|
21
|
+
gem.add_dependency('zk', '~> 0.8.8')
|
22
22
|
|
23
23
|
gem.add_development_dependency('rake')
|
24
24
|
gem.add_development_dependency('rspec')
|
data/spec/cli_spec.rb
CHANGED
@@ -3,12 +3,8 @@ require 'spec_helper'
|
|
3
3
|
module RedisFailover
|
4
4
|
describe CLI do
|
5
5
|
describe '.parse' do
|
6
|
-
it 'returns empty result for empty options' do
|
7
|
-
CLI.parse({}).should == {}
|
8
|
-
end
|
9
|
-
|
10
6
|
it 'properly parses redis nodes' do
|
11
|
-
opts = CLI.parse(['-n host1:1,host2:2,host3:3'])
|
7
|
+
opts = CLI.parse(['-n host1:1,host2:2,host3:3', '-z localhost:1111'])
|
12
8
|
opts[:nodes].should == [
|
13
9
|
{:host => 'host1', :port => '1'},
|
14
10
|
{:host => 'host2', :port => '2'},
|
@@ -16,8 +12,13 @@ module RedisFailover
|
|
16
12
|
]
|
17
13
|
end
|
18
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
|
+
|
19
20
|
it 'properly parses a redis password' do
|
20
|
-
opts = CLI.parse(['-n host:port', '-p redis_pass'])
|
21
|
+
opts = CLI.parse(['-n host:port', '-z localhost:1111', '-p redis_pass'])
|
21
22
|
opts[:nodes].should == [{
|
22
23
|
:host => 'host',
|
23
24
|
:port => 'port',
|
@@ -26,7 +27,12 @@ module RedisFailover
|
|
26
27
|
end
|
27
28
|
|
28
29
|
it 'properly parses max node failures' do
|
29
|
-
opts = CLI.parse([
|
30
|
+
opts = CLI.parse([
|
31
|
+
'-n host:port',
|
32
|
+
'-z localhost:1111',
|
33
|
+
'-p redis_pass',
|
34
|
+
'--max-failures',
|
35
|
+
'1'])
|
30
36
|
opts[:max_failures].should == 1
|
31
37
|
end
|
32
38
|
end
|
data/spec/node_manager_spec.rb
CHANGED
data/spec/node_watcher_spec.rb
CHANGED
@@ -6,7 +6,7 @@ module RedisFailover
|
|
6
6
|
@node_states = {}
|
7
7
|
end
|
8
8
|
|
9
|
-
def
|
9
|
+
def notify_state(node, state)
|
10
10
|
@node_states[node] = state
|
11
11
|
end
|
12
12
|
|
@@ -32,7 +32,7 @@ module RedisFailover
|
|
32
32
|
end
|
33
33
|
|
34
34
|
it 'properly informs manager of available node' do
|
35
|
-
node_manager.
|
35
|
+
node_manager.notify_state(node, :unavailable)
|
36
36
|
watcher = NodeWatcher.new(node_manager, node, 1)
|
37
37
|
watcher.watch
|
38
38
|
sleep(3)
|
@@ -43,7 +43,7 @@ module RedisFailover
|
|
43
43
|
|
44
44
|
context 'node is syncing with master' do
|
45
45
|
it 'properly informs manager of syncing node' do
|
46
|
-
node_manager.
|
46
|
+
node_manager.notify_state(node, :unavailable)
|
47
47
|
node.redis.slaveof('masterhost', 9876)
|
48
48
|
node.redis.force_sync_with_master(true)
|
49
49
|
watcher = NodeWatcher.new(node_manager, node, 1)
|
data/spec/spec_helper.rb
CHANGED
@@ -3,7 +3,20 @@ module RedisFailover
|
|
3
3
|
attr_accessor :master
|
4
4
|
public :current_nodes
|
5
5
|
|
6
|
+
def initialize(options)
|
7
|
+
super
|
8
|
+
@zklock = Object.new
|
9
|
+
@zklock.instance_eval do
|
10
|
+
def with_lock
|
11
|
+
yield
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
6
16
|
def discover_nodes
|
17
|
+
# only discover nodes once in testing
|
18
|
+
return if @nodes_discovered
|
19
|
+
|
7
20
|
master = Node.new(:host => 'master')
|
8
21
|
slave = Node.new(:host => 'slave')
|
9
22
|
[master, slave].each { |node| node.extend(RedisStubSupport) }
|
@@ -11,6 +24,7 @@ module RedisFailover
|
|
11
24
|
slave.make_slave!(master)
|
12
25
|
@master = master
|
13
26
|
@slaves = [slave]
|
27
|
+
@nodes_discovered = true
|
14
28
|
end
|
15
29
|
|
16
30
|
def slaves
|
@@ -29,21 +43,21 @@ module RedisFailover
|
|
29
43
|
def force_unavailable(node)
|
30
44
|
start_processing
|
31
45
|
node.redis.make_unavailable!
|
32
|
-
|
46
|
+
notify_state(node, :unavailable)
|
33
47
|
stop_processing
|
34
48
|
end
|
35
49
|
|
36
50
|
def force_available(node)
|
37
51
|
start_processing
|
38
52
|
node.redis.make_available!
|
39
|
-
|
53
|
+
notify_state(node, :available)
|
40
54
|
stop_processing
|
41
55
|
end
|
42
56
|
|
43
57
|
def force_syncing(node, serve_stale_reads)
|
44
58
|
start_processing
|
45
59
|
node.redis.force_sync_with_master(serve_stale_reads)
|
46
|
-
|
60
|
+
notify_state(node, :syncing)
|
47
61
|
stop_processing
|
48
62
|
end
|
49
63
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis_failover
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-04-
|
12
|
+
date: 2012-04-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
16
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,15 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
25
30
|
- !ruby/object:Gem::Dependency
|
26
31
|
name: redis-namespace
|
27
|
-
requirement:
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
28
33
|
none: false
|
29
34
|
requirements:
|
30
35
|
- - ! '>='
|
@@ -32,32 +37,53 @@ dependencies:
|
|
32
37
|
version: '0'
|
33
38
|
type: :runtime
|
34
39
|
prerelease: false
|
35
|
-
version_requirements:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
36
46
|
- !ruby/object:Gem::Dependency
|
37
47
|
name: multi_json
|
38
|
-
requirement:
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
39
49
|
none: false
|
40
50
|
requirements:
|
41
51
|
- - ! '>='
|
42
52
|
- !ruby/object:Gem::Version
|
43
|
-
version: '0'
|
53
|
+
version: '1.0'
|
54
|
+
- - <
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '1.3'
|
44
57
|
type: :runtime
|
45
58
|
prerelease: false
|
46
|
-
version_requirements:
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ! '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '1.0'
|
65
|
+
- - <
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '1.3'
|
47
68
|
- !ruby/object:Gem::Dependency
|
48
69
|
name: zk
|
49
|
-
requirement:
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
50
71
|
none: false
|
51
72
|
requirements:
|
52
|
-
- -
|
73
|
+
- - ~>
|
53
74
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
75
|
+
version: 0.8.8
|
55
76
|
type: :runtime
|
56
77
|
prerelease: false
|
57
|
-
version_requirements:
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ~>
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: 0.8.8
|
58
84
|
- !ruby/object:Gem::Dependency
|
59
85
|
name: rake
|
60
|
-
requirement:
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
61
87
|
none: false
|
62
88
|
requirements:
|
63
89
|
- - ! '>='
|
@@ -65,10 +91,15 @@ dependencies:
|
|
65
91
|
version: '0'
|
66
92
|
type: :development
|
67
93
|
prerelease: false
|
68
|
-
version_requirements:
|
94
|
+
version_requirements: !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ! '>='
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
69
100
|
- !ruby/object:Gem::Dependency
|
70
101
|
name: rspec
|
71
|
-
requirement:
|
102
|
+
requirement: !ruby/object:Gem::Requirement
|
72
103
|
none: false
|
73
104
|
requirements:
|
74
105
|
- - ! '>='
|
@@ -76,7 +107,12 @@ dependencies:
|
|
76
107
|
version: '0'
|
77
108
|
type: :development
|
78
109
|
prerelease: false
|
79
|
-
version_requirements:
|
110
|
+
version_requirements: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ! '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
80
116
|
description: Redis Failover is a ZooKeeper-based automatic master/slave failover solution
|
81
117
|
for Ruby
|
82
118
|
email:
|
@@ -129,7 +165,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
129
165
|
version: '0'
|
130
166
|
segments:
|
131
167
|
- 0
|
132
|
-
hash:
|
168
|
+
hash: 2836022562258381397
|
133
169
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
170
|
none: false
|
135
171
|
requirements:
|
@@ -138,10 +174,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
138
174
|
version: '0'
|
139
175
|
segments:
|
140
176
|
- 0
|
141
|
-
hash:
|
177
|
+
hash: 2836022562258381397
|
142
178
|
requirements: []
|
143
179
|
rubyforge_project:
|
144
|
-
rubygems_version: 1.8.
|
180
|
+
rubygems_version: 1.8.23
|
145
181
|
signing_key:
|
146
182
|
specification_version: 3
|
147
183
|
summary: Redis Failover is a ZooKeeper-based automatic master/slave failover solution
|