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 +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
|