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.
@@ -2,3 +2,4 @@ language: ruby
2
2
  rvm:
3
3
  - 1.9.2
4
4
  - 1.9.3
5
+ - jruby-19mode
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
- -p, --password password Redis password (optional)
60
- -n, --nodes redis nodes Comma-separated redis host:port pairs (required)
61
- -z zookeeper servers, Comma-separated ZooKeeper host:port pairs (required)
62
- --zkservers
63
- --znode-path path Znode path override for storing redis server list (optional)
64
- --max-failures count Max failures before manager marks node unavailable (default 3)
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 monitor the redis Node Manager daemon process with a tool like Monit to ensure that it is restarted
73
- in the case of a failure.
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).
@@ -1,4 +1,5 @@
1
1
  require 'zk'
2
+ require 'set'
2
3
  require 'redis'
3
4
  require 'thread'
4
5
  require 'logger'
@@ -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('-p', '--password password', 'Redis password (optional)') do |password|
12
- options[:password] = password.strip
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 ZooKeeper servers',
24
- 'Comma-separated ZooKeeper host:port pairs (required)') do |servers|
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('--znode-path path',
29
- 'Znode path override for storing redis server list (optional)') do |path|
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 count',
34
- 'Max failures before manager marks node unavailable (default 3)') do |max|
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 - comma-separated ZooKeeper host:port pairs (required)
70
- # :znode_path - the Znode path override for redis server list (optional)
71
- # :password - password for redis nodes (optional)
72
- # :namespace - namespace for redis nodes (optional)
73
- # :logger - logger override (optional)
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 - max retries for a failure (default 3)
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 - master: #{master_name}, slaves: #{slave_names})>"
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 operation `#{method}` - #{ex.inspect}")
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) && retry
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) && retry
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
- client = Redis.new(:host => host, :port => port, :password => @password)
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
@@ -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
- initialize_path
17
- spawn_watchers
18
- handle_state_changes
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 notify_state_change(node, state)
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 handle_state_changes
33
- while state_change = @queue.pop
59
+ def handle_state_reports
60
+ while state_report = @queue.pop
34
61
  begin
35
- node, state = state_change
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 while handling #{state_change.inspect}: #{ex.inspect}")
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
- # Watches a specific redis node for its availability.
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.notify_state_change(@node, state)
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
 
@@ -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,
@@ -1,3 +1,3 @@
1
1
  module RedisFailover
2
- VERSION = "0.5.4"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -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].freeze
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 <<-RUBY
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 client connection error - rebuilding client: #{ex.inspect}")
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(2) && retry
77
+ sleep(TIMEOUT)
78
+ retry
69
79
  end
70
80
 
71
81
  raise
@@ -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')
@@ -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(['-n host:port', '-p redis_pass', '--max-failures', '1'])
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
@@ -4,6 +4,10 @@ module RedisFailover
4
4
  describe NodeManager do
5
5
  let(:manager) { NodeManagerStub.new({}) }
6
6
 
7
+ before(:each) do
8
+ manager.discover_nodes
9
+ end
10
+
7
11
  describe '#nodes' do
8
12
  it 'returns current master and slave nodes' do
9
13
  manager.current_nodes.should == {
@@ -6,7 +6,7 @@ module RedisFailover
6
6
  @node_states = {}
7
7
  end
8
8
 
9
- def notify_state_change(node, state)
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.notify_state_change(node, :unavailable)
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.notify_state_change(node, :unavailable)
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)
@@ -5,6 +5,7 @@ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
5
5
 
6
6
  class NullObject
7
7
  def method_missing(method, *args, &block)
8
+ yield if block_given?
8
9
  self
9
10
  end
10
11
  end
@@ -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
- notify_state_change(node, :unavailable)
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
- notify_state_change(node, :available)
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
- notify_state_change(node, :syncing)
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.5.4
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-18 00:00:00.000000000 Z
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: &70362366935580 !ruby/object:Gem::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: *70362366935580
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: &70362366935160 !ruby/object:Gem::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: *70362366935160
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: &70362366934740 !ruby/object:Gem::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: *70362366934740
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: &70362366934320 !ruby/object:Gem::Requirement
70
+ requirement: !ruby/object:Gem::Requirement
50
71
  none: false
51
72
  requirements:
52
- - - ! '>='
73
+ - - ~>
53
74
  - !ruby/object:Gem::Version
54
- version: '0'
75
+ version: 0.8.8
55
76
  type: :runtime
56
77
  prerelease: false
57
- version_requirements: *70362366934320
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: &70362366933900 !ruby/object:Gem::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: *70362366933900
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: &70362366933480 !ruby/object:Gem::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: *70362366933480
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: 610999762978001666
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: 610999762978001666
177
+ hash: 2836022562258381397
142
178
  requirements: []
143
179
  rubyforge_project:
144
- rubygems_version: 1.8.16
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