redis 4.1.4 → 4.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83f1f7270db68603d63e86ec43e68348cb5ccb2b4e6759642d89898566bdbaf6
4
- data.tar.gz: 45c5bcc92629ec7d85cdc2b913e7922cd5425f2e6691891efc379aeec73026b3
3
+ metadata.gz: 7a7232fef186e6d6a11a90d8dd9aa7c71114f017e0afe9378999a96c9e6b4e05
4
+ data.tar.gz: 689f176b87909bf61eb60e57d1eb795198162a7e039104c80facf36880964bda
5
5
  SHA512:
6
- metadata.gz: 692dfc5c73c6410492589f38f279976a023f6a2ff13f7b1476806011eb387f41bed784bdeac746de5f4b990b6d22bf297b36dddc7b8e448a842241a389f50796
7
- data.tar.gz: 55a9e305c7563f5dd7d38f50dc7b919967dbb0f6a131ebc5e1569f49f196ab458203b6594394fa9a33ea9e337b741113e781378113783683dd36b87196607b8f
6
+ metadata.gz: 22b11dee92e298b46bb94a707156d3dbf9afb83c8e9e8cbf82cf366d582a7c1b7295d7f09a0fec01965245f4800c2482c0559d646f46ff1c6bba6423ab398ba9
7
+ data.tar.gz: 9a74ba29c8cb3d7634a44c78e30018a048e19cca66c7fad6a226722183f66546bebb370c92ecdf20522bb35ee72a09b2fda64891ff6d94db1702375bd1ba6b46
data/CHANGELOG.md CHANGED
@@ -1,5 +1,81 @@
1
1
  # Unreleased
2
2
 
3
+ # 4.5.1
4
+
5
+ * Restore the accidential auth behavior of redis-rb 4.3.0 with a warning. If provided with the `default` user's password, but a wrong username,
6
+ redis-rb will first try to connect as the provided user, but then will fallback to connect as the `default` user with the provided password.
7
+ This behavior is deprecated and will be removed in Redis 4.6.0. Fix #1038.
8
+
9
+ # 4.5.0
10
+
11
+ * Handle parts of the command using incompatible encodings. See #1037.
12
+ * Add GET option to SET command. See #1036.
13
+ * Add ZRANDMEMBER command. See #1035.
14
+ * Add LMOVE/BLMOVE commands. See #1034.
15
+ * Add ZMSCORE command. See #1032.
16
+ * Add LT/GT options to ZADD. See #1033.
17
+ * Add SMISMEMBER command. See #1031.
18
+ * Add EXAT/PXAT options to SET. See #1028.
19
+ * Add GETDEL/GETEX commands. See #1024.
20
+ * `Redis#exists` now returns an Integer by default, as warned since 4.2.0. The old behavior can be restored with `Redis.exists_returns_integer = false`.
21
+ * Fix Redis < 6 detection during connect. See #1025.
22
+ * Fix fetching command details in Redis cluster when the first node is unhealthy. See #1026.
23
+
24
+ # 4.4.0
25
+
26
+ * Redis cluster: fix cross-slot validation in pipelines. Fix ##1019.
27
+ * Add support for `XAUTOCLAIM`. See #1018.
28
+ * Properly issue `READONLY` when reconnecting to replicas. Fix #1017.
29
+ * Make `del` a noop if passed an empty list of keys. See #998.
30
+ * Add support for `ZINTER`. See #995.
31
+
32
+ # 4.3.1
33
+
34
+ * Fix password authentication against redis server 5 and older.
35
+
36
+ # 4.3.0
37
+
38
+ * Add the TYPE argument to scan and scan_each. See #985.
39
+ * Support AUTH command for ACL. See #967.
40
+
41
+ # 4.2.5
42
+
43
+ * Optimize the ruby connector write buffering. See #964.
44
+
45
+ # 4.2.4
46
+
47
+ * Fix bytesize calculations in the ruby connector, and work on a copy of the buffer. Fix #961, #962.
48
+
49
+ # 4.2.3
50
+
51
+ * Use io/wait instead of IO.select in the ruby connector. See #960.
52
+ * Use exception free non blocking IOs in the ruby connector. See #926.
53
+ * Prevent corruption of the client when an interrupt happen during inside a pipeline block. See #945.
54
+
55
+ # 4.2.2
56
+
57
+ * Fix `WATCH` support for `Redis::Distributed`. See #941.
58
+ * Fix handling of empty stream responses. See #905, #929.
59
+
60
+ # 4.2.1
61
+
62
+ * Fix `exists?` returning an actual boolean when called with multiple keys. See #918.
63
+ * Setting `Redis.exists_returns_integer = false` disables warning message about new behaviour. See #920.
64
+
65
+ # 4.2.0
66
+
67
+ * Convert commands to accept keyword arguments rather than option hashes. This both help catching typos, and reduce needless allocations.
68
+ * Deprecate the synchrony driver. It will be removed in 5.0 and hopefully maintained as a separate gem. See #915.
69
+ * Make `Redis#exists` variadic, will return an Integer if called with multiple keys.
70
+ * Add `Redis#exists?` to get a Boolean if any of the keys exists.
71
+ * `Redis#exists` when called with a single key will warn that future versions will return an Integer.
72
+ Set `Redis.exists_returns_integer = true` to opt-in to the new behavior.
73
+ * Support `keepttl` ooption in `set`. See #913.
74
+ * Optimized initialization of Redis::Cluster. See #912.
75
+ * Accept sentinel options even with string key. See #599.
76
+ * Verify TLS connections by default. See #900.
77
+ * Make `Redis#hset` variadic. It now returns an integer, not a boolean. See #910.
78
+
3
79
  # 4.1.4
4
80
 
5
81
  * Alias `Redis#disconnect` as `#close`. See #901.
@@ -9,6 +85,7 @@
9
85
  * Increase buffer size in the ruby connector. See #880.
10
86
  * Fix thread safety of `Redis.queue`. See #878.
11
87
  * Deprecate `Redis::Future#==` as it's likely to be a mistake. See #876.
88
+ * Support `KEEPTTL` option for SET command. See #913.
12
89
 
13
90
  # 4.1.3
14
91
 
data/README.md CHANGED
@@ -1,8 +1,9 @@
1
- # redis-rb [![Build Status][travis-image]][travis-link] [![Inline docs][inchpages-image]][inchpages-link] ![](https://github.com/redis/redis-rb/workflows/Test/badge.svg?branch=master)
1
+ # redis-rb [![Build Status][gh-actions-image]][gh-actions-link] [![Inline docs][inchpages-image]][inchpages-link]
2
2
 
3
3
  A Ruby client that tries to match [Redis][redis-home]' API one-to-one, while still
4
4
  providing an idiomatic interface.
5
5
 
6
+ See [RubyDoc.info][rubydoc] for the API docs of the latest published gem.
6
7
 
7
8
  ## Getting started
8
9
 
@@ -34,6 +35,9 @@ You can also specify connection options as a [`redis://` URL][redis-url]:
34
35
  redis = Redis.new(url: "redis://:p4ssw0rd@10.0.1.1:6380/15")
35
36
  ```
36
37
 
38
+ The client expects passwords with special chracters to be URL-encoded (i.e.
39
+ `CGI.escape(password)`).
40
+
37
41
  By default, the client will try to read the `REDIS_URL` environment variable
38
42
  and use that as URL to connect to. The above statement is therefore equivalent
39
43
  to setting this environment variable and calling `Redis.new` without arguments.
@@ -50,6 +54,12 @@ To connect to a password protected Redis instance, use:
50
54
  redis = Redis.new(password: "mysecret")
51
55
  ```
52
56
 
57
+ To connect a Redis instance using [ACL](https://redis.io/topics/acl), use:
58
+
59
+ ```ruby
60
+ redis = Redis.new(username: 'myname', password: 'mysecret')
61
+ ```
62
+
53
63
  The Redis class exports methods that are named identical to the commands
54
64
  they execute. The arguments these methods accept are often identical to
55
65
  the arguments specified on the [Redis website][redis-commands]. For
@@ -147,8 +157,8 @@ redis.mget('{key}1', '{key}2')
147
157
 
148
158
  ## Storing objects
149
159
 
150
- Redis only stores strings as values. If you want to store an object, you
151
- can use a serialization mechanism such as JSON:
160
+ Redis "string" types can be used to store serialized Ruby objects, for
161
+ example with JSON:
152
162
 
153
163
  ```ruby
154
164
  require "json"
@@ -261,6 +271,7 @@ All timeout values are specified in seconds.
261
271
  When using pub/sub, you can subscribe to a channel using a timeout as well:
262
272
 
263
273
  ```ruby
274
+ redis = Redis.new(reconnect_attempts: 0)
264
275
  redis.subscribe_with_timeout(5, "news") do |on|
265
276
  on.message do |channel, message|
266
277
  # ...
@@ -322,7 +333,7 @@ This library supports natively terminating client side SSL/TLS connections
322
333
  when talking to Redis via a server-side proxy such as [stunnel], [hitch],
323
334
  or [ghostunnel].
324
335
 
325
- To enable SSL support, pass the `:ssl => :true` option when configuring the
336
+ To enable SSL support, pass the `:ssl => true` option when configuring the
326
337
  Redis client, or pass in `:url => "rediss://..."` (like HTTPS for Redis).
327
338
  You will also need to pass in an `:ssl_params => { ... }` hash used to
328
339
  configure the `OpenSSL::SSL::SSLContext` object used for the connection:
@@ -435,7 +446,7 @@ redis = Redis.new(:driver => :synchrony)
435
446
  ## Testing
436
447
 
437
448
  This library is tested against recent Ruby and Redis versions.
438
- Check [Travis][travis-link] for the exact versions supported.
449
+ Check [Github Actions][gh-actions-link] for the exact versions supported.
439
450
 
440
451
  ## See Also
441
452
 
@@ -451,15 +462,14 @@ client and evangelized Redis in Rubyland. Thank you, Ezra.
451
462
  ## Contributing
452
463
 
453
464
  [Fork the project](https://github.com/redis/redis-rb) and send pull
454
- requests. You can also ask for help at `#redis-rb` on Freenode.
455
-
456
-
457
- [inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg
458
- [inchpages-link]: https://inch-ci.org/github/redis/redis-rb
459
- [redis-commands]: https://redis.io/commands
460
- [redis-home]: https://redis.io
461
- [redis-url]: http://www.iana.org/assignments/uri-schemes/prov/redis
462
- [travis-home]: https://travis-ci.org/
463
- [travis-image]: https://secure.travis-ci.org/redis/redis-rb.svg?branch=master
464
- [travis-link]: https://travis-ci.org/redis/redis-rb
465
- [rubydoc]: http://www.rubydoc.info/gems/redis
465
+ requests.
466
+
467
+
468
+ [inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg
469
+ [inchpages-link]: https://inch-ci.org/github/redis/redis-rb
470
+ [redis-commands]: https://redis.io/commands
471
+ [redis-home]: https://redis.io
472
+ [redis-url]: http://www.iana.org/assignments/uri-schemes/prov/redis
473
+ [gh-actions-image]: https://github.com/redis/redis-rb/workflows/Test/badge.svg
474
+ [gh-actions-link]: https://github.com/redis/redis-rb/actions
475
+ [rubydoc]: http://www.rubydoc.info/gems/redis
data/lib/redis/client.rb CHANGED
@@ -6,24 +6,31 @@ require "cgi"
6
6
 
7
7
  class Redis
8
8
  class Client
9
-
9
+ # Defaults are also used for converting string keys to symbols.
10
10
  DEFAULTS = {
11
- :url => lambda { ENV["REDIS_URL"] },
12
- :scheme => "redis",
13
- :host => "127.0.0.1",
14
- :port => 6379,
15
- :path => nil,
16
- :timeout => 5.0,
17
- :password => nil,
18
- :db => 0,
19
- :driver => nil,
20
- :id => nil,
21
- :tcp_keepalive => 0,
22
- :reconnect_attempts => 1,
23
- :reconnect_delay => 0,
24
- :reconnect_delay_max => 0.5,
25
- :inherit_socket => false
26
- }
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
27
34
 
28
35
  attr_reader :options
29
36
 
@@ -55,6 +62,10 @@ class Redis
55
62
  @options[:read_timeout]
56
63
  end
57
64
 
65
+ def username
66
+ @options[:username]
67
+ end
68
+
58
69
  def password
59
70
  @options[:password]
60
71
  end
@@ -89,7 +100,7 @@ class Redis
89
100
  @pending_reads = 0
90
101
 
91
102
  @connector =
92
- if options.include?(:sentinels)
103
+ if !@options[:sentinels].nil?
93
104
  Connector::Sentinel.new(@options)
94
105
  elsif options.include?(:connector) && options[:connector].respond_to?(:new)
95
106
  options.delete(:connector).new(@options)
@@ -104,7 +115,33 @@ class Redis
104
115
  # Don't try to reconnect when the connection is fresh
105
116
  with_reconnect(false) do
106
117
  establish_connection
107
- call [:auth, password] if password
118
+ if password
119
+ if username
120
+ begin
121
+ call [:auth, username, password]
122
+ rescue CommandError => err # Likely on Redis < 6
123
+ if err.message.match?(/ERR wrong number of arguments for \'auth\' command/)
124
+ call [:auth, password]
125
+ elsif err.message.match?(/WRONGPASS invalid username-password pair/)
126
+ begin
127
+ call [:auth, password]
128
+ rescue CommandError
129
+ raise err
130
+ end
131
+ ::Kernel.warn(
132
+ "[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
133
+ " the provided password was for the default user. This will start failing in redis-rb 4.6."
134
+ )
135
+ else
136
+ raise
137
+ end
138
+ end
139
+ else
140
+ call [:auth, password]
141
+ end
142
+ end
143
+
144
+ call [:readonly] if @options[:readonly]
108
145
  call [:select, db] if db != 0
109
146
  call [:client, :setname, @options[:id]] if @options[:id]
110
147
  @connector.check(self)
@@ -125,7 +162,7 @@ class Redis
125
162
  reply = process([command]) { read }
126
163
  raise reply if reply.is_a?(CommandError)
127
164
 
128
- if block_given?
165
+ if block_given? && reply != 'QUEUED'
129
166
  yield reply
130
167
  else
131
168
  reply
@@ -166,6 +203,7 @@ class Redis
166
203
  end
167
204
  rescue ConnectionError => e
168
205
  return nil if pipeline.shutdown?
206
+
169
207
  # Assume the pipeline was sent in one piece, but execution of
170
208
  # SHUTDOWN caused none of the replies for commands that were executed
171
209
  # prior to it from coming back around.
@@ -244,13 +282,13 @@ class Redis
244
282
  end
245
283
 
246
284
  def connected?
247
- !! (connection && connection.connected?)
285
+ !!(connection && connection.connected?)
248
286
  end
249
287
 
250
288
  def disconnect
251
289
  connection.disconnect if connected?
252
290
  end
253
- alias_method :close, :disconnect
291
+ alias close disconnect
254
292
 
255
293
  def reconnect
256
294
  disconnect
@@ -301,30 +339,27 @@ class Redis
301
339
  with_socket_timeout(0, &blk)
302
340
  end
303
341
 
304
- def with_reconnect(val=true)
305
- begin
306
- original, @reconnect = @reconnect, val
307
- yield
308
- ensure
309
- @reconnect = original
310
- end
342
+ def with_reconnect(val = true)
343
+ original, @reconnect = @reconnect, val
344
+ yield
345
+ ensure
346
+ @reconnect = original
311
347
  end
312
348
 
313
349
  def without_reconnect(&blk)
314
350
  with_reconnect(false, &blk)
315
351
  end
316
352
 
317
- protected
353
+ protected
318
354
 
319
355
  def logging(commands)
320
- return yield unless @logger && @logger.debug?
356
+ return yield unless @logger&.debug?
321
357
 
322
358
  begin
323
359
  commands.each do |name, *args|
324
360
  logged_args = args.map do |a|
325
- case
326
- when a.respond_to?(:inspect) then a.inspect
327
- when a.respond_to?(:to_s) then a.to_s
361
+ if a.respond_to?(:inspect) then a.inspect
362
+ elsif a.respond_to?(:to_s) then a.to_s
328
363
  else
329
364
  # handle poorly-behaved descendants of BasicObject
330
365
  klass = a.instance_exec { (class << self; self end).superclass }
@@ -358,9 +393,9 @@ class Redis
358
393
  Errno::ENETUNREACH,
359
394
  Errno::ENOENT,
360
395
  Errno::ETIMEDOUT,
361
- Errno::EINVAL
396
+ Errno::EINVAL => error
362
397
 
363
- raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
398
+ raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
364
399
  end
365
400
 
366
401
  def ensure_connected
@@ -374,9 +409,9 @@ class Redis
374
409
  if connected?
375
410
  unless inherit_socket? || Process.pid == @pid
376
411
  raise InheritedError,
377
- "Tried to use a connection from a child process without reconnecting. " +
378
- "You need to reconnect to Redis after forking " +
379
- "or set :inherit_socket to true."
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."
380
415
  end
381
416
  else
382
417
  connect
@@ -387,7 +422,7 @@ class Redis
387
422
  disconnect
388
423
 
389
424
  if attempts <= @options[:reconnect_attempts] && @reconnect
390
- sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
425
+ sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
391
426
  @options[:reconnect_delay_max]].min
392
427
 
393
428
  Kernel.sleep(sleep_t)
@@ -409,16 +444,14 @@ class Redis
409
444
 
410
445
  defaults.keys.each do |key|
411
446
  # Fill in defaults if needed
412
- if defaults[key].respond_to?(:call)
413
- defaults[key] = defaults[key].call
414
- end
447
+ defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
415
448
 
416
449
  # Symbolize only keys that are needed
417
- options[key] = options[key.to_s] if options.has_key?(key.to_s)
450
+ options[key] = options[key.to_s] if options.key?(key.to_s)
418
451
  end
419
452
 
420
453
  url = options[:url]
421
- url = defaults[:url] if url == nil
454
+ url = defaults[:url] if url.nil?
422
455
 
423
456
  # Override defaults from URL if given
424
457
  if url
@@ -427,12 +460,13 @@ class Redis
427
460
  uri = URI(url)
428
461
 
429
462
  if uri.scheme == "unix"
430
- defaults[:path] = uri.path
463
+ defaults[:path] = uri.path
431
464
  elsif uri.scheme == "redis" || uri.scheme == "rediss"
432
465
  defaults[:scheme] = uri.scheme
433
466
  defaults[:host] = uri.host if uri.host
434
467
  defaults[:port] = uri.port if uri.port
435
- defaults[:password] = CGI.unescape(uri.password) if uri.password
468
+ defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
469
+ defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
436
470
  defaults[:db] = uri.path[1..-1].to_i if uri.path
437
471
  defaults[:role] = :master
438
472
  else
@@ -458,7 +492,7 @@ class Redis
458
492
  options[:port] = options[:port].to_i
459
493
  end
460
494
 
461
- if options.has_key?(:timeout)
495
+ if options.key?(:timeout)
462
496
  options[:connect_timeout] ||= options[:timeout]
463
497
  options[:read_timeout] ||= options[:timeout]
464
498
  options[:write_timeout] ||= options[:timeout]
@@ -477,7 +511,7 @@ class Redis
477
511
 
478
512
  case options[:tcp_keepalive]
479
513
  when Hash
480
- [:time, :intvl, :probes].each do |key|
514
+ %i[time intvl probes].each do |key|
481
515
  unless options[:tcp_keepalive][key].is_a?(Integer)
482
516
  raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
483
517
  end
@@ -485,13 +519,13 @@ class Redis
485
519
 
486
520
  when Integer
487
521
  if options[:tcp_keepalive] >= 60
488
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 20, :intvl => 10, :probes => 2}
522
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
489
523
 
490
524
  elsif options[:tcp_keepalive] >= 30
491
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 10, :intvl => 5, :probes => 2}
525
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
492
526
 
493
527
  elsif options[:tcp_keepalive] >= 5
494
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 2, :intvl => 2, :probes => 1}
528
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
495
529
  end
496
530
  end
497
531
 
@@ -503,14 +537,14 @@ class Redis
503
537
  def _parse_driver(driver)
504
538
  driver = driver.to_s if driver.is_a?(Symbol)
505
539
 
506
- if driver.kind_of?(String)
540
+ if driver.is_a?(String)
507
541
  begin
508
542
  require_relative "connection/#{driver}"
509
- rescue LoadError, NameError => e
543
+ rescue LoadError, NameError
510
544
  begin
511
- require "connection/#{driver}"
512
- rescue LoadError, NameError => e
513
- raise RuntimeError, "Cannot load driver #{driver.inspect}: #{e.message}"
545
+ require "redis/connection/#{driver}"
546
+ rescue LoadError, NameError => error
547
+ raise "Cannot load driver #{driver.inspect}: #{error.message}"
514
548
  end
515
549
  end
516
550
 
@@ -529,8 +563,7 @@ class Redis
529
563
  @options
530
564
  end
531
565
 
532
- def check(client)
533
- end
566
+ def check(client); end
534
567
 
535
568
  class Sentinel < Connector
536
569
  def initialize(options)
@@ -539,7 +572,7 @@ class Redis
539
572
  @options[:db] = DEFAULTS.fetch(:db)
540
573
 
541
574
  @sentinels = @options.delete(:sentinels).dup
542
- @role = @options.fetch(:role, "master").to_s
575
+ @role = (@options[:role] || "master").to_s
543
576
  @master = @options[:host]
544
577
  end
545
578
 
@@ -562,13 +595,13 @@ class Redis
562
595
 
563
596
  def resolve
564
597
  result = case @role
565
- when "master"
566
- resolve_master
567
- when "slave"
568
- resolve_slave
569
- else
570
- raise ArgumentError, "Unknown instance role #{@role}"
571
- end
598
+ when "master"
599
+ resolve_master
600
+ when "slave"
601
+ resolve_slave
602
+ else
603
+ raise ArgumentError, "Unknown instance role #{@role}"
604
+ end
572
605
 
573
606
  result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
574
607
  end
@@ -576,11 +609,12 @@ class Redis
576
609
  def sentinel_detect
577
610
  @sentinels.each do |sentinel|
578
611
  client = Client.new(@options.merge({
579
- :host => sentinel[:host],
580
- :port => sentinel[:port],
581
- password: sentinel[:password],
582
- :reconnect_attempts => 0,
583
- }))
612
+ host: sentinel[:host] || sentinel["host"],
613
+ port: sentinel[:port] || sentinel["port"],
614
+ username: sentinel[:username] || sentinel["username"],
615
+ password: sentinel[:password] || sentinel["password"],
616
+ reconnect_attempts: 0
617
+ }))
584
618
 
585
619
  begin
586
620
  if result = yield(client)
@@ -602,7 +636,7 @@ class Redis
602
636
  def resolve_master
603
637
  sentinel_detect do |client|
604
638
  if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
605
- {:host => reply[0], :port => reply[1]}
639
+ { host: reply[0], port: reply[1] }
606
640
  end
607
641
  end
608
642
  end
@@ -620,7 +654,7 @@ class Redis
620
654
  slave = slaves.sample
621
655
  {
622
656
  host: slave.fetch('ip'),
623
- port: slave.fetch('port'),
657
+ port: slave.fetch('port')
624
658
  }
625
659
  end
626
660
  end
@@ -10,22 +10,21 @@ class Redis
10
10
  module_function
11
11
 
12
12
  def load(nodes)
13
- details = {}
14
-
15
13
  nodes.each do |node|
16
- details = fetch_command_details(node)
17
- details.empty? ? next : break
14
+ begin
15
+ return fetch_command_details(node)
16
+ rescue CannotConnectError, ConnectionError, CommandError
17
+ next # can retry on another node
18
+ end
18
19
  end
19
20
 
20
- details
21
+ raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
21
22
  end
22
23
 
23
24
  def fetch_command_details(node)
24
25
  node.call(%i[command]).map do |reply|
25
26
  [reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
26
27
  end.to_h
27
- rescue CannotConnectError, ConnectionError, CommandError
28
- {} # can retry on another node
29
28
  end
30
29
 
31
30
  private_class_method :fetch_command_details
@@ -39,6 +39,7 @@ class Redis
39
39
  def call_master(command, &block)
40
40
  try_map do |node_key, client|
41
41
  next if slave?(node_key)
42
+
42
43
  client.call(command, &block)
43
44
  end.values
44
45
  end
@@ -48,6 +49,7 @@ class Redis
48
49
 
49
50
  try_map do |node_key, client|
50
51
  next if master?(node_key)
52
+
51
53
  client.call(command, &block)
52
54
  end.values
53
55
  end
@@ -74,8 +76,9 @@ class Redis
74
76
  clients = options.map do |node_key, option|
75
77
  next if replica_disabled? && slave?(node_key)
76
78
 
79
+ option = option.merge(readonly: true) if slave?(node_key)
80
+
77
81
  client = Client.new(option)
78
- client.call(%i[readonly]) if slave?(node_key)
79
82
  [node_key, client]
80
83
  end
81
84
 
@@ -97,6 +100,7 @@ class Redis
97
100
  end
98
101
 
99
102
  return results if errors.empty?
103
+
100
104
  raise CommandErrorCollection, errors
101
105
  end
102
106
  end
@@ -18,6 +18,7 @@ class Redis
18
18
  @node_opts = build_node_options(node_addrs)
19
19
  @replica = options.delete(:replica) == true
20
20
  add_common_node_option_if_needed(options, @node_opts, :scheme)
21
+ add_common_node_option_if_needed(options, @node_opts, :username)
21
22
  add_common_node_option_if_needed(options, @node_opts, :password)
22
23
  @options = options
23
24
  end
@@ -43,6 +44,7 @@ class Redis
43
44
 
44
45
  def build_node_options(addrs)
45
46
  raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
47
+
46
48
  addrs.map { |addr| parse_node_addr(addr) }
47
49
  end
48
50
 
@@ -62,21 +64,25 @@ class Redis
62
64
  raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
63
65
 
64
66
  db = uri.path.split('/')[1]&.to_i
65
- { scheme: uri.scheme, password: uri.password, host: uri.host, port: uri.port, db: db }.reject { |_, v| v.nil? }
67
+
68
+ { scheme: uri.scheme, username: uri.user, password: uri.password, host: uri.host, port: uri.port, db: db }
69
+ .reject { |_, v| v.nil? || v == '' }
66
70
  rescue URI::InvalidURIError => err
67
71
  raise InvalidClientOptionError, err.message
68
72
  end
69
73
 
70
74
  def parse_node_option(addr)
71
75
  addr = addr.map { |k, v| [k.to_sym, v] }.to_h
72
- raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys' if addr.values_at(:host, :port).any?(&:nil?)
76
+ if addr.values_at(:host, :port).any?(&:nil?)
77
+ raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
78
+ end
73
79
 
74
80
  addr
75
81
  end
76
82
 
77
83
  # Redis cluster node returns only host and port information.
78
84
  # So we should complement additional information such as:
79
- # scheme, password and so on.
85
+ # scheme, username, password and so on.
80
86
  def add_common_node_option_if_needed(options, node_opts, key)
81
87
  return options if options[key].nil? && node_opts.first[key].nil?
82
88