nogara-redis_failover 0.8.9
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/.gitignore +19 -0
- data/.travis.yml +5 -0
- data/.yardopts +6 -0
- data/Changes.md +116 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +190 -0
- data/Rakefile +9 -0
- data/bin/redis_node_manager +7 -0
- data/examples/config.yml +14 -0
- data/examples/multiple_environments_config.yml +15 -0
- data/lib/redis_failover.rb +22 -0
- data/lib/redis_failover/cli.rb +119 -0
- data/lib/redis_failover/client.rb +441 -0
- data/lib/redis_failover/errors.rb +47 -0
- data/lib/redis_failover/manual_failover.rb +40 -0
- data/lib/redis_failover/node.rb +190 -0
- data/lib/redis_failover/node_manager.rb +352 -0
- data/lib/redis_failover/node_watcher.rb +79 -0
- data/lib/redis_failover/runner.rb +28 -0
- data/lib/redis_failover/util.rb +83 -0
- data/lib/redis_failover/version.rb +3 -0
- data/misc/redis_failover.png +0 -0
- data/redis_failover.gemspec +26 -0
- data/spec/cli_spec.rb +75 -0
- data/spec/client_spec.rb +100 -0
- data/spec/node_manager_spec.rb +112 -0
- data/spec/node_spec.rb +84 -0
- data/spec/node_watcher_spec.rb +58 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/config/multiple_environments.yml +15 -0
- data/spec/support/config/multiple_environments_with_chroot.yml +17 -0
- data/spec/support/config/single_environment.yml +7 -0
- data/spec/support/config/single_environment_with_chroot.yml +8 -0
- data/spec/support/node_manager_stub.rb +65 -0
- data/spec/support/redis_stub.rb +105 -0
- data/spec/util_spec.rb +21 -0
- metadata +210 -0
@@ -0,0 +1,119 @@
|
|
1
|
+
module RedisFailover
|
2
|
+
# Parses server command-line arguments.
|
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
|
8
|
+
def self.parse(source)
|
9
|
+
options = {}
|
10
|
+
parser = OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: redis_node_manager [OPTIONS]"
|
12
|
+
opts.separator ""
|
13
|
+
opts.separator "Specific options:"
|
14
|
+
|
15
|
+
opts.on('-n', '--nodes NODES',
|
16
|
+
'Comma-separated redis host:port pairs') do |nodes|
|
17
|
+
options[:nodes] = nodes
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on('-z', '--zkservers SERVERS',
|
21
|
+
'Comma-separated ZooKeeper host:port pairs') do |servers|
|
22
|
+
options[:zkservers] = servers
|
23
|
+
end
|
24
|
+
|
25
|
+
opts.on('-p', '--password PASSWORD', 'Redis password') do |password|
|
26
|
+
options[:password] = password
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on('--znode-path PATH',
|
30
|
+
'Znode path override for storing redis server list') do |path|
|
31
|
+
options[:znode_path] = path
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on('--max-failures COUNT',
|
35
|
+
'Max failures before manager marks node unavailable') do |max|
|
36
|
+
options[:max_failures] = Integer(max)
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on('-C', '--config PATH', 'Path to YAML config file') do |file|
|
40
|
+
options[:config_file] = file
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on('--with-chroot ROOT', 'Path to ZooKeepers chroot') do |chroot|
|
44
|
+
options[:chroot] = chroot
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on('-E', '--environment ENV', 'Config environment to use') do |config_env|
|
48
|
+
options[:config_environment] = config_env
|
49
|
+
end
|
50
|
+
|
51
|
+
opts.on('-h', '--help', 'Display all options') do
|
52
|
+
puts opts
|
53
|
+
exit
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
parser.parse(source)
|
58
|
+
if config_file = options[:config_file]
|
59
|
+
options = from_file(config_file, options[:config_environment])
|
60
|
+
end
|
61
|
+
|
62
|
+
if required_options_missing?(options)
|
63
|
+
puts parser
|
64
|
+
exit
|
65
|
+
end
|
66
|
+
|
67
|
+
prepare(options)
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Boolean] true if required options missing, false otherwise
|
71
|
+
def self.required_options_missing?(options)
|
72
|
+
return true if options.empty?
|
73
|
+
return true unless options.values_at(:nodes, :zkservers).all?
|
74
|
+
false
|
75
|
+
end
|
76
|
+
|
77
|
+
# Parses options from a YAML file.
|
78
|
+
#
|
79
|
+
# @param [String] file the filename
|
80
|
+
# @params [String] env the environment
|
81
|
+
# @return [Hash] the parsed options
|
82
|
+
def self.from_file(file, env = nil)
|
83
|
+
unless File.exists?(file)
|
84
|
+
raise ArgumentError, "File #{file} can't be found"
|
85
|
+
end
|
86
|
+
options = YAML.load_file(file)
|
87
|
+
|
88
|
+
if env
|
89
|
+
options = options.fetch(env.to_sym) do
|
90
|
+
raise ArgumentError, "Environment #{env} can't be found in config"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
options[:nodes] = options[:nodes].join(',')
|
95
|
+
options[:zkservers] = options[:zkservers].join(',')
|
96
|
+
|
97
|
+
options
|
98
|
+
end
|
99
|
+
|
100
|
+
# Prepares the options for the rest of the system.
|
101
|
+
#
|
102
|
+
# @param [Hash] options the options to prepare
|
103
|
+
# @return [Hash] the prepared options
|
104
|
+
def self.prepare(options)
|
105
|
+
options.each_value { |v| v.strip! if v.respond_to?(:strip!) }
|
106
|
+
# turns 'host1:port,host2:port' => [{:host => host, :port => port}, ...]
|
107
|
+
options[:nodes] = options[:nodes].split(',').map do |node|
|
108
|
+
Hash[[:host, :port].zip(node.split(':'))]
|
109
|
+
end
|
110
|
+
|
111
|
+
# assume password is same for all redis nodes
|
112
|
+
if password = options[:password]
|
113
|
+
options[:nodes].each { |opts| opts.update(:password => password) }
|
114
|
+
end
|
115
|
+
|
116
|
+
options
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,441 @@
|
|
1
|
+
module RedisFailover
|
2
|
+
# Redis failover-aware client. RedisFailover::Client is a wrapper over a set
|
3
|
+
# of underlying redis clients, which means all normal redis operations can be
|
4
|
+
# performed on an instance of this class. The class only requires a set of
|
5
|
+
# ZooKeeper server addresses to function properly. The client will automatically
|
6
|
+
# retry failed operations, and handle failover to a new master. The client
|
7
|
+
# registers and listens for watcher events from the Node Manager. When these
|
8
|
+
# events are received, the client fetches the latest set of redis nodes from
|
9
|
+
# ZooKeeper and rebuilds its internal Redis clients appropriately.
|
10
|
+
# RedisFailover::Client also directs write operations to the master, and all
|
11
|
+
# read operations to the slaves.
|
12
|
+
#
|
13
|
+
# @example Usage
|
14
|
+
# zk_servers = 'localhost:2181,localhost:2182,localhost:2183'
|
15
|
+
# client = RedisFailover::Client.new(:zkservers => zk_servers)
|
16
|
+
# client.set('foo', 1) # will be directed to master
|
17
|
+
# client.get('foo') # will be directed to a slave
|
18
|
+
#
|
19
|
+
class Client
|
20
|
+
include Util
|
21
|
+
|
22
|
+
# Maximum allowed elapsed time between notifications from the Node Manager.
|
23
|
+
# When this timeout is reached, the client will raise a NoNodeManagerError
|
24
|
+
# and purge its internal redis clients.
|
25
|
+
ZNODE_UPDATE_TIMEOUT = 9
|
26
|
+
|
27
|
+
# Amount of time to sleep before retrying a failed operation.
|
28
|
+
RETRY_WAIT_TIME = 3
|
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
|
+
# Performance optimization: to avoid unnecessary method_missing calls,
|
77
|
+
# we proactively define methods that dispatch to the underlying redis
|
78
|
+
# calls.
|
79
|
+
Redis.public_instance_methods(false).each do |method|
|
80
|
+
define_method(method) do |*args, &block|
|
81
|
+
dispatch(method, *args, &block)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Creates a new failover redis client.
|
86
|
+
#
|
87
|
+
# @param [Hash] options the options used to initialize the client instance
|
88
|
+
# @option options [String] :zkservers comma-separated ZooKeeper host:port
|
89
|
+
# @option options [String] :znode_path znode path override for redis nodes
|
90
|
+
# @option options [String] :password password for redis nodes
|
91
|
+
# @option options [String] :db database to use for redis nodes
|
92
|
+
# @option options [String] :namespace namespace for redis nodes
|
93
|
+
# @option options [Logger] :logger logger override
|
94
|
+
# @option options [Boolean] :retry_failure indicates if failures are retried
|
95
|
+
# @option options [Integer] :max_retries max retries for a failure
|
96
|
+
# @return [RedisFailover::Client]
|
97
|
+
def initialize(options = {})
|
98
|
+
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
|
+
@master = nil
|
107
|
+
@slaves = []
|
108
|
+
@node_addresses = {}
|
109
|
+
@lock = Monitor.new
|
110
|
+
@current_client_key = "current-client-#{self.object_id}"
|
111
|
+
setup_zk
|
112
|
+
build_clients
|
113
|
+
end
|
114
|
+
|
115
|
+
# Dispatches redis operations to master/slaves.
|
116
|
+
def method_missing(method, *args, &block)
|
117
|
+
if redis_operation?(method)
|
118
|
+
dispatch(method, *args, &block)
|
119
|
+
else
|
120
|
+
super
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Determines whether or not an unknown method can be handled.
|
125
|
+
#
|
126
|
+
# @param [Symbol] method the method to check
|
127
|
+
# @param [Boolean] include_private determines if private methods are checked
|
128
|
+
# @return [Boolean] indicates if the method can be handled
|
129
|
+
def respond_to_missing?(method, include_private)
|
130
|
+
redis_operation?(method) || super
|
131
|
+
end
|
132
|
+
|
133
|
+
# @return [String] a string representation of the client
|
134
|
+
def inspect
|
135
|
+
"#<RedisFailover::Client (master: #{master_name}, slaves: #{slave_names})>"
|
136
|
+
end
|
137
|
+
alias_method :to_s, :inspect
|
138
|
+
|
139
|
+
# Force a manual failover to a new server. A specific server can be specified
|
140
|
+
# via options. If no options are passed, a random slave will be selected as
|
141
|
+
# the candidate for the new master.
|
142
|
+
#
|
143
|
+
# @param [Hash] options the options used for manual failover
|
144
|
+
# @option options [String] :host the host of the failover candidate
|
145
|
+
# @option options [String] :port the port of the failover candidate
|
146
|
+
def manual_failover(options = {})
|
147
|
+
ManualFailover.new(@zk, options).perform
|
148
|
+
self
|
149
|
+
end
|
150
|
+
|
151
|
+
# Gracefully performs a shutdown of this client. This method is
|
152
|
+
# mostly useful when the client is used in a forking environment.
|
153
|
+
# When a fork occurs, you can call this method in an after_fork hook,
|
154
|
+
# and then create a new instance of the client. The underlying
|
155
|
+
# ZooKeeper client and redis clients will be closed.
|
156
|
+
def shutdown
|
157
|
+
@zk.close! if @zk
|
158
|
+
@zk = nil
|
159
|
+
purge_clients
|
160
|
+
end
|
161
|
+
|
162
|
+
# Reconnect will first perform a shutdown of the underlying redis clients.
|
163
|
+
# Next, it attempts to reopen the ZooKeeper client and re-create the redis
|
164
|
+
# clients after it fetches the most up-to-date list from ZooKeeper.
|
165
|
+
def reconnect
|
166
|
+
purge_clients
|
167
|
+
@zk ? @zk.reopen : setup_zk
|
168
|
+
build_clients
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
# Sets up the underlying ZooKeeper connection.
|
174
|
+
def setup_zk
|
175
|
+
@zk = ZK.new(@zkservers)
|
176
|
+
@zk.watcher.register(@znode) { |event| handle_zk_event(event) }
|
177
|
+
@zk.on_expired_session { purge_clients }
|
178
|
+
@zk.on_connected { @zk.stat(@znode, :watch => true) }
|
179
|
+
@zk.stat(@znode, :watch => true)
|
180
|
+
update_znode_timestamp
|
181
|
+
end
|
182
|
+
|
183
|
+
# Handles a ZK event.
|
184
|
+
#
|
185
|
+
# @param [ZK::Event] event the ZK event to handle
|
186
|
+
def handle_zk_event(event)
|
187
|
+
update_znode_timestamp
|
188
|
+
if event.node_created? || event.node_changed?
|
189
|
+
build_clients
|
190
|
+
elsif event.node_deleted?
|
191
|
+
purge_clients
|
192
|
+
@zk.stat(@znode, :watch => true)
|
193
|
+
else
|
194
|
+
logger.error("Unknown ZK node event: #{event.inspect}")
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Determines if a method is a known redis operation.
|
199
|
+
#
|
200
|
+
# @param [Symbol] method the method to check
|
201
|
+
# @return [Boolean] true if redis operation, false otherwise
|
202
|
+
def redis_operation?(method)
|
203
|
+
Redis.public_instance_methods(false).include?(method)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Dispatches a redis operation to a master or slave.
|
207
|
+
#
|
208
|
+
# @param [Symbol] method the method to dispatch
|
209
|
+
# @param [Array] args the arguments to pass to the method
|
210
|
+
# @param [Proc] block an optional block to pass to the method
|
211
|
+
# @return [Object] the result of dispatching the command
|
212
|
+
def dispatch(method, *args, &block)
|
213
|
+
unless recently_heard_from_node_manager?
|
214
|
+
build_clients
|
215
|
+
end
|
216
|
+
|
217
|
+
verify_supported!(method)
|
218
|
+
tries = 0
|
219
|
+
begin
|
220
|
+
client_for(method).send(method, *args, &block)
|
221
|
+
rescue *CONNECTIVITY_ERRORS => ex
|
222
|
+
logger.error("Error while handling `#{method}` - #{ex.inspect}")
|
223
|
+
logger.error(ex.backtrace.join("\n"))
|
224
|
+
|
225
|
+
if tries < @max_retries
|
226
|
+
tries += 1
|
227
|
+
build_clients
|
228
|
+
sleep(RETRY_WAIT_TIME)
|
229
|
+
retry
|
230
|
+
end
|
231
|
+
raise
|
232
|
+
ensure
|
233
|
+
free_client
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Returns the currently known master.
|
238
|
+
#
|
239
|
+
# @return [Redis] the Redis client for the current master
|
240
|
+
# @raise [NoMasterError] if no master is available
|
241
|
+
def master
|
242
|
+
if master = @lock.synchronize { @master }
|
243
|
+
verify_role!(master, :master)
|
244
|
+
return master
|
245
|
+
end
|
246
|
+
raise NoMasterError
|
247
|
+
end
|
248
|
+
|
249
|
+
# Returns a random slave from the list of known slaves.
|
250
|
+
#
|
251
|
+
# @note If there are no slaves, the master is returned.
|
252
|
+
# @return [Redis] the Redis client for the slave or master
|
253
|
+
# @raise [NoMasterError] if no master fallback is available
|
254
|
+
def slave
|
255
|
+
# pick a slave, if none available fallback to master
|
256
|
+
if slave = @lock.synchronize { @slaves.sample }
|
257
|
+
verify_role!(slave, :slave)
|
258
|
+
return slave
|
259
|
+
end
|
260
|
+
master
|
261
|
+
end
|
262
|
+
|
263
|
+
# Builds the Redis clients for the currently known master/slaves.
|
264
|
+
# The current master/slaves are fetched via ZooKeeper.
|
265
|
+
def build_clients
|
266
|
+
@lock.synchronize do
|
267
|
+
begin
|
268
|
+
nodes = fetch_nodes
|
269
|
+
return unless nodes_changed?(nodes)
|
270
|
+
|
271
|
+
purge_clients
|
272
|
+
logger.info("Building new clients for nodes #{nodes}")
|
273
|
+
new_master = new_clients_for(nodes[:master]).first if nodes[:master]
|
274
|
+
new_slaves = new_clients_for(*nodes[:slaves])
|
275
|
+
@master = new_master
|
276
|
+
@slaves = new_slaves
|
277
|
+
rescue
|
278
|
+
purge_clients
|
279
|
+
raise
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Fetches the known redis nodes from ZooKeeper.
|
285
|
+
#
|
286
|
+
# @return [Hash] the known master/slave redis servers
|
287
|
+
def fetch_nodes
|
288
|
+
data = @zk.get(@znode, :watch => true).first
|
289
|
+
nodes = symbolize_keys(decode(data))
|
290
|
+
logger.debug("Fetched nodes: #{nodes}")
|
291
|
+
|
292
|
+
nodes
|
293
|
+
rescue Zookeeper::Exceptions::InheritedConnectionError => ex
|
294
|
+
logger.debug { "Caught #{ex.class} '#{ex.message}' - reopening ZK client" }
|
295
|
+
@zk.reopen
|
296
|
+
retry
|
297
|
+
end
|
298
|
+
|
299
|
+
# Builds new Redis clients for the specified nodes.
|
300
|
+
#
|
301
|
+
# @param [Array<String>] nodes the array of redis host:port pairs
|
302
|
+
# @return [Array<Redis>] the array of corresponding Redis clients
|
303
|
+
def new_clients_for(*nodes)
|
304
|
+
nodes.map do |node|
|
305
|
+
host, port = node.split(':')
|
306
|
+
opts = {:host => host, :port => port}
|
307
|
+
opts.update(:db => @db) if @db
|
308
|
+
opts.update(:password => @password) if @password
|
309
|
+
client = Redis.new(opts)
|
310
|
+
if @namespace
|
311
|
+
client = Redis::Namespace.new(@namespace, :redis => client)
|
312
|
+
end
|
313
|
+
@node_addresses[client] = node
|
314
|
+
client
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
# @return [String] a friendly name for current master
|
319
|
+
def master_name
|
320
|
+
address_for(@master) || 'none'
|
321
|
+
end
|
322
|
+
|
323
|
+
# @return [Array<String>] friendly names for current slaves
|
324
|
+
def slave_names
|
325
|
+
return 'none' if @slaves.empty?
|
326
|
+
addresses_for(@slaves).join(', ')
|
327
|
+
end
|
328
|
+
|
329
|
+
# Verifies the actual role for a redis node.
|
330
|
+
#
|
331
|
+
# @param [Redis] node the redis node to check
|
332
|
+
# @param [Symbol] role the role to verify
|
333
|
+
# @return [Symbol] the verified role
|
334
|
+
# @raise [InvalidNodeRoleError] if the role is invalid
|
335
|
+
def verify_role!(node, role)
|
336
|
+
current_role = node.info['role']
|
337
|
+
if current_role.to_sym != role
|
338
|
+
raise InvalidNodeRoleError.new(address_for(node), role, current_role)
|
339
|
+
end
|
340
|
+
role
|
341
|
+
end
|
342
|
+
|
343
|
+
# Ensures that the method is supported.
|
344
|
+
#
|
345
|
+
# @raise [UnsupportedOperationError] if the operation isn't supported
|
346
|
+
def verify_supported!(method)
|
347
|
+
if UNSUPPORTED_OPS.include?(method)
|
348
|
+
raise UnsupportedOperationError.new(method)
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
# Returns node addresses.
|
353
|
+
#
|
354
|
+
# @param [Array<Redis>] nodes the redis clients
|
355
|
+
# @return [Array<String>] the addresses for the nodes
|
356
|
+
def addresses_for(nodes)
|
357
|
+
nodes.map { |node| address_for(node) }
|
358
|
+
end
|
359
|
+
|
360
|
+
# Returns a node address.
|
361
|
+
#
|
362
|
+
# @param [Redis] node a redis client
|
363
|
+
# @return [String] the address for the node
|
364
|
+
def address_for(node)
|
365
|
+
return unless node
|
366
|
+
@node_addresses[node]
|
367
|
+
end
|
368
|
+
|
369
|
+
# Determines if the currently known redis servers is different
|
370
|
+
# from the nodes returned by ZooKeeper.
|
371
|
+
#
|
372
|
+
# @param [Array<String>] new_nodes the new redis nodes
|
373
|
+
# @return [Boolean] true if nodes are different, false otherwise
|
374
|
+
def nodes_changed?(new_nodes)
|
375
|
+
return true if address_for(@master) != new_nodes[:master]
|
376
|
+
return true if different?(addresses_for(@slaves), new_nodes[:slaves])
|
377
|
+
false
|
378
|
+
end
|
379
|
+
|
380
|
+
# Disconnects one or more redis clients.
|
381
|
+
#
|
382
|
+
# @param [Array<Redis>] redis_clients the redis clients
|
383
|
+
def disconnect(*redis_clients)
|
384
|
+
redis_clients.each do |conn|
|
385
|
+
if conn
|
386
|
+
begin
|
387
|
+
conn.client.disconnect
|
388
|
+
rescue
|
389
|
+
# best effort
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
# Disconnects current redis clients.
|
396
|
+
def purge_clients
|
397
|
+
@lock.synchronize do
|
398
|
+
logger.info("Purging current redis clients")
|
399
|
+
disconnect(@master, *@slaves)
|
400
|
+
@master = nil
|
401
|
+
@slaves = []
|
402
|
+
@node_addresses = {}
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
# Updates timestamp when an event is received by the Node Manager.
|
407
|
+
def update_znode_timestamp
|
408
|
+
@last_znode_timestamp = Time.now
|
409
|
+
end
|
410
|
+
|
411
|
+
# @return [Boolean] indicates if we recently heard from the Node Manager
|
412
|
+
def recently_heard_from_node_manager?
|
413
|
+
return false unless @last_znode_timestamp
|
414
|
+
Time.now - @last_znode_timestamp <= ZNODE_UPDATE_TIMEOUT
|
415
|
+
end
|
416
|
+
|
417
|
+
# Acquires a client to use for the specified operation.
|
418
|
+
#
|
419
|
+
# @param [Symbol] method the method for which to retrieve a client
|
420
|
+
# @return [Redis] a redis client to use
|
421
|
+
# @note
|
422
|
+
# This method stores a stack of clients used to handle the case
|
423
|
+
# where the same RedisFailover::Client instance is referenced by
|
424
|
+
# nested blocks (e.g., block passed to multi).
|
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
|
431
|
+
end
|
432
|
+
|
433
|
+
# Pops a client from the thread-local client stack.
|
434
|
+
def free_client
|
435
|
+
if stack = Thread.current[@current_client_key]
|
436
|
+
stack.pop
|
437
|
+
end
|
438
|
+
nil
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|