redis_failover 0.8.0 → 0.8.1

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/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ -m markdown
2
+ --readme README.md
3
+ -
4
+ Changes.md
5
+ LICENSE
6
+
data/Changes.md CHANGED
@@ -1,3 +1,9 @@
1
+ 0.8.1
2
+ -----------
3
+ - Added YARD documentation.
4
+ - Improve ZooKeeper client connection management.
5
+ - Upgrade to latest ZK gem stable release.
6
+
1
7
  0.8.0
2
8
  -----------
3
9
  - Added manual failover support (can be initiated via RedisFailover::Client#manual_failover)
data/README.md CHANGED
@@ -131,6 +131,10 @@ server passed to #manual_failover, or it will pick a random slave to become the
131
131
  client = RedisFailover::Client.new(:zkservers => 'localhost:2181,localhost:2182,localhost:2183')
132
132
  client.manual_failover(:host => 'localhost', :port => 2222)
133
133
 
134
+ ## Documentation
135
+
136
+ redis_failover uses YARD for its API documentation. Refer to the generated [API documentation](http://rubydoc.info/github/ryanlecompte/redis_failover/master/frames) for full coverage.
137
+
134
138
  ## Requirements
135
139
 
136
140
  - 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.
@@ -1,6 +1,10 @@
1
1
  module RedisFailover
2
2
  # Parses server command-line arguments.
3
3
  class CLI
4
+ # Parses the source of options.
5
+ #
6
+ # @param [Array] source the command-line options to parse
7
+ # @return [Hash] the parsed options
4
8
  def self.parse(source)
5
9
  options = {}
6
10
  parser = OptionParser.new do |opts|
@@ -55,12 +59,17 @@ module RedisFailover
55
59
  prepare(options)
56
60
  end
57
61
 
62
+ # @return [Boolean] true if required options missing, false otherwise
58
63
  def self.required_options_missing?(options)
59
64
  return true if options.empty?
60
65
  return true unless options.values_at(:nodes, :zkservers).all?
61
66
  false
62
67
  end
63
68
 
69
+ # Parses options from a YAML file.
70
+ #
71
+ # @param [String] file the filename
72
+ # @return [Hash] the parsed options
64
73
  def self.from_file(file)
65
74
  unless File.exists?(file)
66
75
  raise ArgumentError, "File #{file} can't be found"
@@ -72,6 +81,10 @@ module RedisFailover
72
81
  options
73
82
  end
74
83
 
84
+ # Prepares the options for the rest of the system.
85
+ #
86
+ # @param [Hash] options the options to prepare
87
+ # @return [Hash] the prepared options
75
88
  def self.prepare(options)
76
89
  options.each_value { |v| v.strip! if v.respond_to?(:strip!) }
77
90
  # turns 'host1:port,host2:port' => [{:host => host, :port => port}, ...]
@@ -8,8 +8,7 @@ module RedisFailover
8
8
  # Redis clients appropriately. RedisFailover::Client also directs write operations to the master,
9
9
  # and all read operations to the slaves.
10
10
  #
11
- # Examples
12
- #
11
+ # @example Usage
13
12
  # client = RedisFailover::Client.new(:zkservers => 'localhost:2181,localhost:2182,localhost:2183')
14
13
  # client.set('foo', 1) # will be directed to master
15
14
  # client.get('foo') # will be directed to a slave
@@ -86,15 +85,16 @@ module RedisFailover
86
85
 
87
86
  # Creates a new failover redis client.
88
87
  #
89
- # Options:
90
- # :zkservers - comma-separated ZooKeeper host:port pairs (required)
91
- # :znode_path - the Znode path override for redis server list (optional)
92
- # :password - password for redis nodes (optional)
93
- # :db - db to use for redis nodes (optional)
94
- # :namespace - namespace for redis nodes (optional)
95
- # :logger - logger override (optional)
96
- # :retry_failure - indicate if failures should be retried (default true)
97
- # :max_retries - max retries for a failure (default 3)
88
+ # @param [Hash] options the options used to initialize the client instance
89
+ # @option options [String] :zkservers comma-separated ZooKeeper host:port pairs (required)
90
+ # @option options [String] :znode_path znode path override for redis server list
91
+ # @option options [String] :password password for redis nodes
92
+ # @option options [String] :db database to use for redis nodes
93
+ # @option options [String] :namespace namespace for redis nodes
94
+ # @option options [Logger] :logger logger override
95
+ # @option options [Boolean] :retry_failure indicates if failures should be retried
96
+ # @option options [Integer] :max_retries max retries for a failure
97
+ # @return [RedisFailover::Client]
98
98
  def initialize(options = {})
99
99
  Util.logger = options[:logger] if options[:logger]
100
100
  @zkservers = options.fetch(:zkservers) { raise ArgumentError, ':zkservers required'}
@@ -108,7 +108,7 @@ module RedisFailover
108
108
  @slaves = []
109
109
  @queue = Queue.new
110
110
  @lock = Monitor.new
111
- start_zk
111
+ setup_zk
112
112
  build_clients
113
113
  end
114
114
 
@@ -121,10 +121,14 @@ module RedisFailover
121
121
  end
122
122
  end
123
123
 
124
- def respond_to?(method)
125
- redis_operation?(method) || super
124
+ # Determines whether or not an unknown method can be handled.
125
+ #
126
+ # @return [Boolean] indicates if the method can be handled
127
+ def respond_to_missing?(method)
128
+ redis_operation?(method)
126
129
  end
127
130
 
131
+ # @return [String] a string representation of the client
128
132
  def inspect
129
133
  "#<RedisFailover::Client (master: #{master_name}, slaves: #{slave_names})>"
130
134
  end
@@ -134,9 +138,9 @@ module RedisFailover
134
138
  # via options. If no options are passed, a random slave will be selected as
135
139
  # the candidate for the new master.
136
140
  #
137
- # Options:
138
- # :host - the host of the failover candidate
139
- # :port - the port of the failover candidate
141
+ # @param [Hash] options the options used for manual failover
142
+ # @option options [String] :host the host of the failover candidate
143
+ # @option options [String] :port the port of the failover candidate
140
144
  def manual_failover(options = {})
141
145
  Manual.failover(zk, options)
142
146
  self
@@ -144,73 +148,45 @@ module RedisFailover
144
148
 
145
149
  private
146
150
 
147
- def zk
148
- @lock.synchronize { @zk }
149
- end
150
-
151
- def start_zk
152
- @delivery_thread ||= Thread.new do
153
- while event = @queue.pop
154
- begin
155
- Proc === event ? event.call : handle_zk_event(event)
156
- rescue => ex
157
- logger.error("Error while handling event: #{ex.inspect}")
158
- logger.error(ex.backtrace.join("\n"))
159
- end
160
- end
161
- end
162
-
163
- reconnect_zk
164
- end
165
-
166
- def handle_session_established
167
- @lock.synchronize do
168
- @zk.watcher.register(@znode) do |event|
169
- @queue << event
170
- end
171
- @zk.on_expired_session do
172
- @queue << proc { reconnect_zk }
173
- end
174
- @zk.event_handler.register_state_handler(:connecting) do
175
- @queue << proc { handle_lost_connection }
176
- end
177
- @zk.on_connected do
178
- @zk.stat(@znode, :watch => true)
179
- end
180
- @zk.stat(@znode, :watch => true)
181
- end
151
+ # Sets up the underlying ZooKeeper connection.
152
+ def setup_zk
153
+ @zk = ZK.new(@zkservers)
154
+ @zk.watcher.register(@znode) { |event| handle_zk_event(event) }
155
+ @zk.on_expired_session { purge_clients }
156
+ @zk.on_connected { @zk.stat(@znode, :watch => true) }
157
+ @zk.stat(@znode, :watch => true)
158
+ update_znode_timestamp
182
159
  end
183
160
 
161
+ # Handles a ZK event.
162
+ #
163
+ # @param [ZK::Event] event the ZK event to handle
184
164
  def handle_zk_event(event)
185
165
  update_znode_timestamp
186
166
  if event.node_created? || event.node_changed?
187
167
  build_clients
188
168
  elsif event.node_deleted?
189
169
  purge_clients
190
- zk.stat(@znode, :watch => true)
170
+ @zk.stat(@znode, :watch => true)
191
171
  else
192
172
  logger.error("Unknown ZK node event: #{event.inspect}")
193
173
  end
194
174
  end
195
175
 
196
- def reconnect_zk
197
- @lock.synchronize do
198
- handle_lost_connection
199
- @zk.close! if @zk
200
- @zk = ZK.new(@zkservers)
201
- handle_session_established
202
- update_znode_timestamp
203
- end
204
- end
205
-
206
- def handle_lost_connection
207
- purge_clients
208
- end
209
-
176
+ # Determines if a method is a known redis operation.
177
+ #
178
+ # @param [Symbol] method the method to check
179
+ # @return [Boolean] true if redis operation, false otherwise
210
180
  def redis_operation?(method)
211
181
  Redis.public_instance_methods(false).include?(method)
212
182
  end
213
183
 
184
+ # Dispatches a redis operation to a master or slave.
185
+ #
186
+ # @param [Symbol] method the method to dispatch
187
+ # @param [Array] args the arguments to pass to the method
188
+ # @param [Proc] block an optional block to pass to the method
189
+ # @return [Object] the result of dispatching the command
214
190
  def dispatch(method, *args, &block)
215
191
  unless recently_heard_from_node_manager?
216
192
  @lock.synchronize do
@@ -243,6 +219,10 @@ module RedisFailover
243
219
  end
244
220
  end
245
221
 
222
+ # Returns the currently known master.
223
+ #
224
+ # @return [Redis] the Redis client for the current master
225
+ # @raise [NoMasterError] if no master is available
246
226
  def master
247
227
  if master = @lock.synchronize { @master }
248
228
  verify_role!(master, :master)
@@ -251,6 +231,11 @@ module RedisFailover
251
231
  raise NoMasterError
252
232
  end
253
233
 
234
+ # Returns a random slave from the list of known slaves.
235
+ #
236
+ # @note If there are no slaves, the master is returned.
237
+ # @return [Redis] the Redis client for the slave or master
238
+ # @raise [NoMasterError] if no master fallback is available
254
239
  def slave
255
240
  # pick a slave, if none available fallback to master
256
241
  if slave = @lock.synchronize { @slaves.sample }
@@ -260,6 +245,8 @@ module RedisFailover
260
245
  master
261
246
  end
262
247
 
248
+ # Builds the Redis clients for the currently known master/slaves.
249
+ # The current master/slaves are fetched via ZooKeeper.
263
250
  def build_clients
264
251
  @lock.synchronize do
265
252
  retried = false
@@ -289,14 +276,21 @@ module RedisFailover
289
276
  end
290
277
  end
291
278
 
279
+ # Fetches the known redis nodes from ZooKeeper.
280
+ #
281
+ # @return [Hash] the known master/slave redis servers
292
282
  def fetch_nodes
293
- data = zk.get(@znode, :watch => true).first
283
+ data = @zk.get(@znode, :watch => true).first
294
284
  nodes = symbolize_keys(decode(data))
295
285
  logger.debug("Fetched nodes: #{nodes}")
296
286
 
297
287
  nodes
298
288
  end
299
289
 
290
+ # Builds new Redis clients for the specified nodes.
291
+ #
292
+ # @param [Array<String>] nodes the array of redis host:port pairs
293
+ # @return [Array<Redis>] the array of corresponding Redis clients
300
294
  def new_clients_for(*nodes)
301
295
  nodes.map do |node|
302
296
  host, port = node.split(':')
@@ -311,15 +305,23 @@ module RedisFailover
311
305
  end
312
306
  end
313
307
 
308
+ # @return [String] a friendly name for current master
314
309
  def master_name
315
310
  address_for(@master) || 'none'
316
311
  end
317
312
 
313
+ # @return [Array<String>] friendly names for current slaves
318
314
  def slave_names
319
315
  return 'none' if @slaves.empty?
320
316
  addresses_for(@slaves).join(', ')
321
317
  end
322
318
 
319
+ # Verifies the actual role for a redis node.
320
+ #
321
+ # @param [Redis] node the redis node to check
322
+ # @param [Symbol] role the role to verify
323
+ # @return [Symbol] the verified role
324
+ # @raise [InvalidNodeRoleError] if the role is invalid
323
325
  def verify_role!(node, role)
324
326
  current_role = node.info['role']
325
327
  if current_role.to_sym != role
@@ -328,29 +330,48 @@ module RedisFailover
328
330
  role
329
331
  end
330
332
 
333
+ # Ensures that the method is supported.
334
+ #
335
+ # @raise [UnsupportedOperationError] if the operation isn't supported
331
336
  def verify_supported!(method)
332
337
  if UNSUPPORTED_OPS.include?(method)
333
338
  raise UnsupportedOperationError.new(method)
334
339
  end
335
340
  end
336
341
 
342
+ # Returns node addresses.
343
+ #
344
+ # @param [Array<Redis>] nodes the redis clients
345
+ # @return [Array<String>] the addresses for the nodes
337
346
  def addresses_for(nodes)
338
347
  nodes.map { |node| address_for(node) }
339
348
  end
340
349
 
350
+ # Returns a node address.
351
+ #
352
+ # @param [Redis] node a redis client
353
+ # @return [String] the address for the node
341
354
  def address_for(node)
342
355
  return unless node
343
356
  "#{node.client.host}:#{node.client.port}"
344
357
  end
345
358
 
359
+ # Determines if the currently known redis servers is different
360
+ # from the nodes returned by ZooKeeper.
361
+ #
362
+ # @param [Array<String>] new_nodes the new redis nodes
363
+ # @return [Boolean] true if nodes are different, false otherwise
346
364
  def nodes_changed?(new_nodes)
347
365
  return true if address_for(@master) != new_nodes[:master]
348
366
  return true if different?(addresses_for(@slaves), new_nodes[:slaves])
349
367
  false
350
368
  end
351
369
 
352
- def disconnect(*connections)
353
- connections.each do |conn|
370
+ # Disconnects one or more redis clients.
371
+ #
372
+ # @param [Array<Redis>] redis_clients the redis clients
373
+ def disconnect(*redis_clients)
374
+ redis_clients.each do |conn|
354
375
  if conn
355
376
  begin
356
377
  conn.client.disconnect
@@ -361,6 +382,7 @@ module RedisFailover
361
382
  end
362
383
  end
363
384
 
385
+ # Disconnects current redis clients and resets this client's view of the world.
364
386
  def purge_clients
365
387
  @lock.synchronize do
366
388
  logger.info("Purging current redis clients")
@@ -370,10 +392,12 @@ module RedisFailover
370
392
  end
371
393
  end
372
394
 
395
+ # Updates timestamp when an event is received by the Node Manager.
373
396
  def update_znode_timestamp
374
397
  @last_znode_timestamp = Time.now
375
398
  end
376
399
 
400
+ # @return [Boolean] indicates if we recently heard from the Node Manager
377
401
  def recently_heard_from_node_manager?
378
402
  return false unless @last_znode_timestamp
379
403
  Time.now - @last_znode_timestamp <= ZNODE_UPDATE_TIMEOUT
@@ -1,33 +1,36 @@
1
1
  module RedisFailover
2
+ # Base class for all RedisFailover errors.
2
3
  class Error < StandardError
3
- attr_reader :original
4
- def initialize(msg = nil, original = $!)
5
- super(msg)
6
- @original = original
7
- end
8
4
  end
9
5
 
6
+ # Raised when a node is specified incorrectly.
10
7
  class InvalidNodeError < Error
11
8
  end
12
9
 
10
+ # Raised when a node changes to an invalid/unknown state.
13
11
  class InvalidNodeStateError < Error
14
12
  def initialize(node, state)
15
13
  super("Invalid state change `#{state}` for node #{node}")
16
14
  end
17
15
  end
18
16
 
17
+ # Raised when a node is unavailable (i.e., unreachable via network).
19
18
  class NodeUnavailableError < Error
20
19
  def initialize(node)
21
20
  super("Node: #{node}")
22
21
  end
23
22
  end
24
23
 
24
+ # Raised when no master is currently available.
25
25
  class NoMasterError < Error
26
26
  end
27
27
 
28
+ # Raised when no slave is currently available.
28
29
  class NoSlaveError < Error
29
30
  end
30
31
 
32
+ # Raised when a redis server is no longer using the same role
33
+ # as previously assumed.
31
34
  class InvalidNodeRoleError < Error
32
35
  def initialize(node, assumed, actual)
33
36
  super("Invalid role detected for node #{node}, client thought " +
@@ -35,6 +38,7 @@ module RedisFailover
35
38
  end
36
39
  end
37
40
 
41
+ # Raised when an unsupported redis operation is performed.
38
42
  class UnsupportedOperationError < Error
39
43
  def initialize(operation)
40
44
  super("Operation `#{operation}` is currently unsupported")
@@ -9,6 +9,13 @@ module RedisFailover
9
9
  # Denotes that any slave can be used as a candidate for promotion.
10
10
  ANY_SLAVE = "ANY_SLAVE".freeze
11
11
 
12
+ # Performs a manual failover. If options is empty, a random slave will
13
+ # be used as a failover candidate.
14
+ #
15
+ # @param [ZK] zk the ZooKeeper client
16
+ # @param [Hash] options the options used for manual failover
17
+ # @option options [String] :host the host of the failover candidate
18
+ # @option options [String] :port the port of the failover candidate
12
19
  def failover(zk, options = {})
13
20
  create_path(zk)
14
21
  node = options.empty? ? ANY_SLAVE : "#{options[:host]}:#{options[:port]}"
@@ -17,6 +24,9 @@ module RedisFailover
17
24
 
18
25
  private
19
26
 
27
+ # Creates the znode path used for coordinating manual failovers.
28
+ #
29
+ # @param [ZK] zk the ZooKeeper cilent
20
30
  def create_path(zk)
21
31
  zk.create(ZNODE_PATH)
22
32
  rescue ZK::Exceptions::NodeExists
@@ -10,26 +10,44 @@ module RedisFailover
10
10
  # NodeUnavailableError will be raised.
11
11
  MAX_OP_WAIT_TIME = 5
12
12
 
13
- attr_reader :host, :port
13
+ # @return [String] the redis server host
14
+ attr_reader :host
14
15
 
16
+ # @return [Integer] the redis server port
17
+ attr_reader :port
18
+
19
+ # Creates a new instance.
20
+ #
21
+ # @param [Hash] options the options used to create the node
22
+ # @option options [String] :host the host of the redis server
23
+ # @option options [String] :port the port of the redis server
15
24
  def initialize(options = {})
16
25
  @host = options.fetch(:host) { raise InvalidNodeError, 'missing host'}
17
26
  @port = Integer(options[:port] || 6379)
18
27
  @password = options[:password]
19
28
  end
20
29
 
30
+ # @return [Boolean] true if this node is a master, false otherwise
21
31
  def master?
22
32
  role == 'master'
23
33
  end
24
34
 
35
+ # @return [Boolean] true if this node is a slave, false otherwise
25
36
  def slave?
26
37
  !master?
27
38
  end
28
39
 
40
+ # Determines if this node is a slave of the given master.
41
+ #
42
+ # @param [Node] master the master to check
43
+ # @return [Boolean] true if slave of master, false otherwise
29
44
  def slave_of?(master)
30
45
  current_master == master
31
46
  end
32
47
 
48
+ # Determines current master of this slave.
49
+ #
50
+ # @return [Node] the node representing the master of this slave
33
51
  def current_master
34
52
  info = fetch_info
35
53
  return unless info[:role] == 'slave'
@@ -47,12 +65,17 @@ module RedisFailover
47
65
  end
48
66
  end
49
67
 
68
+ # Wakes up this node by pushing a value to its internal
69
+ # queue used by #wait.
50
70
  def wakeup
51
71
  perform_operation do |redis|
52
72
  redis.lpush(wait_key, '1')
53
73
  end
54
74
  end
55
75
 
76
+ # Makes this node a slave of the given node.
77
+ #
78
+ # @param [Node] node the node of which to become a slave
56
79
  def make_slave!(node)
57
80
  perform_operation do |redis|
58
81
  unless slave_of?(node)
@@ -63,6 +86,7 @@ module RedisFailover
63
86
  end
64
87
  end
65
88
 
89
+ # Makes this node a master node.
66
90
  def make_master!
67
91
  perform_operation do |redis|
68
92
  unless master?
@@ -73,14 +97,20 @@ module RedisFailover
73
97
  end
74
98
  end
75
99
 
100
+ # @return [String] an inspect string for this node
76
101
  def inspect
77
102
  "<RedisFailover::Node #{to_s}>"
78
103
  end
79
104
 
105
+ # @return [String] a friendly string for this node
80
106
  def to_s
81
107
  "#{@host}:#{@port}"
82
108
  end
83
109
 
110
+ # Determines if this node is equal to another node.
111
+ #
112
+ # @param [Node] other the other node to compare
113
+ # @return [Boolean] true if equal, false otherwise
84
114
  def ==(other)
85
115
  return false unless Node === other
86
116
  return true if self.equal?(other)
@@ -88,10 +118,15 @@ module RedisFailover
88
118
  end
89
119
  alias_method :eql?, :==
90
120
 
121
+
122
+ # @return [Integer] a hash value for this node
91
123
  def hash
92
124
  to_s.hash
93
125
  end
94
126
 
127
+ # Fetches information/stats for this node.
128
+ #
129
+ # @return [Hash] the info for this node
95
130
  def fetch_info
96
131
  perform_operation do |redis|
97
132
  symbolize_keys(redis.info)
@@ -99,12 +134,14 @@ module RedisFailover
99
134
  end
100
135
  alias_method :ping, :fetch_info
101
136
 
137
+ # @return [Boolean] determines if this node prohibits stale reads
102
138
  def prohibits_stale_reads?
103
139
  perform_operation do |redis|
104
140
  redis.config('get', 'slave-serve-stale-data').last == 'no'
105
141
  end
106
142
  end
107
143
 
144
+ # @return [Boolean] determines if this node is syncing with its master
108
145
  def syncing_with_master?
109
146
  perform_operation do |redis|
110
147
  fetch_info[:master_sync_in_progress] == '1'
@@ -113,18 +150,25 @@ module RedisFailover
113
150
 
114
151
  private
115
152
 
153
+ # @return [String] the current role for this node
116
154
  def role
117
155
  fetch_info[:role]
118
156
  end
119
157
 
158
+ # @return [String] the name of the wait queue for this node
120
159
  def wait_key
121
160
  @wait_key ||= "_redis_failover_#{SecureRandom.hex(32)}"
122
161
  end
123
162
 
163
+ # @return [Redis] a new redis client instance for this node
124
164
  def new_client
125
165
  Redis.new(:host => @host, :password => @password, :port => @port)
126
166
  end
127
167
 
168
+ # Safely performs a redis operation within a given timeout window.
169
+ #
170
+ # @yield [Redis] the redis client to use for the operation
171
+ # @raise [NodeUnavailableError] if node is currently unreachable
128
172
  def perform_operation
129
173
  redis = nil
130
174
  Timeout.timeout(MAX_OP_WAIT_TIME) do
@@ -20,6 +20,14 @@ module RedisFailover
20
20
  # Number of seconds to wait before retrying bootstrap process.
21
21
  TIMEOUT = 5
22
22
 
23
+ # Creates a new instance.
24
+ #
25
+ # @param [Hash] options the options used to initialize the manager
26
+ # @option options [String] :zkservers comma-separated ZooKeeper host:port pairs (required)
27
+ # @option options [String] :znode_path znode path override for redis server list
28
+ # @option options [String] :password password for redis nodes
29
+ # @option options [Array<String>] :nodes the nodes to manage
30
+ # @option options [String] :max_failures the max failures for a particular node
23
31
  def initialize(options)
24
32
  logger.info("Redis Node Manager v#{VERSION} starting (#{RUBY_DESCRIPTION})")
25
33
  @options = options
@@ -28,6 +36,9 @@ module RedisFailover
28
36
  @mutex = Mutex.new
29
37
  end
30
38
 
39
+ # Starts the node manager.
40
+ #
41
+ # @note This is a blocking method, it does not return until the manager terminates.
31
42
  def start
32
43
  @queue = Queue.new
33
44
  @leader = false
@@ -49,10 +60,16 @@ module RedisFailover
49
60
  retry
50
61
  end
51
62
 
63
+ # Notifies the manager of a state change. Used primarily by {RedisFailover::NodeWatcher}
64
+ # to inform the manager of watched node states.
65
+ #
66
+ # @param [Node] node the node
67
+ # @param [Symbol] state the state
52
68
  def notify_state(node, state)
53
69
  @queue << [node, state]
54
70
  end
55
71
 
72
+ # Performs a graceful shutdown of the manager.
56
73
  def shutdown
57
74
  @queue.clear
58
75
  @queue << nil
@@ -62,6 +79,7 @@ module RedisFailover
62
79
 
63
80
  private
64
81
 
82
+ # Configures the ZooKeeper client.
65
83
  def setup_zk
66
84
  @zk.close! if @zk
67
85
  @zk = ZK.new(@options[:zkservers])
@@ -74,12 +92,11 @@ module RedisFailover
74
92
  end
75
93
  end
76
94
 
77
- @zk.on_connected do
78
- @zk.stat(@manual_znode, :watch => true)
79
- end
95
+ @zk.on_connected { @zk.stat(@manual_znode, :watch => true) }
80
96
  @zk.stat(@manual_znode, :watch => true)
81
97
  end
82
98
 
99
+ # Handles periodic state reports from {RedisFailover::NodeWatcher} instances.
83
100
  def handle_state_reports
84
101
  while state_report = @queue.pop
85
102
  begin
@@ -104,6 +121,9 @@ module RedisFailover
104
121
  end
105
122
  end
106
123
 
124
+ # Handles an unavailable node.
125
+ #
126
+ # @param [Node] node the unavailable node
107
127
  def handle_unavailable(node)
108
128
  # no-op if we already know about this node
109
129
  return if @unavailable.include?(node)
@@ -119,6 +139,9 @@ module RedisFailover
119
139
  end
120
140
  end
121
141
 
142
+ # Handles an available node.
143
+ #
144
+ # @param [Node] node the available node
122
145
  def handle_available(node)
123
146
  reconcile(node)
124
147
 
@@ -138,6 +161,9 @@ module RedisFailover
138
161
  @unavailable.delete(node)
139
162
  end
140
163
 
164
+ # Handles a node that is currently syncing.
165
+ #
166
+ # @param [Node] node the syncing node
141
167
  def handle_syncing(node)
142
168
  reconcile(node)
143
169
 
@@ -151,6 +177,9 @@ module RedisFailover
151
177
  handle_available(node)
152
178
  end
153
179
 
180
+ # Handles a manual failover request to the given node.
181
+ #
182
+ # @param [Node] node the candidate node for failover
154
183
  def handle_manual_failover(node)
155
184
  # no-op if node to be failed over is already master
156
185
  return if @master == node
@@ -162,6 +191,10 @@ module RedisFailover
162
191
  promote_new_master(node)
163
192
  end
164
193
 
194
+ # Promotes a new master.
195
+ #
196
+ # @param [Node] node the optional node to promote
197
+ # @note if no node is specified, a random slave will be used
165
198
  def promote_new_master(node = nil)
166
199
  delete_path
167
200
  @master = nil
@@ -182,6 +215,7 @@ module RedisFailover
182
215
  logger.info("Successfully promoted #{candidate} to master.")
183
216
  end
184
217
 
218
+ # Discovers the current master and slave nodes.
185
219
  def discover_nodes
186
220
  @unavailable = []
187
221
  nodes = @options[:nodes].map { |opts| Node.new(opts) }.uniq
@@ -194,6 +228,7 @@ module RedisFailover
194
228
  redirect_slaves_to(@master)
195
229
  end
196
230
 
231
+ # Spawns the {RedisFailover::NodeWatcher} instances for each managed node.
197
232
  def spawn_watchers
198
233
  @watchers = [@master, @slaves, @unavailable].flatten.map do |node|
199
234
  NodeWatcher.new(self, node, @options[:max_failures] || 3)
@@ -201,6 +236,10 @@ module RedisFailover
201
236
  @watchers.each(&:watch)
202
237
  end
203
238
 
239
+ # Searches for the master node.
240
+ #
241
+ # @param [Array<Node>] nodes the nodes to search
242
+ # @return [Node] the found master node, nil if not found
204
243
  def find_master(nodes)
205
244
  nodes.find do |node|
206
245
  begin
@@ -211,6 +250,9 @@ module RedisFailover
211
250
  end
212
251
  end
213
252
 
253
+ # Redirects all slaves to the specified node.
254
+ #
255
+ # @param [Node] node the node to which slaves are redirected
214
256
  def redirect_slaves_to(node)
215
257
  @slaves.dup.each do |slave|
216
258
  begin
@@ -222,6 +264,9 @@ module RedisFailover
222
264
  end
223
265
  end
224
266
 
267
+ # Forces a slave to be marked as unavailable.
268
+ #
269
+ # @param [Node] node the node to force as unavailable
225
270
  def force_unavailable_slave(node)
226
271
  @slaves.delete(node)
227
272
  @unavailable << node unless @unavailable.include?(node)
@@ -231,6 +276,8 @@ module RedisFailover
231
276
  # and completely lost its dynamically set run-time role by the node
232
277
  # manager. This method ensures that the node resumes its role as
233
278
  # determined by the manager.
279
+ #
280
+ # @param [Node] node the node to reconcile
234
281
  def reconcile(node)
235
282
  return if @master == node && node.master?
236
283
  return if @master && node.slave_of?(@master)
@@ -248,6 +295,7 @@ module RedisFailover
248
295
  end
249
296
  end
250
297
 
298
+ # @return [Hash] the set of current nodes grouped by category
251
299
  def current_nodes
252
300
  {
253
301
  :master => @master ? @master.to_s : nil,
@@ -256,6 +304,7 @@ module RedisFailover
256
304
  }
257
305
  end
258
306
 
307
+ # Deletes the znode path containing the redis nodes.
259
308
  def delete_path
260
309
  @zk.delete(@znode)
261
310
  logger.info("Deleted ZooKeeper node #{@znode}")
@@ -263,6 +312,7 @@ module RedisFailover
263
312
  logger.info("Tried to delete missing znode: #{ex.inspect}")
264
313
  end
265
314
 
315
+ # Creates the znode path containing the redis nodes.
266
316
  def create_path
267
317
  @zk.create(@znode, encode(current_nodes), :ephemeral => true)
268
318
  logger.info("Created ZooKeeper node #{@znode}")
@@ -270,16 +320,19 @@ module RedisFailover
270
320
  # best effort
271
321
  end
272
322
 
323
+ # Initializes the znode path containing the redis nodes.
273
324
  def initialize_path
274
325
  create_path
275
326
  write_state
276
327
  end
277
328
 
329
+ # Writes the current redis nodes state to the znode path.
278
330
  def write_state
279
331
  create_path
280
332
  @zk.set(@znode, encode(current_nodes))
281
333
  end
282
334
 
335
+ # Schedules a manual failover to a redis node.
283
336
  def schedule_manual_failover
284
337
  return unless @leader
285
338
  new_master = @zk.get(@manual_znode, :watch => true).first
@@ -8,6 +8,11 @@ module RedisFailover
8
8
  # Time to sleep before checking on the monitored node's status.
9
9
  WATCHER_SLEEP_TIME = 2
10
10
 
11
+ # Creates a new instance.
12
+ #
13
+ # @param [NodeManager] manager the node manager
14
+ # @param [Node] node the node to watch
15
+ # @param [Integer] max_failures the max failues before reporting node as down
11
16
  def initialize(manager, node, max_failures)
12
17
  @manager = manager
13
18
  @node = node
@@ -16,11 +21,16 @@ module RedisFailover
16
21
  @done = false
17
22
  end
18
23
 
24
+ # Starts the node watcher.
25
+ #
26
+ # @note this method returns immediately and causes monitoring to be
27
+ # performed in a new background thread
19
28
  def watch
20
29
  @monitor_thread ||= Thread.new { monitor_node }
21
30
  self
22
31
  end
23
32
 
33
+ # Performs a graceful shutdown of this watcher.
24
34
  def shutdown
25
35
  @done = true
26
36
  @node.wakeup
@@ -31,6 +41,8 @@ module RedisFailover
31
41
 
32
42
  private
33
43
 
44
+ # Periodically monitors the redis node and reports state changes to
45
+ # the {RedisFailover::NodeManager}.
34
46
  def monitor_node
35
47
  failures = 0
36
48
 
@@ -57,6 +69,9 @@ module RedisFailover
57
69
  end
58
70
  end
59
71
 
72
+ # Notifies the manager of a node's state.
73
+ #
74
+ # @param [Symbol] state the node's state
60
75
  def notify(state)
61
76
  @manager.notify_state(@node, state)
62
77
  end
@@ -1,6 +1,11 @@
1
1
  module RedisFailover
2
- # Runner is responsible for bootstrapping the redis Node Manager.
2
+ # Runner is responsible for bootstrapping the Node Manager.
3
3
  class Runner
4
+ # Launches the Node Manager in a background thread.
5
+ #
6
+ # @param [Array] options the command-line options
7
+ # @note this method blocks and does not return until the
8
+ # Node Manager is gracefully stopped
4
9
  def self.run(options)
5
10
  options = CLI.parse(options)
6
11
  @node_manager = NodeManager.new(options)
@@ -9,6 +14,7 @@ module RedisFailover
9
14
  node_manager_thread.join
10
15
  end
11
16
 
17
+ # Traps shutdown signals.
12
18
  def self.trap_signals
13
19
  [:INT, :TERM].each do |signal|
14
20
  trap(signal) do
@@ -18,14 +18,24 @@ module RedisFailover
18
18
  REDIS_ERRORS
19
19
  ].flatten.freeze
20
20
 
21
+ # Symbolizes the keys of the specified hash.
22
+ #
23
+ # @param [Hash] hash a hash for which keys should be symbolized
24
+ # @return [Hash] a new hash with symbolized keys
21
25
  def symbolize_keys(hash)
22
26
  Hash[hash.map { |k, v| [k.to_sym, v] }]
23
27
  end
24
28
 
29
+ # Determines if two arrays are different.
30
+ #
31
+ # @param [Array] ary_a the first array
32
+ # @param [Array] ary_b the second array
33
+ # @return [Boolean] true if arrays are different, false otherwise
25
34
  def different?(ary_a, ary_b)
26
35
  ((ary_a | ary_b) - (ary_a & ary_b)).size > 0
27
36
  end
28
37
 
38
+ # @return [Logger] the logger instance to use
29
39
  def self.logger
30
40
  @logger ||= begin
31
41
  logger = Logger.new(STDOUT)
@@ -37,18 +47,30 @@ module RedisFailover
37
47
  end
38
48
  end
39
49
 
50
+ # Sets a new logger to use.
51
+ #
52
+ # @param [Logger] logger a new logger to use
40
53
  def self.logger=(logger)
41
54
  @logger = logger
42
55
  end
43
56
 
57
+ # @return [Logger] the logger instance to use
44
58
  def logger
45
59
  Util.logger
46
60
  end
47
61
 
62
+ # Encodes the specified data in JSON format.
63
+ #
64
+ # @param [Object] data the data to encode
65
+ # @return [String] the JSON-encoded data
48
66
  def encode(data)
49
67
  MultiJson.encode(data)
50
68
  end
51
69
 
70
+ # Decodes the specified JSON data.
71
+ #
72
+ # @param [String] data the JSON data to decode
73
+ # @return [Object] the decoded data
52
74
  def decode(data)
53
75
  return unless data
54
76
  MultiJson.decode(data)
@@ -1,3 +1,3 @@
1
1
  module RedisFailover
2
- VERSION = "0.8.0"
2
+ VERSION = "0.8.1"
3
3
  end
@@ -18,8 +18,9 @@ Gem::Specification.new do |gem|
18
18
  gem.add_dependency('redis')
19
19
  gem.add_dependency('redis-namespace')
20
20
  gem.add_dependency('multi_json', '~> 1')
21
- gem.add_dependency('zk', '~> 1.0')
21
+ gem.add_dependency('zk', '~> 1.1')
22
22
 
23
23
  gem.add_development_dependency('rake')
24
24
  gem.add_development_dependency('rspec')
25
+ gem.add_development_dependency('yard')
25
26
  end
data/spec/client_spec.rb CHANGED
@@ -19,7 +19,8 @@ module RedisFailover
19
19
  }
20
20
  end
21
21
 
22
- def setup_zookeeper_client
22
+ def setup_zk
23
+ @zk = NullObject.new
23
24
  update_znode_timestamp
24
25
  end
25
26
  end
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.8.0
4
+ version: 0.8.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-02 00:00:00.000000000 Z
12
+ date: 2012-05-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
@@ -66,7 +66,7 @@ dependencies:
66
66
  requirements:
67
67
  - - ~>
68
68
  - !ruby/object:Gem::Version
69
- version: '1.0'
69
+ version: '1.1'
70
70
  type: :runtime
71
71
  prerelease: false
72
72
  version_requirements: !ruby/object:Gem::Requirement
@@ -74,7 +74,7 @@ dependencies:
74
74
  requirements:
75
75
  - - ~>
76
76
  - !ruby/object:Gem::Version
77
- version: '1.0'
77
+ version: '1.1'
78
78
  - !ruby/object:Gem::Dependency
79
79
  name: rake
80
80
  requirement: !ruby/object:Gem::Requirement
@@ -107,6 +107,22 @@ dependencies:
107
107
  - - ! '>='
108
108
  - !ruby/object:Gem::Version
109
109
  version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: yard
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
110
126
  description: Redis Failover is a ZooKeeper-based automatic master/slave failover solution
111
127
  for Ruby
112
128
  email:
@@ -118,6 +134,7 @@ extra_rdoc_files: []
118
134
  files:
119
135
  - .gitignore
120
136
  - .travis.yml
137
+ - .yardopts
121
138
  - Changes.md
122
139
  - Gemfile
123
140
  - LICENSE
@@ -160,7 +177,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
160
177
  version: '0'
161
178
  segments:
162
179
  - 0
163
- hash: -3688012640966712276
180
+ hash: -3105018591679682945
164
181
  required_rubygems_version: !ruby/object:Gem::Requirement
165
182
  none: false
166
183
  requirements:
@@ -169,7 +186,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
169
186
  version: '0'
170
187
  segments:
171
188
  - 0
172
- hash: -3688012640966712276
189
+ hash: -3105018591679682945
173
190
  requirements: []
174
191
  rubyforge_project:
175
192
  rubygems_version: 1.8.23
@@ -187,3 +204,4 @@ test_files:
187
204
  - spec/support/node_manager_stub.rb
188
205
  - spec/support/redis_stub.rb
189
206
  - spec/util_spec.rb
207
+ has_rdoc: