redis 3.3.5 → 4.4.0

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 +140 -2
  3. data/README.md +144 -79
  4. data/lib/redis.rb +1260 -403
  5. data/lib/redis/client.rb +152 -90
  6. data/lib/redis/cluster.rb +291 -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 +108 -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 +154 -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,19 @@ 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
129
+
130
+ call [:readonly] if @options[:readonly]
103
131
  call [:select, db] if db != 0
104
132
  call [:client, :setname, @options[:id]] if @options[:id]
105
133
  @connector.check(self)
@@ -120,7 +148,7 @@ class Redis
120
148
  reply = process([command]) { read }
121
149
  raise reply if reply.is_a?(CommandError)
122
150
 
123
- if block_given?
151
+ if block_given? && reply != 'QUEUED'
124
152
  yield reply
125
153
  else
126
154
  reply
@@ -152,13 +180,16 @@ class Redis
152
180
  end
153
181
 
154
182
  def call_pipeline(pipeline)
183
+ return [] if pipeline.futures.empty?
184
+
155
185
  with_reconnect pipeline.with_reconnect? do
156
186
  begin
157
- pipeline.finish(call_pipelined(pipeline.commands)).tap do
187
+ pipeline.finish(call_pipelined(pipeline)).tap do
158
188
  self.db = pipeline.db if pipeline.db
159
189
  end
160
190
  rescue ConnectionError => e
161
191
  return nil if pipeline.shutdown?
192
+
162
193
  # Assume the pipeline was sent in one piece, but execution of
163
194
  # SHUTDOWN caused none of the replies for commands that were executed
164
195
  # prior to it from coming back around.
@@ -167,8 +198,8 @@ class Redis
167
198
  end
168
199
  end
169
200
 
170
- def call_pipelined(commands)
171
- return [] if commands.empty?
201
+ def call_pipelined(pipeline)
202
+ return [] if pipeline.futures.empty?
172
203
 
173
204
  # The method #ensure_connected (called from #process) reconnects once on
174
205
  # I/O errors. To make an effort in making sure that commands are not
@@ -178,6 +209,8 @@ class Redis
178
209
  # already successfully executed commands. To circumvent this, don't retry
179
210
  # after the first reply has been read successfully.
180
211
 
212
+ commands = pipeline.commands
213
+
181
214
  result = Array.new(commands.size)
182
215
  reconnect = @reconnect
183
216
 
@@ -185,13 +218,14 @@ class Redis
185
218
  exception = nil
186
219
 
187
220
  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
221
+ pipeline.timeouts.each_with_index do |timeout, i|
222
+ reply = if timeout
223
+ with_socket_timeout(timeout) { read }
224
+ else
225
+ read
226
+ end
227
+ result[i] = reply
228
+ @reconnect = false
195
229
  exception = reply if exception.nil? && reply.is_a?(CommandError)
196
230
  end
197
231
  end
@@ -234,12 +268,13 @@ class Redis
234
268
  end
235
269
 
236
270
  def connected?
237
- !! (connection && connection.connected?)
271
+ !!(connection && connection.connected?)
238
272
  end
239
273
 
240
274
  def disconnect
241
275
  connection.disconnect if connected?
242
276
  end
277
+ alias close disconnect
243
278
 
244
279
  def reconnect
245
280
  disconnect
@@ -274,12 +309,15 @@ class Redis
274
309
 
275
310
  def with_socket_timeout(timeout)
276
311
  connect unless connected?
312
+ original = @options[:read_timeout]
277
313
 
278
314
  begin
279
315
  connection.timeout = timeout
316
+ @options[:read_timeout] = timeout # for reconnection
280
317
  yield
281
318
  ensure
282
319
  connection.timeout = self.timeout if connected?
320
+ @options[:read_timeout] = original
283
321
  end
284
322
  end
285
323
 
@@ -287,30 +325,27 @@ class Redis
287
325
  with_socket_timeout(0, &blk)
288
326
  end
289
327
 
290
- def with_reconnect(val=true)
291
- begin
292
- original, @reconnect = @reconnect, val
293
- yield
294
- ensure
295
- @reconnect = original
296
- end
328
+ def with_reconnect(val = true)
329
+ original, @reconnect = @reconnect, val
330
+ yield
331
+ ensure
332
+ @reconnect = original
297
333
  end
298
334
 
299
335
  def without_reconnect(&blk)
300
336
  with_reconnect(false, &blk)
301
337
  end
302
338
 
303
- protected
339
+ protected
304
340
 
305
341
  def logging(commands)
306
- return yield unless @logger && @logger.debug?
342
+ return yield unless @logger&.debug?
307
343
 
308
344
  begin
309
345
  commands.each do |name, *args|
310
346
  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
347
+ if a.respond_to?(:inspect) then a.inspect
348
+ elsif a.respond_to?(:to_s) then a.to_s
314
349
  else
315
350
  # handle poorly-behaved descendants of BasicObject
316
351
  klass = a.instance_exec { (class << self; self end).superclass }
@@ -336,13 +371,17 @@ class Redis
336
371
  @connection = @options[:driver].connect(@options)
337
372
  @pending_reads = 0
338
373
  rescue TimeoutError,
374
+ SocketError,
375
+ Errno::EADDRNOTAVAIL,
339
376
  Errno::ECONNREFUSED,
340
377
  Errno::EHOSTDOWN,
341
378
  Errno::EHOSTUNREACH,
342
379
  Errno::ENETUNREACH,
343
- Errno::ETIMEDOUT
380
+ Errno::ENOENT,
381
+ Errno::ETIMEDOUT,
382
+ Errno::EINVAL => error
344
383
 
345
- raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
384
+ raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
346
385
  end
347
386
 
348
387
  def ensure_connected
@@ -356,9 +395,9 @@ class Redis
356
395
  if connected?
357
396
  unless inherit_socket? || Process.pid == @pid
358
397
  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."
398
+ "Tried to use a connection from a child process without reconnecting. " \
399
+ "You need to reconnect to Redis after forking " \
400
+ "or set :inherit_socket to true."
362
401
  end
363
402
  else
364
403
  connect
@@ -369,6 +408,10 @@ class Redis
369
408
  disconnect
370
409
 
371
410
  if attempts <= @options[:reconnect_attempts] && @reconnect
411
+ sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
412
+ @options[:reconnect_delay_max]].min
413
+
414
+ Kernel.sleep(sleep_t)
372
415
  retry
373
416
  else
374
417
  raise
@@ -387,15 +430,14 @@ class Redis
387
430
 
388
431
  defaults.keys.each do |key|
389
432
  # Fill in defaults if needed
390
- if defaults[key].respond_to?(:call)
391
- defaults[key] = defaults[key].call
392
- end
433
+ defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
393
434
 
394
435
  # Symbolize only keys that are needed
395
- options[key] = options[key.to_s] if options.has_key?(key.to_s)
436
+ options[key] = options[key.to_s] if options.key?(key.to_s)
396
437
  end
397
438
 
398
- url = options[:url] || defaults[:url]
439
+ url = options[:url]
440
+ url = defaults[:url] if url.nil?
399
441
 
400
442
  # Override defaults from URL if given
401
443
  if url
@@ -404,12 +446,13 @@ class Redis
404
446
  uri = URI(url)
405
447
 
406
448
  if uri.scheme == "unix"
407
- defaults[:path] = uri.path
449
+ defaults[:path] = uri.path
408
450
  elsif uri.scheme == "redis" || uri.scheme == "rediss"
409
451
  defaults[:scheme] = uri.scheme
410
452
  defaults[:host] = uri.host if uri.host
411
453
  defaults[:port] = uri.port if uri.port
412
- defaults[:password] = CGI.unescape(uri.password) if uri.password
454
+ defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
455
+ defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
413
456
  defaults[:db] = uri.path[1..-1].to_i if uri.path
414
457
  defaults[:role] = :master
415
458
  else
@@ -435,7 +478,7 @@ class Redis
435
478
  options[:port] = options[:port].to_i
436
479
  end
437
480
 
438
- if options.has_key?(:timeout)
481
+ if options.key?(:timeout)
439
482
  options[:connect_timeout] ||= options[:timeout]
440
483
  options[:read_timeout] ||= options[:timeout]
441
484
  options[:write_timeout] ||= options[:timeout]
@@ -445,12 +488,16 @@ class Redis
445
488
  options[:read_timeout] = Float(options[:read_timeout])
446
489
  options[:write_timeout] = Float(options[:write_timeout])
447
490
 
491
+ options[:reconnect_attempts] = options[:reconnect_attempts].to_i
492
+ options[:reconnect_delay] = options[:reconnect_delay].to_f
493
+ options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
494
+
448
495
  options[:db] = options[:db].to_i
449
496
  options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
450
497
 
451
498
  case options[:tcp_keepalive]
452
499
  when Hash
453
- [:time, :intvl, :probes].each do |key|
500
+ %i[time intvl probes].each do |key|
454
501
  unless options[:tcp_keepalive][key].is_a?(Integer)
455
502
  raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
456
503
  end
@@ -458,13 +505,13 @@ class Redis
458
505
 
459
506
  when Integer
460
507
  if options[:tcp_keepalive] >= 60
461
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 20, :intvl => 10, :probes => 2}
508
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
462
509
 
463
510
  elsif options[:tcp_keepalive] >= 30
464
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 10, :intvl => 5, :probes => 2}
511
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
465
512
 
466
513
  elsif options[:tcp_keepalive] >= 5
467
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 2, :intvl => 2, :probes => 1}
514
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
468
515
  end
469
516
  end
470
517
 
@@ -476,13 +523,18 @@ class Redis
476
523
  def _parse_driver(driver)
477
524
  driver = driver.to_s if driver.is_a?(Symbol)
478
525
 
479
- if driver.kind_of?(String)
526
+ if driver.is_a?(String)
480
527
  begin
481
- require "redis/connection/#{driver}"
482
- driver = Connection.const_get(driver.capitalize)
528
+ require_relative "connection/#{driver}"
483
529
  rescue LoadError, NameError
484
- raise RuntimeError, "Cannot load driver #{driver.inspect}"
530
+ begin
531
+ require "redis/connection/#{driver}"
532
+ rescue LoadError, NameError => error
533
+ raise "Cannot load driver #{driver.inspect}: #{error.message}"
534
+ end
485
535
  end
536
+
537
+ driver = Connection.const_get(driver.capitalize)
486
538
  end
487
539
 
488
540
  driver
@@ -497,18 +549,16 @@ class Redis
497
549
  @options
498
550
  end
499
551
 
500
- def check(client)
501
- end
552
+ def check(client); end
502
553
 
503
554
  class Sentinel < Connector
504
555
  def initialize(options)
505
556
  super(options)
506
557
 
507
- @options[:password] = DEFAULTS.fetch(:password)
508
558
  @options[:db] = DEFAULTS.fetch(:db)
509
559
 
510
560
  @sentinels = @options.delete(:sentinels).dup
511
- @role = @options.fetch(:role, "master").to_s
561
+ @role = (@options[:role] || "master").to_s
512
562
  @master = @options[:host]
513
563
  end
514
564
 
@@ -531,13 +581,13 @@ class Redis
531
581
 
532
582
  def resolve
533
583
  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
584
+ when "master"
585
+ resolve_master
586
+ when "slave"
587
+ resolve_slave
588
+ else
589
+ raise ArgumentError, "Unknown instance role #{@role}"
590
+ end
541
591
 
542
592
  result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
543
593
  end
@@ -545,10 +595,12 @@ class Redis
545
595
  def sentinel_detect
546
596
  @sentinels.each do |sentinel|
547
597
  client = Client.new(@options.merge({
548
- :host => sentinel[:host],
549
- :port => sentinel[:port],
550
- :reconnect_attempts => 0,
551
- }))
598
+ host: sentinel[:host] || sentinel["host"],
599
+ port: sentinel[:port] || sentinel["port"],
600
+ username: sentinel[:username] || sentinel["username"],
601
+ password: sentinel[:password] || sentinel["password"],
602
+ reconnect_attempts: 0
603
+ }))
552
604
 
553
605
  begin
554
606
  if result = yield(client)
@@ -570,7 +622,7 @@ class Redis
570
622
  def resolve_master
571
623
  sentinel_detect do |client|
572
624
  if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
573
- {:host => reply[0], :port => reply[1]}
625
+ { host: reply[0], port: reply[1] }
574
626
  end
575
627
  end
576
628
  end
@@ -578,9 +630,19 @@ class Redis
578
630
  def resolve_slave
579
631
  sentinel_detect do |client|
580
632
  if reply = client.call(["sentinel", "slaves", @master])
581
- slave = Hash[*reply.sample]
582
-
583
- {:host => slave.fetch("ip"), :port => slave.fetch("port")}
633
+ slaves = reply.map { |s| s.each_slice(2).to_h }
634
+ slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
635
+ slaves.reject! { |s| s.fetch('flags').include?('s_down') }
636
+
637
+ if slaves.empty?
638
+ raise CannotConnectError, 'No slaves available.'
639
+ else
640
+ slave = slaves.sample
641
+ {
642
+ host: slave.fetch('ip'),
643
+ port: slave.fetch('port')
644
+ }
645
+ end
584
646
  end
585
647
  end
586
648
  end