nogara-redis_failover 0.8.9 → 0.8.10

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/Changes.md CHANGED
@@ -1,3 +1,13 @@
1
+ HEAD
2
+ -----------
3
+ - Make Node Manager's lock path vary with its main znode. (Bira)
4
+ - Node Manager's znode for holding current list of redis nodes is no longer ephemeral. This is unnecessary since the current master should only be changed by redis_failover.
5
+ - Introduce :master_only option for RedisFailover::Client (disabled by default). This option configures the client to direct all read/write operations to the master.
6
+ - Introduce :safe_mode option (enabled by default). This option configures the client to purge its redis clients when a ZK session expires or when the client hasn't recently heard from the node manager.
7
+ - Introduce RedisFailover::Client#on_node_change callback notification for when the currently known list of master/slave redis nodes changes.
8
+ - Added #current_master and #current_slaves to RedisFailover::Client. This is useful for programmatically doing things based on the current master/slaves.
9
+ - redis_node_manager should start if no redis servers are available (#29)
10
+
1
11
  0.8.9
2
12
  -----------
3
13
  - Handle errors raised by redis 3.x client (tsilen)
data/README.md CHANGED
@@ -129,6 +129,16 @@ The full set of options that can be passed to RedisFailover::Client are:
129
129
  :logger - logger override (optional)
130
130
  :retry_failure - indicate if failures should be retried (default true)
131
131
  :max_retries - max retries for a failure (default 3)
132
+ :safe_mode - indicates if safe mode is used or not (default true)
133
+ :master_only - indicates if only redis master is used (default false)
134
+
135
+ The RedisFailover::Client also supports a custom callback that will be invoked whenever the list of redis clients changes. Example usage:
136
+
137
+ RedisFailover::Client.new(:zkservers => 'localhost:2181,localhost:2182,localhost:2183') do |client|
138
+ client.on_node_change do |master, slaves|
139
+ logger.info("Nodes changed! master: #{master}, slaves: #{slaves}")
140
+ end
141
+ end
132
142
 
133
143
  ## Manual Failover
134
144
 
@@ -27,52 +27,6 @@ module RedisFailover
27
27
  # Amount of time to sleep before retrying a failed operation.
28
28
  RETRY_WAIT_TIME = 3
29
29
 
30
- # Redis read operations that are automatically dispatched to slaves. Any
31
- # operation not listed here will be dispatched to the master.
32
- REDIS_READ_OPS = Set[
33
- :echo,
34
- :exists,
35
- :get,
36
- :getbit,
37
- :getrange,
38
- :hexists,
39
- :hget,
40
- :hgetall,
41
- :hkeys,
42
- :hlen,
43
- :hmget,
44
- :hvals,
45
- :keys,
46
- :lindex,
47
- :llen,
48
- :lrange,
49
- :mapped_hmget,
50
- :mapped_mget,
51
- :mget,
52
- :scard,
53
- :sdiff,
54
- :sinter,
55
- :sismember,
56
- :smembers,
57
- :srandmember,
58
- :strlen,
59
- :sunion,
60
- :type,
61
- :zcard,
62
- :zcount,
63
- :zrange,
64
- :zrangebyscore,
65
- :zrank,
66
- :zrevrange,
67
- :zrevrangebyscore,
68
- :zrevrank,
69
- :zscore
70
- ].freeze
71
-
72
- # Unsupported Redis operations. These don't make sense in a client
73
- # that abstracts the master/slave servers.
74
- UNSUPPORTED_OPS = Set[:select, :dbsize].freeze
75
-
76
30
  # Performance optimization: to avoid unnecessary method_missing calls,
77
31
  # we proactively define methods that dispatch to the underlying redis
78
32
  # calls.
@@ -93,25 +47,37 @@ module RedisFailover
93
47
  # @option options [Logger] :logger logger override
94
48
  # @option options [Boolean] :retry_failure indicates if failures are retried
95
49
  # @option options [Integer] :max_retries max retries for a failure
50
+ # @option options [Boolean] :safe_mode indicates if safe mode is used or not
51
+ # @option options [Boolean] :master_only indicates if only redis master is used
96
52
  # @return [RedisFailover::Client]
97
53
  def initialize(options = {})
98
54
  Util.logger = options[:logger] if options[:logger]
99
- @zkservers = options.fetch(:zkservers) { raise ArgumentError, ':zkservers required'}
100
- @znode = options[:znode_path] || Util::DEFAULT_ZNODE_PATH
101
- @namespace = options[:namespace]
102
- @password = options[:password]
103
- @db = options[:db]
104
- @retry = options[:retry_failure] || true
105
- @max_retries = @retry ? options.fetch(:max_retries, 3) : 0
106
55
  @master = nil
107
56
  @slaves = []
108
57
  @node_addresses = {}
109
58
  @lock = Monitor.new
110
59
  @current_client_key = "current-client-#{self.object_id}"
60
+ yield self if block_given?
61
+
62
+ parse_options(options)
111
63
  setup_zk
112
64
  build_clients
113
65
  end
114
66
 
67
+ # Specifies a callback to invoke when the current redis node list changes.
68
+ #
69
+ # @param [Proc] a callback with current master and slaves as arguments
70
+ #
71
+ # @example Usage
72
+ # RedisFailover::Client.new(:zkservers => zk_servers) do |client|
73
+ # client.on_node_change do |master, slaves|
74
+ # logger.info("Nodes changed! master: #{master}, slaves: #{slaves}")
75
+ # end
76
+ # end
77
+ def on_node_change(&callback)
78
+ @on_node_change = callback
79
+ end
80
+
115
81
  # Dispatches redis operations to master/slaves.
116
82
  def method_missing(method, *args, &block)
117
83
  if redis_operation?(method)
@@ -168,13 +134,31 @@ module RedisFailover
168
134
  build_clients
169
135
  end
170
136
 
137
+ # Retrieves the current redis master.
138
+ #
139
+ # @return [String] the host/port of the current master
140
+ def current_master
141
+ master = @lock.synchronize { @master }
142
+ address_for(master)
143
+ end
144
+
145
+ # Retrieves the current redis slaves.
146
+ #
147
+ # @return [Array<String>] an array of known slave host/port addresses
148
+ def current_slaves
149
+ slaves = @lock.synchronize { @slaves }
150
+ addresses_for(slaves)
151
+ end
152
+
171
153
  private
172
154
 
173
155
  # Sets up the underlying ZooKeeper connection.
174
156
  def setup_zk
175
157
  @zk = ZK.new(@zkservers)
176
158
  @zk.watcher.register(@znode) { |event| handle_zk_event(event) }
177
- @zk.on_expired_session { purge_clients }
159
+ if @safe_mode
160
+ @zk.on_expired_session { purge_clients }
161
+ end
178
162
  @zk.on_connected { @zk.stat(@znode, :watch => true) }
179
163
  @zk.stat(@znode, :watch => true)
180
164
  update_znode_timestamp
@@ -210,7 +194,7 @@ module RedisFailover
210
194
  # @param [Proc] block an optional block to pass to the method
211
195
  # @return [Object] the result of dispatching the command
212
196
  def dispatch(method, *args, &block)
213
- unless recently_heard_from_node_manager?
197
+ if @safe_mode && !recently_heard_from_node_manager?
214
198
  build_clients
215
199
  end
216
200
 
@@ -277,10 +261,26 @@ module RedisFailover
277
261
  rescue
278
262
  purge_clients
279
263
  raise
264
+ ensure
265
+ if should_notify?
266
+ @on_node_change.call(current_master, current_slaves)
267
+ @last_notified_master = current_master
268
+ @last_notified_slaves = current_slaves
269
+ end
280
270
  end
281
271
  end
282
272
  end
283
273
 
274
+ # Determines if the on_node_change callback should be invoked.
275
+ #
276
+ # @return [Boolean] true if callback should be invoked, false otherwise
277
+ def should_notify?
278
+ return false unless @on_node_change
279
+ return true if @last_notified_master != current_master
280
+ return true if different?(Array(@last_notified_slaves), current_slaves)
281
+ false
282
+ end
283
+
284
284
  # Fetches the known redis nodes from ZooKeeper.
285
285
  #
286
286
  # @return [Hash] the known master/slave redis servers
@@ -423,11 +423,19 @@ module RedisFailover
423
423
  # where the same RedisFailover::Client instance is referenced by
424
424
  # nested blocks (e.g., block passed to multi).
425
425
  def client_for(method)
426
- # stack = Thread.current[@current_client_key] ||= []
427
- # client = stack.last || (REDIS_READ_OPS.include?(method) ? slave : master)
428
- # stack << client
429
- # client
430
- master
426
+ stack = Thread.current[@current_client_key] ||= []
427
+ client = if stack.last
428
+ stack.last
429
+ elsif @master_only
430
+ master
431
+ elsif REDIS_READ_OPS.include?(method)
432
+ slave
433
+ else
434
+ master
435
+ end
436
+
437
+ stack << client
438
+ client
431
439
  end
432
440
 
433
441
  # Pops a client from the thread-local client stack.
@@ -437,5 +445,20 @@ module RedisFailover
437
445
  end
438
446
  nil
439
447
  end
448
+
449
+ # Parses the configuration operations.
450
+ #
451
+ # @param [Hash] options the configuration options
452
+ def parse_options(options)
453
+ @zkservers = options.fetch(:zkservers) { raise ArgumentError, ':zkservers required'}
454
+ @znode = options.fetch(:znode_path, Util::DEFAULT_ZNODE_PATH)
455
+ @namespace = options[:namespace]
456
+ @password = options[:password]
457
+ @db = options[:db]
458
+ @retry = options.fetch(:retry_failure, true)
459
+ @max_retries = @retry ? options.fetch(:max_retries, 3) : 0
460
+ @safe_mode = options.fetch(:safe_mode, true)
461
+ @master_only = options.fetch(:master_only, false)
462
+ end
440
463
  end
441
464
  end
@@ -9,14 +9,6 @@ module RedisFailover
9
9
  class NodeManager
10
10
  include Util
11
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
12
  # Number of seconds to wait before retrying bootstrap process.
21
13
  TIMEOUT = 5
22
14
 
@@ -34,6 +26,14 @@ module RedisFailover
34
26
  @znode = @options[:znode_path] || Util::DEFAULT_ZNODE_PATH
35
27
  @manual_znode = ManualFailover::ZNODE_PATH
36
28
  @mutex = Mutex.new
29
+
30
+ # Name for the znode that handles exclusive locking between multiple
31
+ # Node Manager processes. Whoever holds the lock will be considered
32
+ # the "master" Node Manager, and will be responsible for monitoring
33
+ # the redis nodes. When a Node Manager that holds the lock disappears
34
+ # or fails, another Node Manager process will grab the lock and
35
+ # become the
36
+ @lock_path = "#{@znode}_lock".freeze
37
37
  end
38
38
 
39
39
  # Starts the node manager.
@@ -44,7 +44,7 @@ module RedisFailover
44
44
  @leader = false
45
45
  setup_zk
46
46
  logger.info('Waiting to become master Node Manager ...')
47
- @zk.with_lock(LOCK_PATH) do
47
+ @zk.with_lock(@lock_path) do
48
48
  @leader = true
49
49
  logger.info('Acquired master Node Manager lock')
50
50
  discover_nodes
@@ -219,13 +219,13 @@ module RedisFailover
219
219
  def discover_nodes
220
220
  @unavailable = []
221
221
  nodes = @options[:nodes].map { |opts| Node.new(opts) }.uniq
222
- raise NoMasterError unless @master = find_master(nodes)
222
+ @master = find_master(nodes)
223
223
  @slaves = nodes - [@master]
224
224
  logger.info("Managing master (#{@master}) and slaves" +
225
225
  " (#{@slaves.map(&:to_s).join(', ')})")
226
226
 
227
227
  # ensure that slaves are correctly pointing to this master
228
- redirect_slaves_to(@master)
228
+ redirect_slaves_to(@master) if @master
229
229
  end
230
230
 
231
231
  # Spawns the {RedisFailover::NodeWatcher} instances for each managed node.
@@ -315,7 +315,7 @@ module RedisFailover
315
315
  # Creates the znode path containing the redis nodes.
316
316
  def create_path
317
317
  unless @zk.exists?(@znode)
318
- @zk.create(@znode, encode(current_nodes), :ephemeral => true)
318
+ @zk.create(@znode, encode(current_nodes))
319
319
  logger.info("Created ZooKeeper node #{@znode}")
320
320
  end
321
321
  rescue ZK::Exceptions::NodeExists
@@ -5,6 +5,52 @@ module RedisFailover
5
5
  module Util
6
6
  extend self
7
7
 
8
+ # Redis read operations that are automatically dispatched to slaves. Any
9
+ # operation not listed here will be dispatched to the master.
10
+ REDIS_READ_OPS = Set[
11
+ :echo,
12
+ :exists,
13
+ :get,
14
+ :getbit,
15
+ :getrange,
16
+ :hexists,
17
+ :hget,
18
+ :hgetall,
19
+ :hkeys,
20
+ :hlen,
21
+ :hmget,
22
+ :hvals,
23
+ :keys,
24
+ :lindex,
25
+ :llen,
26
+ :lrange,
27
+ :mapped_hmget,
28
+ :mapped_mget,
29
+ :mget,
30
+ :scard,
31
+ :sdiff,
32
+ :sinter,
33
+ :sismember,
34
+ :smembers,
35
+ :srandmember,
36
+ :strlen,
37
+ :sunion,
38
+ :type,
39
+ :zcard,
40
+ :zcount,
41
+ :zrange,
42
+ :zrangebyscore,
43
+ :zrank,
44
+ :zrevrange,
45
+ :zrevrangebyscore,
46
+ :zrevrank,
47
+ :zscore
48
+ ].freeze
49
+
50
+ # Unsupported Redis operations. These don't make sense in a client
51
+ # that abstracts the master/slave servers.
52
+ UNSUPPORTED_OPS = Set[:select, :dbsize].freeze
53
+
8
54
  # Default node in ZK that contains the current list of available redis nodes.
9
55
  DEFAULT_ZNODE_PATH = '/redis_failover_nodes'.freeze
10
56
 
@@ -12,7 +58,7 @@ module RedisFailover
12
58
  REDIS_ERRORS = Errno.constants.map { |c| Errno.const_get(c) }
13
59
 
14
60
  # Connectivity errors that the redis (>3.x) client raises.
15
- REDIS_ERRORS << Redis::BaseError if Redis.const_defined?("BaseError")
61
+ REDIS_ERRORS << Redis::BaseError if Redis.const_defined?('BaseError')
16
62
  REDIS_ERRORS.freeze
17
63
 
18
64
  # Full set of errors related to connectivity.
@@ -1,3 +1,3 @@
1
1
  module RedisFailover
2
- VERSION = '0.8.9'
2
+ VERSION = '0.8.10'
3
3
  end
@@ -15,7 +15,7 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = RedisFailover::VERSION
17
17
 
18
- gem.add_dependency('redis', '~> 3')
18
+ gem.add_dependency('redis', ['>= 2.2', '< 4'])
19
19
  gem.add_dependency('redis-namespace')
20
20
  gem.add_dependency('multi_json', '~> 1')
21
21
  gem.add_dependency('zk', '~> 1.6')
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nogara-redis_failover
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.9
4
+ version: 0.8.10
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,24 +9,30 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-07 00:00:00.000000000 Z
12
+ date: 2012-08-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
- - - ~>
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '2.2'
22
+ - - <
20
23
  - !ruby/object:Gem::Version
21
- version: '3'
24
+ version: '4'
22
25
  type: :runtime
23
26
  prerelease: false
24
27
  version_requirements: !ruby/object:Gem::Requirement
25
28
  none: false
26
29
  requirements:
27
- - - ~>
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '2.2'
33
+ - - <
28
34
  - !ruby/object:Gem::Version
29
- version: '3'
35
+ version: '4'
30
36
  - !ruby/object:Gem::Dependency
31
37
  name: redis-namespace
32
38
  requirement: !ruby/object:Gem::Requirement