nogara-redis_failover 0.8.9 → 0.8.10

Sign up to get free protection for your applications and to get access to all the features.
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