redis 3.2.2 → 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 (118) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +175 -13
  3. data/README.md +223 -76
  4. data/lib/redis.rb +1360 -445
  5. data/lib/redis/client.rb +183 -103
  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 +9 -6
  19. data/lib/redis/connection/registry.rb +2 -1
  20. data/lib/redis/connection/ruby.rb +168 -63
  21. data/lib/redis/connection/synchrony.rb +29 -7
  22. data/lib/redis/distributed.rb +156 -74
  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 +20 -13
  27. data/lib/redis/version.rb +3 -1
  28. metadata +41 -170
  29. data/.gitignore +0 -16
  30. data/.travis.yml +0 -59
  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/command_map_test.rb +0 -30
  56. data/test/commands_on_hashes_test.rb +0 -21
  57. data/test/commands_on_hyper_log_log_test.rb +0 -21
  58. data/test/commands_on_lists_test.rb +0 -20
  59. data/test/commands_on_sets_test.rb +0 -77
  60. data/test/commands_on_sorted_sets_test.rb +0 -137
  61. data/test/commands_on_strings_test.rb +0 -101
  62. data/test/commands_on_value_types_test.rb +0 -133
  63. data/test/connection_handling_test.rb +0 -250
  64. data/test/db/.gitkeep +0 -0
  65. data/test/distributed_blocking_commands_test.rb +0 -46
  66. data/test/distributed_commands_on_hashes_test.rb +0 -10
  67. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  68. data/test/distributed_commands_on_lists_test.rb +0 -22
  69. data/test/distributed_commands_on_sets_test.rb +0 -83
  70. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  71. data/test/distributed_commands_on_strings_test.rb +0 -59
  72. data/test/distributed_commands_on_value_types_test.rb +0 -95
  73. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  74. data/test/distributed_connection_handling_test.rb +0 -23
  75. data/test/distributed_internals_test.rb +0 -79
  76. data/test/distributed_key_tags_test.rb +0 -52
  77. data/test/distributed_persistence_control_commands_test.rb +0 -26
  78. data/test/distributed_publish_subscribe_test.rb +0 -92
  79. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  80. data/test/distributed_scripting_test.rb +0 -102
  81. data/test/distributed_sorting_test.rb +0 -20
  82. data/test/distributed_test.rb +0 -58
  83. data/test/distributed_transactions_test.rb +0 -32
  84. data/test/encoding_test.rb +0 -18
  85. data/test/error_replies_test.rb +0 -59
  86. data/test/fork_safety_test.rb +0 -65
  87. data/test/helper.rb +0 -232
  88. data/test/helper_test.rb +0 -24
  89. data/test/internals_test.rb +0 -437
  90. data/test/lint/blocking_commands.rb +0 -150
  91. data/test/lint/hashes.rb +0 -162
  92. data/test/lint/hyper_log_log.rb +0 -60
  93. data/test/lint/lists.rb +0 -143
  94. data/test/lint/sets.rb +0 -125
  95. data/test/lint/sorted_sets.rb +0 -316
  96. data/test/lint/strings.rb +0 -260
  97. data/test/lint/value_types.rb +0 -122
  98. data/test/persistence_control_commands_test.rb +0 -26
  99. data/test/pipelining_commands_test.rb +0 -242
  100. data/test/publish_subscribe_test.rb +0 -254
  101. data/test/remote_server_control_commands_test.rb +0 -118
  102. data/test/scanning_test.rb +0 -413
  103. data/test/scripting_test.rb +0 -78
  104. data/test/sentinel_command_test.rb +0 -80
  105. data/test/sentinel_test.rb +0 -255
  106. data/test/sorting_test.rb +0 -59
  107. data/test/support/connection/hiredis.rb +0 -1
  108. data/test/support/connection/ruby.rb +0 -1
  109. data/test/support/connection/synchrony.rb +0 -17
  110. data/test/support/redis_mock.rb +0 -119
  111. data/test/support/wire/synchrony.rb +0 -24
  112. data/test/support/wire/thread.rb +0 -5
  113. data/test/synchrony_driver.rb +0 -88
  114. data/test/test.conf.erb +0 -9
  115. data/test/thread_safety_test.rb +0 -32
  116. data/test/transactions_test.rb +0 -264
  117. data/test/unknown_commands_test.rb +0 -14
  118. data/test/url_param_test.rb +0 -138
data/lib/redis/client.rb CHANGED
@@ -1,30 +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
- :connect_timeout => 5.0,
16
- :password => nil,
17
- :db => 0,
18
- :driver => nil,
19
- :id => nil,
20
- :tcp_keepalive => 0,
21
- :reconnect_attempts => 1,
22
- :inherit_socket => false
23
- }
24
-
25
- def options
26
- Marshal.load(Marshal.dump(@options))
27
- 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
28
36
 
29
37
  def scheme
30
38
  @options[:scheme]
@@ -42,8 +50,20 @@ class Redis
42
50
  @options[:path]
43
51
  end
44
52
 
53
+ def read_timeout
54
+ @options[:read_timeout]
55
+ end
56
+
57
+ def connect_timeout
58
+ @options[:connect_timeout]
59
+ end
60
+
45
61
  def timeout
46
- @options[:timeout]
62
+ @options[:read_timeout]
63
+ end
64
+
65
+ def username
66
+ @options[:username]
47
67
  end
48
68
 
49
69
  def password
@@ -79,11 +99,14 @@ class Redis
79
99
 
80
100
  @pending_reads = 0
81
101
 
82
- if options.include?(:sentinels)
83
- @connector = Connector::Sentinel.new(@options)
84
- else
85
- @connector = Connector.new(@options)
86
- 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
87
110
  end
88
111
 
89
112
  def connect
@@ -92,7 +115,19 @@ class Redis
92
115
  # Don't try to reconnect when the connection is fresh
93
116
  with_reconnect(false) do
94
117
  establish_connection
95
- 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]
96
131
  call [:select, db] if db != 0
97
132
  call [:client, :setname, @options[:id]] if @options[:id]
98
133
  @connector.check(self)
@@ -109,21 +144,21 @@ class Redis
109
144
  path || "#{host}:#{port}"
110
145
  end
111
146
 
112
- def call(command, &block)
147
+ def call(command)
113
148
  reply = process([command]) { read }
114
149
  raise reply if reply.is_a?(CommandError)
115
150
 
116
- if block
117
- block.call(reply)
151
+ if block_given? && reply != 'QUEUED'
152
+ yield reply
118
153
  else
119
154
  reply
120
155
  end
121
156
  end
122
157
 
123
- def call_loop(command)
158
+ def call_loop(command, timeout = 0)
124
159
  error = nil
125
160
 
126
- result = without_socket_timeout do
161
+ result = with_socket_timeout(timeout) do
127
162
  process([command]) do
128
163
  loop do
129
164
  reply = read
@@ -145,13 +180,16 @@ class Redis
145
180
  end
146
181
 
147
182
  def call_pipeline(pipeline)
183
+ return [] if pipeline.futures.empty?
184
+
148
185
  with_reconnect pipeline.with_reconnect? do
149
186
  begin
150
- pipeline.finish(call_pipelined(pipeline.commands)).tap do
187
+ pipeline.finish(call_pipelined(pipeline)).tap do
151
188
  self.db = pipeline.db if pipeline.db
152
189
  end
153
190
  rescue ConnectionError => e
154
191
  return nil if pipeline.shutdown?
192
+
155
193
  # Assume the pipeline was sent in one piece, but execution of
156
194
  # SHUTDOWN caused none of the replies for commands that were executed
157
195
  # prior to it from coming back around.
@@ -160,8 +198,8 @@ class Redis
160
198
  end
161
199
  end
162
200
 
163
- def call_pipelined(commands)
164
- return [] if commands.empty?
201
+ def call_pipelined(pipeline)
202
+ return [] if pipeline.futures.empty?
165
203
 
166
204
  # The method #ensure_connected (called from #process) reconnects once on
167
205
  # I/O errors. To make an effort in making sure that commands are not
@@ -171,19 +209,28 @@ class Redis
171
209
  # already successfully executed commands. To circumvent this, don't retry
172
210
  # after the first reply has been read successfully.
173
211
 
212
+ commands = pipeline.commands
213
+
174
214
  result = Array.new(commands.size)
175
215
  reconnect = @reconnect
176
216
 
177
217
  begin
178
- process(commands) do
179
- result[0] = read
180
-
181
- @reconnect = false
218
+ exception = nil
182
219
 
183
- (commands.size - 1).times do |i|
184
- result[i + 1] = read
220
+ process(commands) do
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
229
+ exception = reply if exception.nil? && reply.is_a?(CommandError)
185
230
  end
186
231
  end
232
+
233
+ raise exception if exception
187
234
  ensure
188
235
  @reconnect = reconnect
189
236
  end
@@ -221,12 +268,13 @@ class Redis
221
268
  end
222
269
 
223
270
  def connected?
224
- !! (connection && connection.connected?)
271
+ !!(connection && connection.connected?)
225
272
  end
226
273
 
227
274
  def disconnect
228
275
  connection.disconnect if connected?
229
276
  end
277
+ alias close disconnect
230
278
 
231
279
  def reconnect
232
280
  disconnect
@@ -261,12 +309,15 @@ class Redis
261
309
 
262
310
  def with_socket_timeout(timeout)
263
311
  connect unless connected?
312
+ original = @options[:read_timeout]
264
313
 
265
314
  begin
266
315
  connection.timeout = timeout
316
+ @options[:read_timeout] = timeout # for reconnection
267
317
  yield
268
318
  ensure
269
319
  connection.timeout = self.timeout if connected?
320
+ @options[:read_timeout] = original
270
321
  end
271
322
  end
272
323
 
@@ -274,30 +325,27 @@ class Redis
274
325
  with_socket_timeout(0, &blk)
275
326
  end
276
327
 
277
- def with_reconnect(val=true)
278
- begin
279
- original, @reconnect = @reconnect, val
280
- yield
281
- ensure
282
- @reconnect = original
283
- end
328
+ def with_reconnect(val = true)
329
+ original, @reconnect = @reconnect, val
330
+ yield
331
+ ensure
332
+ @reconnect = original
284
333
  end
285
334
 
286
335
  def without_reconnect(&blk)
287
336
  with_reconnect(false, &blk)
288
337
  end
289
338
 
290
- protected
339
+ protected
291
340
 
292
341
  def logging(commands)
293
- return yield unless @logger && @logger.debug?
342
+ return yield unless @logger&.debug?
294
343
 
295
344
  begin
296
345
  commands.each do |name, *args|
297
346
  logged_args = args.map do |a|
298
- case
299
- when a.respond_to?(:inspect) then a.inspect
300
- 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
301
349
  else
302
350
  # handle poorly-behaved descendants of BasicObject
303
351
  klass = a.instance_exec { (class << self; self end).superclass }
@@ -323,13 +371,17 @@ class Redis
323
371
  @connection = @options[:driver].connect(@options)
324
372
  @pending_reads = 0
325
373
  rescue TimeoutError,
374
+ SocketError,
375
+ Errno::EADDRNOTAVAIL,
326
376
  Errno::ECONNREFUSED,
327
377
  Errno::EHOSTDOWN,
328
378
  Errno::EHOSTUNREACH,
329
379
  Errno::ENETUNREACH,
330
- Errno::ETIMEDOUT
380
+ Errno::ENOENT,
381
+ Errno::ETIMEDOUT,
382
+ Errno::EINVAL => error
331
383
 
332
- raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
384
+ raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
333
385
  end
334
386
 
335
387
  def ensure_connected
@@ -343,9 +395,9 @@ class Redis
343
395
  if connected?
344
396
  unless inherit_socket? || Process.pid == @pid
345
397
  raise InheritedError,
346
- "Tried to use a connection from a child process without reconnecting. " +
347
- "You need to reconnect to Redis after forking " +
348
- "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."
349
401
  end
350
402
  else
351
403
  connect
@@ -356,6 +408,10 @@ class Redis
356
408
  disconnect
357
409
 
358
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)
359
415
  retry
360
416
  else
361
417
  raise
@@ -374,15 +430,14 @@ class Redis
374
430
 
375
431
  defaults.keys.each do |key|
376
432
  # Fill in defaults if needed
377
- if defaults[key].respond_to?(:call)
378
- defaults[key] = defaults[key].call
379
- end
433
+ defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
380
434
 
381
435
  # Symbolize only keys that are needed
382
- 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)
383
437
  end
384
438
 
385
- url = options[:url] || defaults[:url]
439
+ url = options[:url]
440
+ url = defaults[:url] if url.nil?
386
441
 
387
442
  # Override defaults from URL if given
388
443
  if url
@@ -391,17 +446,20 @@ class Redis
391
446
  uri = URI(url)
392
447
 
393
448
  if uri.scheme == "unix"
394
- defaults[:path] = uri.path
395
- elsif uri.scheme == "redis"
449
+ defaults[:path] = uri.path
450
+ elsif uri.scheme == "redis" || uri.scheme == "rediss"
396
451
  defaults[:scheme] = uri.scheme
397
452
  defaults[:host] = uri.host if uri.host
398
453
  defaults[:port] = uri.port if uri.port
399
- 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?
400
456
  defaults[:db] = uri.path[1..-1].to_i if uri.path
401
457
  defaults[:role] = :master
402
458
  else
403
459
  raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
404
460
  end
461
+
462
+ defaults[:ssl] = true if uri.scheme == "rediss"
405
463
  end
406
464
 
407
465
  # Use default when option is not specified or nil
@@ -420,33 +478,40 @@ class Redis
420
478
  options[:port] = options[:port].to_i
421
479
  end
422
480
 
423
- options[:timeout] = options[:timeout].to_f
424
- options[:connect_timeout] = if options[:connect_timeout]
425
- options[:connect_timeout].to_f
426
- else
427
- options[:timeout]
481
+ if options.key?(:timeout)
482
+ options[:connect_timeout] ||= options[:timeout]
483
+ options[:read_timeout] ||= options[:timeout]
484
+ options[:write_timeout] ||= options[:timeout]
428
485
  end
429
486
 
487
+ options[:connect_timeout] = Float(options[:connect_timeout])
488
+ options[:read_timeout] = Float(options[:read_timeout])
489
+ options[:write_timeout] = Float(options[:write_timeout])
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
+
430
495
  options[:db] = options[:db].to_i
431
496
  options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
432
497
 
433
498
  case options[:tcp_keepalive]
434
499
  when Hash
435
- [:time, :intvl, :probes].each do |key|
436
- unless options[:tcp_keepalive][key].is_a?(Fixnum)
437
- raise "Expected the #{key.inspect} key in :tcp_keepalive to be a Fixnum"
500
+ %i[time intvl probes].each do |key|
501
+ unless options[:tcp_keepalive][key].is_a?(Integer)
502
+ raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
438
503
  end
439
504
  end
440
505
 
441
- when Fixnum
506
+ when Integer
442
507
  if options[:tcp_keepalive] >= 60
443
- 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 }
444
509
 
445
510
  elsif options[:tcp_keepalive] >= 30
446
- 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 }
447
512
 
448
513
  elsif options[:tcp_keepalive] >= 5
449
- 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 }
450
515
  end
451
516
  end
452
517
 
@@ -458,13 +523,18 @@ class Redis
458
523
  def _parse_driver(driver)
459
524
  driver = driver.to_s if driver.is_a?(Symbol)
460
525
 
461
- if driver.kind_of?(String)
526
+ if driver.is_a?(String)
462
527
  begin
463
- require "redis/connection/#{driver}"
464
- driver = Connection.const_get(driver.capitalize)
528
+ require_relative "connection/#{driver}"
465
529
  rescue LoadError, NameError
466
- 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
467
535
  end
536
+
537
+ driver = Connection.const_get(driver.capitalize)
468
538
  end
469
539
 
470
540
  driver
@@ -479,18 +549,16 @@ class Redis
479
549
  @options
480
550
  end
481
551
 
482
- def check(client)
483
- end
552
+ def check(client); end
484
553
 
485
554
  class Sentinel < Connector
486
555
  def initialize(options)
487
556
  super(options)
488
557
 
489
- @options[:password] = DEFAULTS.fetch(:password)
490
558
  @options[:db] = DEFAULTS.fetch(:db)
491
559
 
492
560
  @sentinels = @options.delete(:sentinels).dup
493
- @role = @options.fetch(:role, "master").to_s
561
+ @role = (@options[:role] || "master").to_s
494
562
  @master = @options[:host]
495
563
  end
496
564
 
@@ -513,13 +581,13 @@ class Redis
513
581
 
514
582
  def resolve
515
583
  result = case @role
516
- when "master"
517
- resolve_master
518
- when "slave"
519
- resolve_slave
520
- else
521
- raise ArgumentError, "Unknown instance role #{@role}"
522
- end
584
+ when "master"
585
+ resolve_master
586
+ when "slave"
587
+ resolve_slave
588
+ else
589
+ raise ArgumentError, "Unknown instance role #{@role}"
590
+ end
523
591
 
524
592
  result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
525
593
  end
@@ -527,10 +595,12 @@ class Redis
527
595
  def sentinel_detect
528
596
  @sentinels.each do |sentinel|
529
597
  client = Client.new(@options.merge({
530
- :host => sentinel[:host],
531
- :port => sentinel[:port],
532
- :reconnect_attempts => 0,
533
- }))
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
+ }))
534
604
 
535
605
  begin
536
606
  if result = yield(client)
@@ -552,7 +622,7 @@ class Redis
552
622
  def resolve_master
553
623
  sentinel_detect do |client|
554
624
  if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
555
- {:host => reply[0], :port => reply[1]}
625
+ { host: reply[0], port: reply[1] }
556
626
  end
557
627
  end
558
628
  end
@@ -560,9 +630,19 @@ class Redis
560
630
  def resolve_slave
561
631
  sentinel_detect do |client|
562
632
  if reply = client.call(["sentinel", "slaves", @master])
563
- slave = Hash[*reply.sample]
564
-
565
- {: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
566
646
  end
567
647
  end
568
648
  end