redis 4.7.1 → 5.0.4

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/README.md +75 -161
  4. data/lib/redis/client.rb +92 -608
  5. data/lib/redis/commands/bitmaps.rb +4 -1
  6. data/lib/redis/commands/cluster.rb +1 -18
  7. data/lib/redis/commands/connection.rb +5 -10
  8. data/lib/redis/commands/geo.rb +3 -3
  9. data/lib/redis/commands/hashes.rb +8 -5
  10. data/lib/redis/commands/hyper_log_log.rb +1 -1
  11. data/lib/redis/commands/keys.rb +53 -27
  12. data/lib/redis/commands/lists.rb +19 -23
  13. data/lib/redis/commands/pubsub.rb +7 -25
  14. data/lib/redis/commands/server.rb +15 -15
  15. data/lib/redis/commands/sets.rb +43 -36
  16. data/lib/redis/commands/sorted_sets.rb +18 -12
  17. data/lib/redis/commands/streams.rb +12 -10
  18. data/lib/redis/commands/strings.rb +16 -15
  19. data/lib/redis/commands/transactions.rb +7 -31
  20. data/lib/redis/commands.rb +1 -8
  21. data/lib/redis/distributed.rb +100 -67
  22. data/lib/redis/errors.rb +14 -50
  23. data/lib/redis/hash_ring.rb +26 -26
  24. data/lib/redis/pipeline.rb +43 -222
  25. data/lib/redis/subscribe.rb +23 -15
  26. data/lib/redis/version.rb +1 -1
  27. data/lib/redis.rb +88 -182
  28. metadata +9 -53
  29. data/lib/redis/cluster/command.rb +0 -79
  30. data/lib/redis/cluster/command_loader.rb +0 -33
  31. data/lib/redis/cluster/key_slot_converter.rb +0 -72
  32. data/lib/redis/cluster/node.rb +0 -120
  33. data/lib/redis/cluster/node_key.rb +0 -31
  34. data/lib/redis/cluster/node_loader.rb +0 -34
  35. data/lib/redis/cluster/option.rb +0 -100
  36. data/lib/redis/cluster/slot.rb +0 -86
  37. data/lib/redis/cluster/slot_loader.rb +0 -46
  38. data/lib/redis/cluster.rb +0 -315
  39. data/lib/redis/connection/command_helper.rb +0 -41
  40. data/lib/redis/connection/hiredis.rb +0 -66
  41. data/lib/redis/connection/registry.rb +0 -13
  42. data/lib/redis/connection/ruby.rb +0 -437
  43. data/lib/redis/connection/synchrony.rb +0 -148
  44. data/lib/redis/connection.rb +0 -11
data/lib/redis/client.rb CHANGED
@@ -1,667 +1,151 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "socket"
4
- require "cgi"
5
- require "redis/errors"
3
+ require 'redis-client'
6
4
 
7
5
  class Redis
8
- class Client
9
- # Defaults are also used for converting string keys to symbols.
10
- DEFAULTS = {
11
- url: -> { ENV["REDIS_URL"] },
12
- scheme: "redis",
13
- host: "127.0.0.1",
14
- port: 6379,
15
- path: nil,
16
- read_timeout: nil,
17
- write_timeout: nil,
18
- connect_timeout: nil,
19
- timeout: 5.0,
20
- username: nil,
21
- password: nil,
22
- db: 0,
23
- driver: nil,
24
- id: nil,
25
- tcp_keepalive: 0,
26
- reconnect_attempts: 1,
27
- reconnect_delay: 0,
28
- reconnect_delay_max: 0.5,
29
- inherit_socket: false,
30
- logger: nil,
31
- sentinels: nil,
32
- role: nil
33
- }.freeze
6
+ class Client < ::RedisClient
7
+ ERROR_MAPPING = {
8
+ RedisClient::ConnectionError => Redis::ConnectionError,
9
+ RedisClient::CommandError => Redis::CommandError,
10
+ RedisClient::ReadTimeoutError => Redis::TimeoutError,
11
+ RedisClient::CannotConnectError => Redis::CannotConnectError,
12
+ RedisClient::AuthenticationError => Redis::CannotConnectError,
13
+ RedisClient::FailoverError => Redis::CannotConnectError,
14
+ RedisClient::PermissionError => Redis::PermissionError,
15
+ RedisClient::WrongTypeError => Redis::WrongTypeError,
16
+ RedisClient::ReadOnlyError => Redis::ReadOnlyError,
17
+ RedisClient::ProtocolError => Redis::ProtocolError,
18
+ RedisClient::OutOfMemoryError => Redis::OutOfMemoryError,
19
+ }
34
20
 
35
- attr_reader :options, :connection, :command_map
36
-
37
- def scheme
38
- @options[:scheme]
39
- end
40
-
41
- def host
42
- @options[:host]
43
- end
21
+ class << self
22
+ def config(**kwargs)
23
+ super(protocol: 2, **kwargs)
24
+ end
44
25
 
45
- def port
46
- @options[:port]
26
+ def sentinel(**kwargs)
27
+ super(protocol: 2, **kwargs)
28
+ end
47
29
  end
48
30
 
49
- def path
50
- @options[:path]
31
+ def initialize(*)
32
+ super
33
+ @inherit_socket = false
34
+ @pid = nil
51
35
  end
36
+ ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true)
52
37
 
53
- def read_timeout
54
- @options[:read_timeout]
38
+ def id
39
+ config.id
55
40
  end
56
41
 
57
- def connect_timeout
58
- @options[:connect_timeout]
42
+ def server_url
43
+ config.server_url
59
44
  end
60
45
 
61
46
  def timeout
62
- @options[:read_timeout]
63
- end
64
-
65
- def username
66
- @options[:username]
67
- end
68
-
69
- def password
70
- @options[:password]
47
+ config.read_timeout
71
48
  end
72
49
 
73
50
  def db
74
- @options[:db]
75
- end
76
-
77
- def db=(db)
78
- @options[:db] = db.to_i
79
- end
80
-
81
- def driver
82
- @options[:driver]
83
- end
84
-
85
- def inherit_socket?
86
- @options[:inherit_socket]
87
- end
88
-
89
- attr_accessor :logger
90
-
91
- def initialize(options = {})
92
- @options = _parse_options(options)
93
- @reconnect = true
94
- @logger = @options[:logger]
95
- @connection = nil
96
- @command_map = {}
97
-
98
- @pending_reads = 0
99
-
100
- @connector =
101
- if !@options[:sentinels].nil?
102
- Connector::Sentinel.new(@options)
103
- elsif options.include?(:connector) && options[:connector].respond_to?(:new)
104
- options.delete(:connector).new(@options)
105
- else
106
- Connector.new(@options)
107
- end
108
- end
109
-
110
- def connect
111
- @pid = Process.pid
112
-
113
- # Don't try to reconnect when the connection is fresh
114
- with_reconnect(false) do
115
- establish_connection
116
- if password
117
- if username
118
- begin
119
- call [:auth, username, password]
120
- rescue CommandError => err # Likely on Redis < 6
121
- case err.message
122
- when /ERR wrong number of arguments for 'auth' command/
123
- call [:auth, password]
124
- when /WRONGPASS invalid username-password pair/
125
- begin
126
- call [:auth, password]
127
- rescue CommandError
128
- raise err
129
- end
130
- ::Redis.deprecate!(
131
- "[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
132
- " the provided password was for the default user. This will start failing in redis-rb 5.0.0."
133
- )
134
- else
135
- raise
136
- end
137
- end
138
- else
139
- call [:auth, password]
140
- end
141
- end
142
-
143
- call [:readonly] if @options[:readonly]
144
- call [:select, db] if db != 0
145
- call [:client, :setname, @options[:id]] if @options[:id]
146
- @connector.check(self)
147
- end
148
-
149
- self
150
- end
151
-
152
- def id
153
- @options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
51
+ config.db
154
52
  end
155
53
 
156
- def location
157
- path || "#{host}:#{port}"
158
- end
159
-
160
- def call(command)
161
- reply = process([command]) { read }
162
- raise reply if reply.is_a?(CommandError)
163
-
164
- if block_given? && reply != 'QUEUED'
165
- yield reply
166
- else
167
- reply
168
- end
169
- end
170
-
171
- def call_loop(command, timeout = 0)
172
- error = nil
173
-
174
- result = with_socket_timeout(timeout) do
175
- process([command]) do
176
- loop do
177
- reply = read
178
- if reply.is_a?(CommandError)
179
- error = reply
180
- break
181
- else
182
- yield reply
183
- end
184
- end
185
- end
186
- end
187
-
188
- # Raise error when previous block broke out of the loop.
189
- raise error if error
190
-
191
- # Result is set to the value that the provided block used to break.
192
- result
193
- end
194
-
195
- def call_pipeline(pipeline)
196
- return [] if pipeline.futures.empty?
197
-
198
- with_reconnect pipeline.with_reconnect? do
199
- begin
200
- pipeline.finish(call_pipelined(pipeline)).tap do
201
- self.db = pipeline.db if pipeline.db
202
- end
203
- rescue ConnectionError => e
204
- return nil if pipeline.shutdown?
205
-
206
- # Assume the pipeline was sent in one piece, but execution of
207
- # SHUTDOWN caused none of the replies for commands that were executed
208
- # prior to it from coming back around.
209
- raise e
210
- end
211
- end
212
- end
213
-
214
- def call_pipelined(pipeline)
215
- return [] if pipeline.futures.empty?
216
-
217
- # The method #ensure_connected (called from #process) reconnects once on
218
- # I/O errors. To make an effort in making sure that commands are not
219
- # executed more than once, only allow reconnection before the first reply
220
- # has been read. When an error occurs after the first reply has been
221
- # read, retrying would re-execute the entire pipeline, thus re-issuing
222
- # already successfully executed commands. To circumvent this, don't retry
223
- # after the first reply has been read successfully.
224
-
225
- commands = pipeline.commands
226
-
227
- result = Array.new(commands.size)
228
- reconnect = @reconnect
229
-
230
- begin
231
- exception = nil
232
-
233
- process(commands) do
234
- pipeline.timeouts.each_with_index do |timeout, i|
235
- reply = if timeout
236
- with_socket_timeout(timeout) { read }
237
- else
238
- read
239
- end
240
- result[i] = reply
241
- @reconnect = false
242
- exception = reply if exception.nil? && reply.is_a?(CommandError)
243
- end
244
- end
245
-
246
- raise exception if exception
247
- ensure
248
- @reconnect = reconnect
249
- end
250
-
251
- result
252
- end
253
-
254
- def call_with_timeout(command, extra_timeout, &blk)
255
- timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout
256
- with_socket_timeout(timeout) do
257
- call(command, &blk)
258
- end
259
- rescue ConnectionError
260
- retry
261
- end
262
-
263
- def call_without_timeout(command, &blk)
264
- call_with_timeout(command, 0, &blk)
54
+ def host
55
+ config.host unless config.path
265
56
  end
266
57
 
267
- def process(commands)
268
- logging(commands) do
269
- ensure_connected do
270
- commands.each do |command|
271
- if command_map[command.first]
272
- command = command.dup
273
- command[0] = command_map[command.first]
274
- end
275
-
276
- write(command)
277
- end
278
-
279
- yield if block_given?
280
- end
281
- end
58
+ def port
59
+ config.port unless config.path
282
60
  end
283
61
 
284
- def connected?
285
- !!(connection && connection.connected?)
62
+ def path
63
+ config.path
286
64
  end
287
65
 
288
- def disconnect
289
- connection.disconnect if connected?
66
+ def username
67
+ config.username
290
68
  end
291
- alias close disconnect
292
69
 
293
- def reconnect
294
- disconnect
295
- connect
70
+ def password
71
+ config.password
296
72
  end
297
73
 
298
- def io
299
- yield
300
- rescue TimeoutError => e1
301
- # Add a message to the exception without destroying the original stack
302
- e2 = TimeoutError.new("Connection timed out")
303
- e2.set_backtrace(e1.backtrace)
304
- raise e2
305
- rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL, EOFError => e
306
- raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
307
- end
74
+ undef_method :call
75
+ undef_method :call_once
76
+ undef_method :call_once_v
77
+ undef_method :blocking_call
308
78
 
309
- def read
310
- io do
311
- value = connection.read
312
- @pending_reads -= 1
313
- value
314
- end
79
+ def call_v(command, &block)
80
+ super(command, &block)
81
+ rescue ::RedisClient::Error => error
82
+ translate_error!(error)
315
83
  end
316
84
 
317
- def write(command)
318
- io do
319
- @pending_reads += 1
320
- connection.write(command)
85
+ def blocking_call_v(timeout, command, &block)
86
+ if timeout && timeout > 0
87
+ # Can't use the command timeout argument as the connection timeout
88
+ # otherwise it would be very racy. So we add an extra 100ms to account for
89
+ # the network delay.
90
+ timeout += 0.1
321
91
  end
322
- end
323
-
324
- def with_socket_timeout(timeout)
325
- connect unless connected?
326
- original = @options[:read_timeout]
327
92
 
328
- begin
329
- connection.timeout = timeout
330
- @options[:read_timeout] = timeout # for reconnection
331
- yield
332
- ensure
333
- connection.timeout = self.timeout if connected?
334
- @options[:read_timeout] = original
335
- end
93
+ super(timeout, command, &block)
94
+ rescue ::RedisClient::Error => error
95
+ translate_error!(error)
336
96
  end
337
97
 
338
- def without_socket_timeout(&blk)
339
- with_socket_timeout(0, &blk)
98
+ def pipelined
99
+ super
100
+ rescue ::RedisClient::Error => error
101
+ translate_error!(error)
340
102
  end
341
103
 
342
- def with_reconnect(val = true)
343
- original, @reconnect = @reconnect, val
344
- yield
345
- ensure
346
- @reconnect = original
104
+ def multi
105
+ super
106
+ rescue ::RedisClient::Error => error
107
+ translate_error!(error)
347
108
  end
348
109
 
349
- def without_reconnect(&blk)
350
- with_reconnect(false, &blk)
110
+ def disable_reconnection(&block)
111
+ ensure_connected(retryable: false, &block)
351
112
  end
352
113
 
353
- protected
354
-
355
- def logging(commands)
356
- return yield unless @logger&.debug?
357
-
358
- begin
359
- commands.each do |name, *args|
360
- logged_args = args.map do |a|
361
- if a.respond_to?(:inspect) then a.inspect
362
- elsif a.respond_to?(:to_s) then a.to_s
363
- else
364
- # handle poorly-behaved descendants of BasicObject
365
- klass = a.instance_exec { (class << self; self end).superclass }
366
- "\#<#{klass}:#{a.__id__}>"
367
- end
368
- end
369
- @logger.debug("[Redis] command=#{name.to_s.upcase} args=#{logged_args.join(' ')}")
370
- end
371
-
372
- t1 = Time.now
373
- yield
374
- ensure
375
- @logger.debug("[Redis] call_time=%0.2f ms" % ((Time.now - t1) * 1000)) if t1
376
- end
114
+ def inherit_socket!
115
+ @inherit_socket = true
377
116
  end
378
117
 
379
- def establish_connection
380
- server = @connector.resolve.dup
381
-
382
- @options[:host] = server[:host]
383
- @options[:port] = Integer(server[:port]) if server.include?(:port)
384
-
385
- @connection = @options[:driver].connect(@options)
386
- @pending_reads = 0
387
- rescue TimeoutError,
388
- SocketError,
389
- Errno::EADDRNOTAVAIL,
390
- Errno::ECONNREFUSED,
391
- Errno::EHOSTDOWN,
392
- Errno::EHOSTUNREACH,
393
- Errno::ENETUNREACH,
394
- Errno::ENOENT,
395
- Errno::ETIMEDOUT,
396
- Errno::EINVAL => error
397
-
398
- raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
118
+ def close
119
+ super
120
+ @pid = nil
399
121
  end
400
122
 
401
- def ensure_connected
402
- disconnect if @pending_reads > 0
403
-
404
- attempts = 0
405
-
406
- begin
407
- attempts += 1
408
-
409
- if connected?
410
- unless inherit_socket? || Process.pid == @pid
411
- raise InheritedError,
412
- "Tried to use a connection from a child process without reconnecting. " \
413
- "You need to reconnect to Redis after forking " \
414
- "or set :inherit_socket to true."
415
- end
416
- else
417
- connect
418
- end
123
+ private
419
124
 
420
- yield
421
- rescue BaseConnectionError
422
- disconnect
423
-
424
- if attempts <= @options[:reconnect_attempts] && @reconnect
425
- sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
426
- @options[:reconnect_delay_max]].min
427
-
428
- Kernel.sleep(sleep_t)
429
- retry
430
- else
431
- raise
432
- end
433
- rescue Exception
434
- disconnect
435
- raise
436
- end
125
+ def translate_error!(error)
126
+ redis_error = translate_error_class(error.class)
127
+ raise redis_error, error.message, error.backtrace
437
128
  end
438
129
 
439
- def _parse_options(options)
440
- return options if options[:_parsed]
441
-
442
- defaults = DEFAULTS.dup
443
- options = options.dup
444
-
445
- defaults.each_key do |key|
446
- # Fill in defaults if needed
447
- defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
448
-
449
- # Symbolize only keys that are needed
450
- options[key] = options[key.to_s] if options.key?(key.to_s)
451
- end
452
-
453
- url = options[:url]
454
- url = defaults[:url] if url.nil?
455
-
456
- # Override defaults from URL if given
457
- if url
458
- require "uri"
459
-
460
- uri = URI(url)
461
-
462
- case uri.scheme
463
- when "unix"
464
- defaults[:path] = uri.path
465
- when "redis", "rediss"
466
- defaults[:scheme] = uri.scheme
467
- defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
468
- defaults[:port] = uri.port if uri.port
469
- defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
470
- defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
471
- defaults[:db] = uri.path[1..-1].to_i if uri.path
472
- defaults[:role] = :master
473
- else
474
- raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
475
- end
476
-
477
- defaults[:ssl] = true if uri.scheme == "rediss"
478
- end
479
-
480
- # Use default when option is not specified or nil
481
- defaults.each_key do |key|
482
- options[key] = defaults[key] if options[key].nil?
483
- end
484
-
485
- if options[:path]
486
- # Unix socket
487
- options[:scheme] = "unix"
488
- options.delete(:host)
489
- options.delete(:port)
130
+ def translate_error_class(error_class)
131
+ ERROR_MAPPING.fetch(error_class)
132
+ rescue IndexError
133
+ if (client_error = error_class.ancestors.find { |a| ERROR_MAPPING[a] })
134
+ ERROR_MAPPING[error_class] = ERROR_MAPPING[client_error]
490
135
  else
491
- # TCP socket
492
- options[:host] = options[:host].to_s
493
- options[:port] = options[:port].to_i
494
- end
495
-
496
- if options.key?(:timeout)
497
- options[:connect_timeout] ||= options[:timeout]
498
- options[:read_timeout] ||= options[:timeout]
499
- options[:write_timeout] ||= options[:timeout]
500
- end
501
-
502
- options[:connect_timeout] = Float(options[:connect_timeout])
503
- options[:read_timeout] = Float(options[:read_timeout])
504
- options[:write_timeout] = Float(options[:write_timeout])
505
-
506
- options[:reconnect_attempts] = options[:reconnect_attempts].to_i
507
- options[:reconnect_delay] = options[:reconnect_delay].to_f
508
- options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
509
-
510
- options[:db] = options[:db].to_i
511
- options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
512
-
513
- case options[:tcp_keepalive]
514
- when Hash
515
- %i[time intvl probes].each do |key|
516
- unless options[:tcp_keepalive][key].is_a?(Integer)
517
- raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
518
- end
519
- end
520
-
521
- when Integer
522
- if options[:tcp_keepalive] >= 60
523
- options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
524
-
525
- elsif options[:tcp_keepalive] >= 30
526
- options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
527
-
528
- elsif options[:tcp_keepalive] >= 5
529
- options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
530
- end
531
- end
532
-
533
- options[:_parsed] = true
534
-
535
- options
536
- end
537
-
538
- def _parse_driver(driver)
539
- driver = driver.to_s if driver.is_a?(Symbol)
540
-
541
- if driver.is_a?(String)
542
- begin
543
- require_relative "connection/#{driver}"
544
- rescue LoadError, NameError
545
- begin
546
- require "redis/connection/#{driver}"
547
- rescue LoadError, NameError => error
548
- raise "Cannot load driver #{driver.inspect}: #{error.message}"
549
- end
550
- end
551
-
552
- driver = Connection.const_get(driver.capitalize)
136
+ raise
553
137
  end
554
-
555
- driver
556
138
  end
557
139
 
558
- class Connector
559
- def initialize(options)
560
- @options = options.dup
140
+ def ensure_connected(retryable: true)
141
+ unless @inherit_socket || (@pid ||= Process.pid) == Process.pid
142
+ raise InheritedError,
143
+ "Tried to use a connection from a child process without reconnecting. " \
144
+ "You need to reconnect to Redis after forking " \
145
+ "or set :inherit_socket to true."
561
146
  end
562
147
 
563
- def resolve
564
- @options
565
- end
566
-
567
- def check(client); end
568
-
569
- class Sentinel < Connector
570
- def initialize(options)
571
- super(options)
572
-
573
- @options[:db] = DEFAULTS.fetch(:db)
574
-
575
- @sentinels = @options.delete(:sentinels).dup
576
- @role = (@options[:role] || "master").to_s
577
- @master = @options[:host]
578
- end
579
-
580
- def check(client)
581
- # Check the instance is really of the role we are looking for.
582
- # We can't assume the command is supported since it was introduced
583
- # recently and this client should work with old stuff.
584
- begin
585
- role = client.call([:role])[0]
586
- rescue Redis::CommandError
587
- # Assume the test is passed if we can't get a reply from ROLE...
588
- role = @role
589
- end
590
-
591
- if role != @role
592
- client.disconnect
593
- raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
594
- end
595
- end
596
-
597
- def resolve
598
- result = case @role
599
- when "master"
600
- resolve_master
601
- when "slave"
602
- resolve_slave
603
- else
604
- raise ArgumentError, "Unknown instance role #{@role}"
605
- end
606
-
607
- result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
608
- end
609
-
610
- def sentinel_detect
611
- @sentinels.each do |sentinel|
612
- client = Client.new(@options.merge({
613
- host: sentinel[:host] || sentinel["host"],
614
- port: sentinel[:port] || sentinel["port"],
615
- username: sentinel[:username] || sentinel["username"],
616
- password: sentinel[:password] || sentinel["password"],
617
- reconnect_attempts: 0
618
- }))
619
-
620
- begin
621
- if result = yield(client)
622
- # This sentinel responded. Make sure we ask it first next time.
623
- @sentinels.delete(sentinel)
624
- @sentinels.unshift(sentinel)
625
-
626
- return result
627
- end
628
- rescue BaseConnectionError
629
- ensure
630
- client.disconnect
631
- end
632
- end
633
-
634
- raise CannotConnectError, "No sentinels available."
635
- end
636
-
637
- def resolve_master
638
- sentinel_detect do |client|
639
- if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
640
- { host: reply[0], port: reply[1] }
641
- end
642
- end
643
- end
644
-
645
- def resolve_slave
646
- sentinel_detect do |client|
647
- if reply = client.call(["sentinel", "slaves", @master])
648
- slaves = reply.map { |s| s.each_slice(2).to_h }
649
- slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
650
- slaves.reject! { |s| s.fetch('flags').include?('s_down') }
651
-
652
- if slaves.empty?
653
- raise CannotConnectError, 'No slaves available.'
654
- else
655
- slave = slaves.sample
656
- {
657
- host: slave.fetch('ip'),
658
- port: slave.fetch('port')
659
- }
660
- end
661
- end
662
- end
663
- end
664
- end
148
+ super
665
149
  end
666
150
  end
667
151
  end