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 +10 -0
- data/README.md +10 -0
- data/lib/redis_failover/client.rb +83 -60
- data/lib/redis_failover/node_manager.rb +12 -12
- data/lib/redis_failover/util.rb +47 -1
- data/lib/redis_failover/version.rb +1 -1
- data/redis_failover.gemspec +1 -1
- metadata +12 -6
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
|
-
@
|
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
|
-
|
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
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
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(
|
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
|
-
|
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)
|
318
|
+
@zk.create(@znode, encode(current_nodes))
|
319
319
|
logger.info("Created ZooKeeper node #{@znode}")
|
320
320
|
end
|
321
321
|
rescue ZK::Exceptions::NodeExists
|
data/lib/redis_failover/util.rb
CHANGED
@@ -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?(
|
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.
|
data/redis_failover.gemspec
CHANGED
@@ -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', '
|
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.
|
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-
|
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: '
|
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: '
|
35
|
+
version: '4'
|
30
36
|
- !ruby/object:Gem::Dependency
|
31
37
|
name: redis-namespace
|
32
38
|
requirement: !ruby/object:Gem::Requirement
|