redis 3.3.5 → 4.3.1

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 (130) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +132 -2
  3. data/README.md +144 -79
  4. data/lib/redis.rb +1174 -405
  5. data/lib/redis/client.rb +150 -90
  6. data/lib/redis/cluster.rb +295 -0
  7. data/lib/redis/cluster/command.rb +81 -0
  8. data/lib/redis/cluster/command_loader.rb +34 -0
  9. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  10. data/lib/redis/cluster/node.rb +107 -0
  11. data/lib/redis/cluster/node_key.rb +31 -0
  12. data/lib/redis/cluster/node_loader.rb +37 -0
  13. data/lib/redis/cluster/option.rb +93 -0
  14. data/lib/redis/cluster/slot.rb +86 -0
  15. data/lib/redis/cluster/slot_loader.rb +49 -0
  16. data/lib/redis/connection.rb +4 -2
  17. data/lib/redis/connection/command_helper.rb +5 -10
  18. data/lib/redis/connection/hiredis.rb +6 -5
  19. data/lib/redis/connection/registry.rb +2 -1
  20. data/lib/redis/connection/ruby.rb +126 -128
  21. data/lib/redis/connection/synchrony.rb +21 -8
  22. data/lib/redis/distributed.rb +147 -72
  23. data/lib/redis/errors.rb +48 -0
  24. data/lib/redis/hash_ring.rb +30 -73
  25. data/lib/redis/pipeline.rb +55 -15
  26. data/lib/redis/subscribe.rb +11 -12
  27. data/lib/redis/version.rb +3 -1
  28. metadata +49 -202
  29. data/.gitignore +0 -16
  30. data/.travis.yml +0 -89
  31. data/.travis/Gemfile +0 -11
  32. data/.yardopts +0 -3
  33. data/Gemfile +0 -4
  34. data/Rakefile +0 -87
  35. data/benchmarking/logging.rb +0 -71
  36. data/benchmarking/pipeline.rb +0 -51
  37. data/benchmarking/speed.rb +0 -21
  38. data/benchmarking/suite.rb +0 -24
  39. data/benchmarking/worker.rb +0 -71
  40. data/examples/basic.rb +0 -15
  41. data/examples/consistency.rb +0 -114
  42. data/examples/dist_redis.rb +0 -43
  43. data/examples/incr-decr.rb +0 -17
  44. data/examples/list.rb +0 -26
  45. data/examples/pubsub.rb +0 -37
  46. data/examples/sentinel.rb +0 -41
  47. data/examples/sentinel/sentinel.conf +0 -9
  48. data/examples/sentinel/start +0 -49
  49. data/examples/sets.rb +0 -36
  50. data/examples/unicorn/config.ru +0 -3
  51. data/examples/unicorn/unicorn.rb +0 -20
  52. data/redis.gemspec +0 -44
  53. data/test/bitpos_test.rb +0 -69
  54. data/test/blocking_commands_test.rb +0 -42
  55. data/test/client_test.rb +0 -59
  56. data/test/command_map_test.rb +0 -30
  57. data/test/commands_on_hashes_test.rb +0 -21
  58. data/test/commands_on_hyper_log_log_test.rb +0 -21
  59. data/test/commands_on_lists_test.rb +0 -20
  60. data/test/commands_on_sets_test.rb +0 -77
  61. data/test/commands_on_sorted_sets_test.rb +0 -137
  62. data/test/commands_on_strings_test.rb +0 -101
  63. data/test/commands_on_value_types_test.rb +0 -133
  64. data/test/connection_handling_test.rb +0 -277
  65. data/test/connection_test.rb +0 -57
  66. data/test/db/.gitkeep +0 -0
  67. data/test/distributed_blocking_commands_test.rb +0 -46
  68. data/test/distributed_commands_on_hashes_test.rb +0 -10
  69. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  70. data/test/distributed_commands_on_lists_test.rb +0 -22
  71. data/test/distributed_commands_on_sets_test.rb +0 -83
  72. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  73. data/test/distributed_commands_on_strings_test.rb +0 -59
  74. data/test/distributed_commands_on_value_types_test.rb +0 -95
  75. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  76. data/test/distributed_connection_handling_test.rb +0 -23
  77. data/test/distributed_internals_test.rb +0 -79
  78. data/test/distributed_key_tags_test.rb +0 -52
  79. data/test/distributed_persistence_control_commands_test.rb +0 -26
  80. data/test/distributed_publish_subscribe_test.rb +0 -92
  81. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  82. data/test/distributed_scripting_test.rb +0 -102
  83. data/test/distributed_sorting_test.rb +0 -20
  84. data/test/distributed_test.rb +0 -58
  85. data/test/distributed_transactions_test.rb +0 -32
  86. data/test/encoding_test.rb +0 -18
  87. data/test/error_replies_test.rb +0 -59
  88. data/test/fork_safety_test.rb +0 -65
  89. data/test/helper.rb +0 -232
  90. data/test/helper_test.rb +0 -24
  91. data/test/internals_test.rb +0 -417
  92. data/test/lint/blocking_commands.rb +0 -150
  93. data/test/lint/hashes.rb +0 -162
  94. data/test/lint/hyper_log_log.rb +0 -60
  95. data/test/lint/lists.rb +0 -143
  96. data/test/lint/sets.rb +0 -140
  97. data/test/lint/sorted_sets.rb +0 -316
  98. data/test/lint/strings.rb +0 -260
  99. data/test/lint/value_types.rb +0 -122
  100. data/test/persistence_control_commands_test.rb +0 -26
  101. data/test/pipelining_commands_test.rb +0 -242
  102. data/test/publish_subscribe_test.rb +0 -282
  103. data/test/remote_server_control_commands_test.rb +0 -118
  104. data/test/scanning_test.rb +0 -413
  105. data/test/scripting_test.rb +0 -78
  106. data/test/sentinel_command_test.rb +0 -80
  107. data/test/sentinel_test.rb +0 -255
  108. data/test/sorting_test.rb +0 -59
  109. data/test/ssl_test.rb +0 -73
  110. data/test/support/connection/hiredis.rb +0 -1
  111. data/test/support/connection/ruby.rb +0 -1
  112. data/test/support/connection/synchrony.rb +0 -17
  113. data/test/support/redis_mock.rb +0 -130
  114. data/test/support/ssl/gen_certs.sh +0 -31
  115. data/test/support/ssl/trusted-ca.crt +0 -25
  116. data/test/support/ssl/trusted-ca.key +0 -27
  117. data/test/support/ssl/trusted-cert.crt +0 -81
  118. data/test/support/ssl/trusted-cert.key +0 -28
  119. data/test/support/ssl/untrusted-ca.crt +0 -26
  120. data/test/support/ssl/untrusted-ca.key +0 -27
  121. data/test/support/ssl/untrusted-cert.crt +0 -82
  122. data/test/support/ssl/untrusted-cert.key +0 -28
  123. data/test/support/wire/synchrony.rb +0 -24
  124. data/test/support/wire/thread.rb +0 -5
  125. data/test/synchrony_driver.rb +0 -88
  126. data/test/test.conf.erb +0 -9
  127. data/test/thread_safety_test.rb +0 -62
  128. data/test/transactions_test.rb +0 -264
  129. data/test/unknown_commands_test.rb +0 -14
  130. data/test/url_param_test.rb +0 -138
data/lib/redis/client.rb CHANGED
@@ -1,29 +1,38 @@
1
- require "redis/errors"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
2
4
  require "socket"
3
5
  require "cgi"
4
6
 
5
7
  class Redis
6
8
  class Client
7
-
9
+ # Defaults are also used for converting string keys to symbols.
8
10
  DEFAULTS = {
9
- :url => lambda { ENV["REDIS_URL"] },
10
- :scheme => "redis",
11
- :host => "127.0.0.1",
12
- :port => 6379,
13
- :path => nil,
14
- :timeout => 5.0,
15
- :password => nil,
16
- :db => 0,
17
- :driver => nil,
18
- :id => nil,
19
- :tcp_keepalive => 0,
20
- :reconnect_attempts => 1,
21
- :inherit_socket => false
22
- }
23
-
24
- def options
25
- Marshal.load(Marshal.dump(@options))
26
- end
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
34
+
35
+ attr_reader :options
27
36
 
28
37
  def scheme
29
38
  @options[:scheme]
@@ -53,6 +62,10 @@ class Redis
53
62
  @options[:read_timeout]
54
63
  end
55
64
 
65
+ def username
66
+ @options[:username]
67
+ end
68
+
56
69
  def password
57
70
  @options[:password]
58
71
  end
@@ -86,11 +99,14 @@ class Redis
86
99
 
87
100
  @pending_reads = 0
88
101
 
89
- if options.include?(:sentinels)
90
- @connector = Connector::Sentinel.new(@options)
91
- else
92
- @connector = Connector.new(@options)
93
- end
102
+ @connector =
103
+ if !@options[:sentinels].nil?
104
+ Connector::Sentinel.new(@options)
105
+ elsif options.include?(:connector) && options[:connector].respond_to?(:new)
106
+ options.delete(:connector).new(@options)
107
+ else
108
+ Connector.new(@options)
109
+ end
94
110
  end
95
111
 
96
112
  def connect
@@ -99,7 +115,17 @@ class Redis
99
115
  # Don't try to reconnect when the connection is fresh
100
116
  with_reconnect(false) do
101
117
  establish_connection
102
- call [:auth, password] if password
118
+ if password
119
+ if username
120
+ begin
121
+ call [:auth, username, password]
122
+ rescue CommandError # Likely on Redis < 6
123
+ call [:auth, password]
124
+ end
125
+ else
126
+ call [:auth, password]
127
+ end
128
+ end
103
129
  call [:select, db] if db != 0
104
130
  call [:client, :setname, @options[:id]] if @options[:id]
105
131
  @connector.check(self)
@@ -120,7 +146,7 @@ class Redis
120
146
  reply = process([command]) { read }
121
147
  raise reply if reply.is_a?(CommandError)
122
148
 
123
- if block_given?
149
+ if block_given? && reply != 'QUEUED'
124
150
  yield reply
125
151
  else
126
152
  reply
@@ -152,13 +178,16 @@ class Redis
152
178
  end
153
179
 
154
180
  def call_pipeline(pipeline)
181
+ return [] if pipeline.futures.empty?
182
+
155
183
  with_reconnect pipeline.with_reconnect? do
156
184
  begin
157
- pipeline.finish(call_pipelined(pipeline.commands)).tap do
185
+ pipeline.finish(call_pipelined(pipeline)).tap do
158
186
  self.db = pipeline.db if pipeline.db
159
187
  end
160
188
  rescue ConnectionError => e
161
189
  return nil if pipeline.shutdown?
190
+
162
191
  # Assume the pipeline was sent in one piece, but execution of
163
192
  # SHUTDOWN caused none of the replies for commands that were executed
164
193
  # prior to it from coming back around.
@@ -167,8 +196,8 @@ class Redis
167
196
  end
168
197
  end
169
198
 
170
- def call_pipelined(commands)
171
- return [] if commands.empty?
199
+ def call_pipelined(pipeline)
200
+ return [] if pipeline.futures.empty?
172
201
 
173
202
  # The method #ensure_connected (called from #process) reconnects once on
174
203
  # I/O errors. To make an effort in making sure that commands are not
@@ -178,6 +207,8 @@ class Redis
178
207
  # already successfully executed commands. To circumvent this, don't retry
179
208
  # after the first reply has been read successfully.
180
209
 
210
+ commands = pipeline.commands
211
+
181
212
  result = Array.new(commands.size)
182
213
  reconnect = @reconnect
183
214
 
@@ -185,13 +216,14 @@ class Redis
185
216
  exception = nil
186
217
 
187
218
  process(commands) do
188
- result[0] = read
189
-
190
- @reconnect = false
191
-
192
- (commands.size - 1).times do |i|
193
- reply = read
194
- result[i + 1] = reply
219
+ pipeline.timeouts.each_with_index do |timeout, i|
220
+ reply = if timeout
221
+ with_socket_timeout(timeout) { read }
222
+ else
223
+ read
224
+ end
225
+ result[i] = reply
226
+ @reconnect = false
195
227
  exception = reply if exception.nil? && reply.is_a?(CommandError)
196
228
  end
197
229
  end
@@ -234,12 +266,13 @@ class Redis
234
266
  end
235
267
 
236
268
  def connected?
237
- !! (connection && connection.connected?)
269
+ !!(connection && connection.connected?)
238
270
  end
239
271
 
240
272
  def disconnect
241
273
  connection.disconnect if connected?
242
274
  end
275
+ alias close disconnect
243
276
 
244
277
  def reconnect
245
278
  disconnect
@@ -274,12 +307,15 @@ class Redis
274
307
 
275
308
  def with_socket_timeout(timeout)
276
309
  connect unless connected?
310
+ original = @options[:read_timeout]
277
311
 
278
312
  begin
279
313
  connection.timeout = timeout
314
+ @options[:read_timeout] = timeout # for reconnection
280
315
  yield
281
316
  ensure
282
317
  connection.timeout = self.timeout if connected?
318
+ @options[:read_timeout] = original
283
319
  end
284
320
  end
285
321
 
@@ -287,30 +323,27 @@ class Redis
287
323
  with_socket_timeout(0, &blk)
288
324
  end
289
325
 
290
- def with_reconnect(val=true)
291
- begin
292
- original, @reconnect = @reconnect, val
293
- yield
294
- ensure
295
- @reconnect = original
296
- end
326
+ def with_reconnect(val = true)
327
+ original, @reconnect = @reconnect, val
328
+ yield
329
+ ensure
330
+ @reconnect = original
297
331
  end
298
332
 
299
333
  def without_reconnect(&blk)
300
334
  with_reconnect(false, &blk)
301
335
  end
302
336
 
303
- protected
337
+ protected
304
338
 
305
339
  def logging(commands)
306
- return yield unless @logger && @logger.debug?
340
+ return yield unless @logger&.debug?
307
341
 
308
342
  begin
309
343
  commands.each do |name, *args|
310
344
  logged_args = args.map do |a|
311
- case
312
- when a.respond_to?(:inspect) then a.inspect
313
- when a.respond_to?(:to_s) then a.to_s
345
+ if a.respond_to?(:inspect) then a.inspect
346
+ elsif a.respond_to?(:to_s) then a.to_s
314
347
  else
315
348
  # handle poorly-behaved descendants of BasicObject
316
349
  klass = a.instance_exec { (class << self; self end).superclass }
@@ -336,13 +369,17 @@ class Redis
336
369
  @connection = @options[:driver].connect(@options)
337
370
  @pending_reads = 0
338
371
  rescue TimeoutError,
372
+ SocketError,
373
+ Errno::EADDRNOTAVAIL,
339
374
  Errno::ECONNREFUSED,
340
375
  Errno::EHOSTDOWN,
341
376
  Errno::EHOSTUNREACH,
342
377
  Errno::ENETUNREACH,
343
- Errno::ETIMEDOUT
378
+ Errno::ENOENT,
379
+ Errno::ETIMEDOUT,
380
+ Errno::EINVAL => error
344
381
 
345
- raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
382
+ raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
346
383
  end
347
384
 
348
385
  def ensure_connected
@@ -356,9 +393,9 @@ class Redis
356
393
  if connected?
357
394
  unless inherit_socket? || Process.pid == @pid
358
395
  raise InheritedError,
359
- "Tried to use a connection from a child process without reconnecting. " +
360
- "You need to reconnect to Redis after forking " +
361
- "or set :inherit_socket to true."
396
+ "Tried to use a connection from a child process without reconnecting. " \
397
+ "You need to reconnect to Redis after forking " \
398
+ "or set :inherit_socket to true."
362
399
  end
363
400
  else
364
401
  connect
@@ -369,6 +406,10 @@ class Redis
369
406
  disconnect
370
407
 
371
408
  if attempts <= @options[:reconnect_attempts] && @reconnect
409
+ sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
410
+ @options[:reconnect_delay_max]].min
411
+
412
+ Kernel.sleep(sleep_t)
372
413
  retry
373
414
  else
374
415
  raise
@@ -387,15 +428,14 @@ class Redis
387
428
 
388
429
  defaults.keys.each do |key|
389
430
  # Fill in defaults if needed
390
- if defaults[key].respond_to?(:call)
391
- defaults[key] = defaults[key].call
392
- end
431
+ defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
393
432
 
394
433
  # Symbolize only keys that are needed
395
- options[key] = options[key.to_s] if options.has_key?(key.to_s)
434
+ options[key] = options[key.to_s] if options.key?(key.to_s)
396
435
  end
397
436
 
398
- url = options[:url] || defaults[:url]
437
+ url = options[:url]
438
+ url = defaults[:url] if url.nil?
399
439
 
400
440
  # Override defaults from URL if given
401
441
  if url
@@ -404,12 +444,13 @@ class Redis
404
444
  uri = URI(url)
405
445
 
406
446
  if uri.scheme == "unix"
407
- defaults[:path] = uri.path
447
+ defaults[:path] = uri.path
408
448
  elsif uri.scheme == "redis" || uri.scheme == "rediss"
409
449
  defaults[:scheme] = uri.scheme
410
450
  defaults[:host] = uri.host if uri.host
411
451
  defaults[:port] = uri.port if uri.port
412
- defaults[:password] = CGI.unescape(uri.password) if uri.password
452
+ defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
453
+ defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
413
454
  defaults[:db] = uri.path[1..-1].to_i if uri.path
414
455
  defaults[:role] = :master
415
456
  else
@@ -435,7 +476,7 @@ class Redis
435
476
  options[:port] = options[:port].to_i
436
477
  end
437
478
 
438
- if options.has_key?(:timeout)
479
+ if options.key?(:timeout)
439
480
  options[:connect_timeout] ||= options[:timeout]
440
481
  options[:read_timeout] ||= options[:timeout]
441
482
  options[:write_timeout] ||= options[:timeout]
@@ -445,12 +486,16 @@ class Redis
445
486
  options[:read_timeout] = Float(options[:read_timeout])
446
487
  options[:write_timeout] = Float(options[:write_timeout])
447
488
 
489
+ options[:reconnect_attempts] = options[:reconnect_attempts].to_i
490
+ options[:reconnect_delay] = options[:reconnect_delay].to_f
491
+ options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
492
+
448
493
  options[:db] = options[:db].to_i
449
494
  options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
450
495
 
451
496
  case options[:tcp_keepalive]
452
497
  when Hash
453
- [:time, :intvl, :probes].each do |key|
498
+ %i[time intvl probes].each do |key|
454
499
  unless options[:tcp_keepalive][key].is_a?(Integer)
455
500
  raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
456
501
  end
@@ -458,13 +503,13 @@ class Redis
458
503
 
459
504
  when Integer
460
505
  if options[:tcp_keepalive] >= 60
461
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 20, :intvl => 10, :probes => 2}
506
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
462
507
 
463
508
  elsif options[:tcp_keepalive] >= 30
464
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 10, :intvl => 5, :probes => 2}
509
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
465
510
 
466
511
  elsif options[:tcp_keepalive] >= 5
467
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 2, :intvl => 2, :probes => 1}
512
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
468
513
  end
469
514
  end
470
515
 
@@ -476,13 +521,18 @@ class Redis
476
521
  def _parse_driver(driver)
477
522
  driver = driver.to_s if driver.is_a?(Symbol)
478
523
 
479
- if driver.kind_of?(String)
524
+ if driver.is_a?(String)
480
525
  begin
481
- require "redis/connection/#{driver}"
482
- driver = Connection.const_get(driver.capitalize)
526
+ require_relative "connection/#{driver}"
483
527
  rescue LoadError, NameError
484
- raise RuntimeError, "Cannot load driver #{driver.inspect}"
528
+ begin
529
+ require "redis/connection/#{driver}"
530
+ rescue LoadError, NameError => error
531
+ raise "Cannot load driver #{driver.inspect}: #{error.message}"
532
+ end
485
533
  end
534
+
535
+ driver = Connection.const_get(driver.capitalize)
486
536
  end
487
537
 
488
538
  driver
@@ -497,18 +547,16 @@ class Redis
497
547
  @options
498
548
  end
499
549
 
500
- def check(client)
501
- end
550
+ def check(client); end
502
551
 
503
552
  class Sentinel < Connector
504
553
  def initialize(options)
505
554
  super(options)
506
555
 
507
- @options[:password] = DEFAULTS.fetch(:password)
508
556
  @options[:db] = DEFAULTS.fetch(:db)
509
557
 
510
558
  @sentinels = @options.delete(:sentinels).dup
511
- @role = @options.fetch(:role, "master").to_s
559
+ @role = (@options[:role] || "master").to_s
512
560
  @master = @options[:host]
513
561
  end
514
562
 
@@ -531,13 +579,13 @@ class Redis
531
579
 
532
580
  def resolve
533
581
  result = case @role
534
- when "master"
535
- resolve_master
536
- when "slave"
537
- resolve_slave
538
- else
539
- raise ArgumentError, "Unknown instance role #{@role}"
540
- end
582
+ when "master"
583
+ resolve_master
584
+ when "slave"
585
+ resolve_slave
586
+ else
587
+ raise ArgumentError, "Unknown instance role #{@role}"
588
+ end
541
589
 
542
590
  result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
543
591
  end
@@ -545,10 +593,12 @@ class Redis
545
593
  def sentinel_detect
546
594
  @sentinels.each do |sentinel|
547
595
  client = Client.new(@options.merge({
548
- :host => sentinel[:host],
549
- :port => sentinel[:port],
550
- :reconnect_attempts => 0,
551
- }))
596
+ host: sentinel[:host] || sentinel["host"],
597
+ port: sentinel[:port] || sentinel["port"],
598
+ username: sentinel[:username] || sentinel["username"],
599
+ password: sentinel[:password] || sentinel["password"],
600
+ reconnect_attempts: 0
601
+ }))
552
602
 
553
603
  begin
554
604
  if result = yield(client)
@@ -570,7 +620,7 @@ class Redis
570
620
  def resolve_master
571
621
  sentinel_detect do |client|
572
622
  if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
573
- {:host => reply[0], :port => reply[1]}
623
+ { host: reply[0], port: reply[1] }
574
624
  end
575
625
  end
576
626
  end
@@ -578,9 +628,19 @@ class Redis
578
628
  def resolve_slave
579
629
  sentinel_detect do |client|
580
630
  if reply = client.call(["sentinel", "slaves", @master])
581
- slave = Hash[*reply.sample]
582
-
583
- {:host => slave.fetch("ip"), :port => slave.fetch("port")}
631
+ slaves = reply.map { |s| s.each_slice(2).to_h }
632
+ slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
633
+ slaves.reject! { |s| s.fetch('flags').include?('s_down') }
634
+
635
+ if slaves.empty?
636
+ raise CannotConnectError, 'No slaves available.'
637
+ else
638
+ slave = slaves.sample
639
+ {
640
+ host: slave.fetch('ip'),
641
+ port: slave.fetch('port')
642
+ }
643
+ end
584
644
  end
585
645
  end
586
646
  end