redis 4.2.5 → 4.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +113 -0
  3. data/README.md +41 -21
  4. data/lib/redis/client.rb +52 -27
  5. data/lib/redis/cluster/command.rb +4 -6
  6. data/lib/redis/cluster/command_loader.rb +8 -9
  7. data/lib/redis/cluster/node.rb +14 -1
  8. data/lib/redis/cluster/node_loader.rb +8 -11
  9. data/lib/redis/cluster/option.rb +14 -4
  10. data/lib/redis/cluster/slot_loader.rb +9 -12
  11. data/lib/redis/cluster.rb +33 -13
  12. data/lib/redis/commands/bitmaps.rb +63 -0
  13. data/lib/redis/commands/cluster.rb +45 -0
  14. data/lib/redis/commands/connection.rb +58 -0
  15. data/lib/redis/commands/geo.rb +84 -0
  16. data/lib/redis/commands/hashes.rb +251 -0
  17. data/lib/redis/commands/hyper_log_log.rb +37 -0
  18. data/lib/redis/commands/keys.rb +455 -0
  19. data/lib/redis/commands/lists.rb +290 -0
  20. data/lib/redis/commands/pubsub.rb +72 -0
  21. data/lib/redis/commands/scripting.rb +114 -0
  22. data/lib/redis/commands/server.rb +188 -0
  23. data/lib/redis/commands/sets.rb +223 -0
  24. data/lib/redis/commands/sorted_sets.rb +812 -0
  25. data/lib/redis/commands/streams.rb +382 -0
  26. data/lib/redis/commands/strings.rb +313 -0
  27. data/lib/redis/commands/transactions.rb +139 -0
  28. data/lib/redis/commands.rb +240 -0
  29. data/lib/redis/connection/command_helper.rb +2 -0
  30. data/lib/redis/connection/hiredis.rb +3 -2
  31. data/lib/redis/connection/ruby.rb +19 -9
  32. data/lib/redis/connection/synchrony.rb +10 -8
  33. data/lib/redis/connection.rb +1 -1
  34. data/lib/redis/distributed.rb +124 -29
  35. data/lib/redis/errors.rb +9 -0
  36. data/lib/redis/pipeline.rb +128 -3
  37. data/lib/redis/version.rb +1 -1
  38. data/lib/redis.rb +139 -3377
  39. metadata +22 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 862e8133262fead707e4ca317b29d0e35425e3a7861ea1a52033bc91ba61bf6d
4
- data.tar.gz: 5b2b32250d783fe58b0af54cc386f2568241644a0badc0b7247bb29b1ac94a93
3
+ metadata.gz: b18788ff80698e8f79fb103e7419d9ba74fb0b6a5eb55c672422cd76abff985c
4
+ data.tar.gz: 1eed18a57039c677c894564ceaa1cf6bc9c0535be51501d078b09d75617a9d89
5
5
  SHA512:
6
- metadata.gz: 2aab289f4f22b2f3a804ca7b0da4cf95e352a8d246611490d8d803edebbbc7e7c299355cd0b90e6f3ac8bc0d11e9bf3792a328c95847d32e1d984729afe66ed2
7
- data.tar.gz: d9ec8ba4d314d099e909cdddf6dfb4c3c14b853b46f45c4909ac3ba10fb1880bd9a7b0465257fa91f9a4ea6c9f82723c16c177810ac15c89fab3790d7af31ad0
6
+ metadata.gz: 5f2f7ce595d431f548c126a63c5ce697cc9e596e9b0eaa00f1e0186ae3853e73922c6e130b989ba9e026aec32f766a774433fdd1cff216420e837837db90659b
7
+ data.tar.gz: 02fb8debb0d11f7b9d04f7616093535426f0ee5a0994992fcf3187ab00aa890dcd2fb248f14da49837508a3ccbb5756be208fc5d3b8dcd18ec6aaf0fb1bb1193
data/CHANGELOG.md CHANGED
@@ -1,5 +1,118 @@
1
1
  # Unreleased
2
2
 
3
+ # 4.8.1
4
+
5
+ * Automatically reconnect after fork regardless of `reconnect_attempts`
6
+
7
+ # 4.8.0
8
+
9
+ * Introduce `sadd?` and `srem?` as boolean returning versions of `sadd` and `srem`.
10
+ * Deprecate `sadd` and `srem` returning a boolean when called with a single argument.
11
+ To enable the redis 5.0 behavior you can set `Redis.sadd_returns_boolean = false`.
12
+ * Deprecate passing `timeout` as a positional argument in blocking commands (`brpop`, `blop`, etc).
13
+
14
+ # 4.7.1
15
+
16
+ * Gracefully handle OpenSSL 3.0 EOF Errors (`OpenSSL::SSL::SSLError: SSL_read: unexpected eof while reading`). See #1106
17
+ This happens frequently on heroku-22.
18
+
19
+ # 4.7.0
20
+
21
+ * Support single endpoint architecture with SSL/TLS in cluster mode. See #1086.
22
+ * `zrem` and `zadd` act as noop when provided an empty list of keys. See #1097.
23
+ * Support IPv6 URLs.
24
+ * Add `Redis#with` for better compatibility with `connection_pool` usage.
25
+ * Fix the block form of `multi` called inside `pipelined`. Previously the `MUTLI/EXEC` wouldn't be sent. See #1073.
26
+
27
+ # 4.6.0
28
+
29
+ * Deprecate `Redis.current`.
30
+ * Deprecate calling commands on `Redis` inside `Redis#pipelined`. See #1059.
31
+ ```ruby
32
+ redis.pipelined do
33
+ redis.get("key")
34
+ end
35
+ ```
36
+
37
+ should be replaced by:
38
+
39
+ ```ruby
40
+ redis.pipelined do |pipeline|
41
+ pipeline.get("key")
42
+ end
43
+ ```
44
+ * Deprecate calling commands on `Redis` inside `Redis#multi`. See #1059.
45
+ ```ruby
46
+ redis.multi do
47
+ redis.get("key")
48
+ end
49
+ ```
50
+
51
+ should be replaced by:
52
+
53
+ ```ruby
54
+ redis.multi do |transaction|
55
+ transaction.get("key")
56
+ end
57
+ ```
58
+ * Deprecate `Redis#queue` and `Redis#commit`. See #1059.
59
+
60
+ * Fix `zpopmax` and `zpopmin` when called inside a pipeline. See #1055.
61
+ * `Redis#synchronize` is now private like it should always have been.
62
+
63
+ * Add `Redis.silence_deprecations=` to turn off deprecation warnings.
64
+ If you don't wish to see warnings yet, you can set `Redis.silence_deprecations = true`.
65
+ It is however heavily recommended to fix them instead when possible.
66
+ * Add `Redis.raise_deprecations=` to turn deprecation warnings into errors.
67
+ This makes it easier to identitify the source of deprecated APIs usage.
68
+ It is recommended to set `Redis.raise_deprecations = true` in development and test environments.
69
+ * Add new options to ZRANGE. See #1053.
70
+ * Add ZRANGESTORE command. See #1053.
71
+ * Add SCAN support for `Redis::Cluster`. See #1049.
72
+ * Add COPY command. See #1053. See #1048.
73
+ * Add ZDIFFSTORE command. See #1046.
74
+ * Add ZDIFF command. See #1044.
75
+ * Add ZUNION command. See #1042.
76
+ * Add HRANDFIELD command. See #1040.
77
+
78
+ # 4.5.1
79
+
80
+ * 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,
81
+ 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.
82
+ This behavior is deprecated and will be removed in Redis 4.6.0. Fix #1038.
83
+
84
+ # 4.5.0
85
+
86
+ * Handle parts of the command using incompatible encodings. See #1037.
87
+ * Add GET option to SET command. See #1036.
88
+ * Add ZRANDMEMBER command. See #1035.
89
+ * Add LMOVE/BLMOVE commands. See #1034.
90
+ * Add ZMSCORE command. See #1032.
91
+ * Add LT/GT options to ZADD. See #1033.
92
+ * Add SMISMEMBER command. See #1031.
93
+ * Add EXAT/PXAT options to SET. See #1028.
94
+ * Add GETDEL/GETEX commands. See #1024.
95
+ * `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`.
96
+ * Fix Redis < 6 detection during connect. See #1025.
97
+ * Fix fetching command details in Redis cluster when the first node is unhealthy. See #1026.
98
+
99
+ # 4.4.0
100
+
101
+ * Redis cluster: fix cross-slot validation in pipelines. Fix ##1019.
102
+ * Add support for `XAUTOCLAIM`. See #1018.
103
+ * Properly issue `READONLY` when reconnecting to replicas. Fix #1017.
104
+ * Make `del` a noop if passed an empty list of keys. See #998.
105
+ * Add support for `ZINTER`. See #995.
106
+
107
+ # 4.3.1
108
+
109
+ * Fix password authentication against redis server 5 and older.
110
+
111
+ # 4.3.0
112
+
113
+ * Add the TYPE argument to scan and scan_each. See #985.
114
+ * Support AUTH command for ACL. See #967.
115
+
3
116
  # 4.2.5
4
117
 
5
118
  * Optimize the ruby connector write buffering. See #964.
data/README.md CHANGED
@@ -1,4 +1,4 @@
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.
@@ -54,6 +54,12 @@ To connect to a password protected Redis instance, use:
54
54
  redis = Redis.new(password: "mysecret")
55
55
  ```
56
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
+
57
63
  The Redis class exports methods that are named identical to the commands
58
64
  they execute. The arguments these methods accept are often identical to
59
65
  the arguments specified on the [Redis website][redis-commands]. For
@@ -149,6 +155,21 @@ redis.mget('{key}1', '{key}2')
149
155
  * The client support permanent node failures, and will reroute requests to promoted slaves.
150
156
  * The client supports `MOVED` and `ASK` redirections transparently.
151
157
 
158
+ ## Cluster mode with SSL/TLS
159
+ Since Redis can return FQDN of nodes in reply to client since `7.*` with CLUSTER commands, we can use cluster feature with SSL/TLS connection like this:
160
+
161
+ ```ruby
162
+ Redis.new(cluster: %w[rediss://foo.example.com:6379])
163
+ ```
164
+
165
+ On the other hand, in Redis versions prior to `6.*`, you can specify options like the following if cluster mode is enabled and client has to connect to nodes via single endpoint with SSL/TLS.
166
+
167
+ ```ruby
168
+ Redis.new(cluster: %w[rediss://foo-endpoint.example.com:6379], fixed_hostname: 'foo-endpoint.example.com')
169
+ ```
170
+
171
+ In case of the above architecture, if you don't pass the `fixed_hostname` option to the client and servers return IP addresses of nodes, the client may fail to verify certificates.
172
+
152
173
  ## Storing objects
153
174
 
154
175
  Redis "string" types can be used to store serialized Ruby objects, for
@@ -178,9 +199,9 @@ commands to Redis and gathers their replies. These replies are returned
178
199
  by the `#pipelined` method.
179
200
 
180
201
  ```ruby
181
- redis.pipelined do
182
- redis.set "foo", "bar"
183
- redis.incr "baz"
202
+ redis.pipelined do |pipeline|
203
+ pipeline.set "foo", "bar"
204
+ pipeline.incr "baz"
184
205
  end
185
206
  # => ["OK", 1]
186
207
  ```
@@ -194,9 +215,9 @@ the regular pipeline, the replies to the commands are returned by the
194
215
  `#multi` method.
195
216
 
196
217
  ```ruby
197
- redis.multi do
198
- redis.set "foo", "bar"
199
- redis.incr "baz"
218
+ redis.multi do |transaction|
219
+ transaction.set "foo", "bar"
220
+ transaction.incr "baz"
200
221
  end
201
222
  # => ["OK", 1]
202
223
  ```
@@ -204,15 +225,15 @@ end
204
225
  ### Futures
205
226
 
206
227
  Replies to commands in a pipeline can be accessed via the *futures* they
207
- emit (since redis-rb 3.0). All calls inside a pipeline block return a
228
+ emit (since redis-rb 3.0). All calls on the pipeline object return a
208
229
  `Future` object, which responds to the `#value` method. When the
209
230
  pipeline has successfully executed, all futures are assigned their
210
231
  respective replies and can be used.
211
232
 
212
233
  ```ruby
213
- redis.pipelined do
214
- @set = redis.set "foo", "bar"
215
- @incr = redis.incr "baz"
234
+ redis.pipelined do |pipeline|
235
+ @set = pipeline.set "foo", "bar"
236
+ @incr = pipeline.incr "baz"
216
237
  end
217
238
 
218
239
  @set.value
@@ -440,7 +461,7 @@ redis = Redis.new(:driver => :synchrony)
440
461
  ## Testing
441
462
 
442
463
  This library is tested against recent Ruby and Redis versions.
443
- Check [Travis][travis-link] for the exact versions supported.
464
+ Check [Github Actions][gh-actions-link] for the exact versions supported.
444
465
 
445
466
  ## See Also
446
467
 
@@ -459,12 +480,11 @@ client and evangelized Redis in Rubyland. Thank you, Ezra.
459
480
  requests.
460
481
 
461
482
 
462
- [inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg
463
- [inchpages-link]: https://inch-ci.org/github/redis/redis-rb
464
- [redis-commands]: https://redis.io/commands
465
- [redis-home]: https://redis.io
466
- [redis-url]: http://www.iana.org/assignments/uri-schemes/prov/redis
467
- [travis-home]: https://travis-ci.org/
468
- [travis-image]: https://secure.travis-ci.org/redis/redis-rb.svg?branch=master
469
- [travis-link]: https://travis-ci.org/redis/redis-rb
470
- [rubydoc]: http://www.rubydoc.info/gems/redis
483
+ [inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg
484
+ [inchpages-link]: https://inch-ci.org/github/redis/redis-rb
485
+ [redis-commands]: https://redis.io/commands
486
+ [redis-home]: https://redis.io
487
+ [redis-url]: http://www.iana.org/assignments/uri-schemes/prov/redis
488
+ [gh-actions-image]: https://github.com/redis/redis-rb/workflows/Test/badge.svg
489
+ [gh-actions-link]: https://github.com/redis/redis-rb/actions
490
+ [rubydoc]: http://www.rubydoc.info/gems/redis
data/lib/redis/client.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "errors"
4
3
  require "socket"
5
4
  require "cgi"
5
+ require "redis/errors"
6
6
 
7
7
  class Redis
8
8
  class Client
@@ -17,6 +17,7 @@ class Redis
17
17
  write_timeout: nil,
18
18
  connect_timeout: nil,
19
19
  timeout: 5.0,
20
+ username: nil,
20
21
  password: nil,
21
22
  db: 0,
22
23
  driver: nil,
@@ -31,7 +32,7 @@ class Redis
31
32
  role: nil
32
33
  }.freeze
33
34
 
34
- attr_reader :options
35
+ attr_reader :options, :connection, :command_map
35
36
 
36
37
  def scheme
37
38
  @options[:scheme]
@@ -61,6 +62,10 @@ class Redis
61
62
  @options[:read_timeout]
62
63
  end
63
64
 
65
+ def username
66
+ @options[:username]
67
+ end
68
+
64
69
  def password
65
70
  @options[:password]
66
71
  end
@@ -82,8 +87,6 @@ class Redis
82
87
  end
83
88
 
84
89
  attr_accessor :logger
85
- attr_reader :connection
86
- attr_reader :command_map
87
90
 
88
91
  def initialize(options = {})
89
92
  @options = _parse_options(options)
@@ -110,7 +113,34 @@ class Redis
110
113
  # Don't try to reconnect when the connection is fresh
111
114
  with_reconnect(false) do
112
115
  establish_connection
113
- call [:auth, password] if password
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]
114
144
  call [:select, db] if db != 0
115
145
  call [:client, :setname, @options[:id]] if @options[:id]
116
146
  @connector.check(self)
@@ -120,7 +150,7 @@ class Redis
120
150
  end
121
151
 
122
152
  def id
123
- @options[:id] || "redis://#{location}/#{db}"
153
+ @options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
124
154
  end
125
155
 
126
156
  def location
@@ -131,7 +161,7 @@ class Redis
131
161
  reply = process([command]) { read }
132
162
  raise reply if reply.is_a?(CommandError)
133
163
 
134
- if block_given?
164
+ if block_given? && reply != 'QUEUED'
135
165
  yield reply
136
166
  else
137
167
  reply
@@ -221,7 +251,8 @@ class Redis
221
251
  result
222
252
  end
223
253
 
224
- def call_with_timeout(command, timeout, &blk)
254
+ def call_with_timeout(command, extra_timeout, &blk)
255
+ timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout
225
256
  with_socket_timeout(timeout) do
226
257
  call(command, &blk)
227
258
  end
@@ -271,7 +302,7 @@ class Redis
271
302
  e2 = TimeoutError.new("Connection timed out")
272
303
  e2.set_backtrace(e1.backtrace)
273
304
  raise e2
274
- rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL => e
305
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL, EOFError => e
275
306
  raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
276
307
  end
277
308
 
@@ -368,23 +399,14 @@ class Redis
368
399
  end
369
400
 
370
401
  def ensure_connected
371
- disconnect if @pending_reads > 0
402
+ disconnect if @pending_reads > 0 || (@pid != Process.pid && !inherit_socket?)
372
403
 
373
404
  attempts = 0
374
405
 
375
406
  begin
376
407
  attempts += 1
377
408
 
378
- if connected?
379
- unless inherit_socket? || Process.pid == @pid
380
- raise InheritedError,
381
- "Tried to use a connection from a child process without reconnecting. " \
382
- "You need to reconnect to Redis after forking " \
383
- "or set :inherit_socket to true."
384
- end
385
- else
386
- connect
387
- end
409
+ connect unless connected?
388
410
 
389
411
  yield
390
412
  rescue BaseConnectionError
@@ -411,7 +433,7 @@ class Redis
411
433
  defaults = DEFAULTS.dup
412
434
  options = options.dup
413
435
 
414
- defaults.keys.each do |key|
436
+ defaults.each_key do |key|
415
437
  # Fill in defaults if needed
416
438
  defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
417
439
 
@@ -428,13 +450,15 @@ class Redis
428
450
 
429
451
  uri = URI(url)
430
452
 
431
- if uri.scheme == "unix"
453
+ case uri.scheme
454
+ when "unix"
432
455
  defaults[:path] = uri.path
433
- elsif uri.scheme == "redis" || uri.scheme == "rediss"
456
+ when "redis", "rediss"
434
457
  defaults[:scheme] = uri.scheme
435
- defaults[:host] = uri.host if uri.host
458
+ defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
436
459
  defaults[:port] = uri.port if uri.port
437
- defaults[:password] = CGI.unescape(uri.password) if uri.password
460
+ defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
461
+ defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
438
462
  defaults[:db] = uri.path[1..-1].to_i if uri.path
439
463
  defaults[:role] = :master
440
464
  else
@@ -445,7 +469,7 @@ class Redis
445
469
  end
446
470
 
447
471
  # Use default when option is not specified or nil
448
- defaults.keys.each do |key|
472
+ defaults.each_key do |key|
449
473
  options[key] = defaults[key] if options[key].nil?
450
474
  end
451
475
 
@@ -510,7 +534,7 @@ class Redis
510
534
  require_relative "connection/#{driver}"
511
535
  rescue LoadError, NameError
512
536
  begin
513
- require "connection/#{driver}"
537
+ require "redis/connection/#{driver}"
514
538
  rescue LoadError, NameError => error
515
539
  raise "Cannot load driver #{driver.inspect}: #{error.message}"
516
540
  end
@@ -579,6 +603,7 @@ class Redis
579
603
  client = Client.new(@options.merge({
580
604
  host: sentinel[:host] || sentinel["host"],
581
605
  port: sentinel[:port] || sentinel["port"],
606
+ username: sentinel[:username] || sentinel["username"],
582
607
  password: sentinel[:password] || sentinel["password"],
583
608
  reconnect_attempts: 0
584
609
  }))
@@ -31,13 +31,13 @@ class Redis
31
31
  private
32
32
 
33
33
  def pick_details(details)
34
- details.map do |command, detail|
35
- [command, {
34
+ details.transform_values do |detail|
35
+ {
36
36
  first_key_position: detail[:first],
37
37
  write: detail[:flags].include?('write'),
38
38
  readonly: detail[:flags].include?('readonly')
39
- }]
40
- end.to_h
39
+ }
40
+ end
41
41
  end
42
42
 
43
43
  def dig_details(command, key)
@@ -53,8 +53,6 @@ class Redis
53
53
  when 'object' then 2
54
54
  when 'memory'
55
55
  command[1].to_s.casecmp('usage').zero? ? 2 : 0
56
- when 'scan', 'sscan', 'hscan', 'zscan'
57
- determine_optional_key_position(command, 'match')
58
56
  when 'xread', 'xreadgroup'
59
57
  determine_optional_key_position(command, 'streams')
60
58
  else
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../errors'
3
+ require 'redis/errors'
4
4
 
5
5
  class Redis
6
6
  class Cluster
@@ -10,22 +10,21 @@ class Redis
10
10
  module_function
11
11
 
12
12
  def load(nodes)
13
- details = {}
14
-
15
- nodes.each do |node|
16
- details = fetch_command_details(node)
17
- details.empty? ? next : break
13
+ errors = nodes.map do |node|
14
+ begin
15
+ return fetch_command_details(node)
16
+ rescue CannotConnectError, ConnectionError, CommandError => error
17
+ error
18
+ end
18
19
  end
19
20
 
20
- details
21
+ raise InitialSetupError, errors
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
@@ -58,6 +58,18 @@ class Redis
58
58
  try_map { |_, client| client.process(commands, &block) }.values
59
59
  end
60
60
 
61
+ def scale_reading_clients
62
+ reading_clients = []
63
+
64
+ @clients.each do |node_key, client|
65
+ next unless replica_disabled? ? master?(node_key) : slave?(node_key)
66
+
67
+ reading_clients << client
68
+ end
69
+
70
+ reading_clients
71
+ end
72
+
61
73
  private
62
74
 
63
75
  def replica_disabled?
@@ -76,8 +88,9 @@ class Redis
76
88
  clients = options.map do |node_key, option|
77
89
  next if replica_disabled? && slave?(node_key)
78
90
 
91
+ option = option.merge(readonly: true) if slave?(node_key)
92
+
79
93
  client = Client.new(option)
80
- client.call(%i[readonly]) if slave?(node_key)
81
94
  [node_key, client]
82
95
  end
83
96
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../errors'
3
+ require 'redis/errors'
4
4
 
5
5
  class Redis
6
6
  class Cluster
@@ -9,16 +9,15 @@ class Redis
9
9
  module_function
10
10
 
11
11
  def load_flags(nodes)
12
- info = {}
13
-
14
- nodes.each do |node|
15
- info = fetch_node_info(node)
16
- info.empty? ? next : break
12
+ errors = nodes.map do |node|
13
+ begin
14
+ return fetch_node_info(node)
15
+ rescue CannotConnectError, ConnectionError, CommandError => error
16
+ error
17
+ end
17
18
  end
18
19
 
19
- return info unless info.empty?
20
-
21
- raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
20
+ raise InitialSetupError, errors
22
21
  end
23
22
 
24
23
  def fetch_node_info(node)
@@ -27,8 +26,6 @@ class Redis
27
26
  .map { |str| str.split(' ') }
28
27
  .map { |arr| [arr[1].split('@').first, (arr[2].split(',') & %w[master slave]).first] }
29
28
  .to_h
30
- rescue CannotConnectError, ConnectionError, CommandError
31
- {} # can retry on another node
32
29
  end
33
30
 
34
31
  private_class_method :fetch_node_info
@@ -17,14 +17,20 @@ class Redis
17
17
  node_addrs = options.delete(:cluster)
18
18
  @node_opts = build_node_options(node_addrs)
19
19
  @replica = options.delete(:replica) == true
20
+ @fixed_hostname = options.delete(:fixed_hostname)
20
21
  add_common_node_option_if_needed(options, @node_opts, :scheme)
22
+ add_common_node_option_if_needed(options, @node_opts, :username)
21
23
  add_common_node_option_if_needed(options, @node_opts, :password)
22
24
  @options = options
23
25
  end
24
26
 
25
27
  def per_node_key
26
- @node_opts.map { |opt| [NodeKey.build_from_host_port(opt[:host], opt[:port]), @options.merge(opt)] }
27
- .to_h
28
+ @node_opts.map do |opt|
29
+ node_key = NodeKey.build_from_host_port(opt[:host], opt[:port])
30
+ options = @options.merge(opt)
31
+ options = options.merge(host: @fixed_hostname) if @fixed_hostname && !@fixed_hostname.empty?
32
+ [node_key, options]
33
+ end.to_h
28
34
  end
29
35
 
30
36
  def use_replica?
@@ -63,7 +69,11 @@ class Redis
63
69
  raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
64
70
 
65
71
  db = uri.path.split('/')[1]&.to_i
66
- { scheme: uri.scheme, password: uri.password, host: uri.host, port: uri.port, db: db }.reject { |_, v| v.nil? }
72
+ username = uri.user ? URI.decode_www_form_component(uri.user) : nil
73
+ password = uri.password ? URI.decode_www_form_component(uri.password) : nil
74
+
75
+ { scheme: uri.scheme, username: username, password: password, host: uri.host, port: uri.port, db: db }
76
+ .reject { |_, v| v.nil? || v == '' }
67
77
  rescue URI::InvalidURIError => err
68
78
  raise InvalidClientOptionError, err.message
69
79
  end
@@ -79,7 +89,7 @@ class Redis
79
89
 
80
90
  # Redis cluster node returns only host and port information.
81
91
  # So we should complement additional information such as:
82
- # scheme, password and so on.
92
+ # scheme, username, password and so on.
83
93
  def add_common_node_option_if_needed(options, node_opts, key)
84
94
  return options if options[key].nil? && node_opts.first[key].nil?
85
95
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../errors'
4
- require_relative 'node_key'
3
+ require 'redis/errors'
4
+ require 'redis/cluster/node_key'
5
5
 
6
6
  class Redis
7
7
  class Cluster
@@ -10,16 +10,15 @@ class Redis
10
10
  module_function
11
11
 
12
12
  def load(nodes)
13
- info = {}
14
-
15
- nodes.each do |node|
16
- info = fetch_slot_info(node)
17
- info.empty? ? next : break
13
+ errors = nodes.map do |node|
14
+ begin
15
+ return fetch_slot_info(node)
16
+ rescue CannotConnectError, ConnectionError, CommandError => error
17
+ error
18
+ end
18
19
  end
19
20
 
20
- return info unless info.empty?
21
-
22
- raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
21
+ raise InitialSetupError, errors
23
22
  end
24
23
 
25
24
  def fetch_slot_info(node)
@@ -27,8 +26,6 @@ class Redis
27
26
  node.call(%i[cluster slots])
28
27
  .flat_map { |arr| parse_slot_info(arr, default_ip: node.host) }
29
28
  .each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] }
30
- rescue CannotConnectError, ConnectionError, CommandError
31
- {} # can retry on another node
32
29
  end
33
30
 
34
31
  def parse_slot_info(arr, default_ip:)