redis 4.1.2 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0b16163b5714933ba20edb9595d7669c7270502586546f3e8758192039113c1
4
- data.tar.gz: 150dacb8ae77a51536e3f2bc6813144b0176415d008adaa5d5e6c4d7f25624a1
3
+ metadata.gz: 9796f6646b7d3aaeeb5ef37629fb1a43422285724d8d2901a219ef4f2882eff5
4
+ data.tar.gz: b89f4f1d6a3c9ee93202ce08cdcd3ed184694b5693f7461be7cf7e517139a278
5
5
  SHA512:
6
- metadata.gz: 90a9fcfdefc41f5feaedfea2bde2be1ad39308dee10f0045ec227577d067d795aaaede4aec200ed6f5777167842540ff5adb6a927fe6a0a0e5ac46ea8eeaf737
7
- data.tar.gz: 90c74c87892ef96d0d6ef2bd5934f8e3064d30064a9310fc2b1e31aba066ccb6a005d030458a35d9ad09ed403749e3356df3072927e6a749fc21caedee75f936
6
+ metadata.gz: be709e1aad1acee8d7c3e121e946060ce9b693ac80e25af200e2a579988f3952d6522cb9855917dc76628ae2038365bb8754b49a7cc6e90395706ba30fc87a86
7
+ data.tar.gz: 00102b01b4b37daab76fc90990f980a4710bec0e797a70d65a7544ef304f01f9b9babbdcc680da71513dd8f79649bd5f529e6804e71297b26978625bea746367
data/CHANGELOG.md CHANGED
@@ -1,8 +1,35 @@
1
1
  # Unreleased
2
2
 
3
- # 4.1.2
3
+ # 4.2.0
4
+
5
+ * Convert commands to accept keyword arguments rather than option hashes. This both help catching typos, and reduce needless allocations.
6
+ * Deprecate the synchrony driver. It will be removed in 5.0 and hopefully maintained as a separate gem. See #915.
7
+ * Make `Redis#exists` variadic, will return an Integer if called with multiple keys.
8
+ * Add `Redis#exists?` to get a Boolean if any of the keys exists.
9
+ * `Redis#exists` when called with a single key will warn that future versions will return an Integer.
10
+ Set `Redis.exists_returns_integer = true` to opt-in to the new behavior.
11
+ * Support `keepttl` ooption in `set`. See #913.
12
+ * Optimized initialization of Redis::Cluster. See #912.
13
+ * Accept sentinel options even with string key. See #599.
14
+ * Verify TLS connections by default. See #900.
15
+
16
+ # 4.1.4
17
+
18
+ * Alias `Redis#disconnect` as `#close`. See #901.
19
+ * Handle clusters with multiple slot ranges. See #894.
20
+ * Fix password authentication to a redis cluster. See #889.
21
+ * Handle recursive MOVED responses. See #882.
22
+ * Increase buffer size in the ruby connector. See #880.
23
+ * Fix thread safety of `Redis.queue`. See #878.
24
+ * Deprecate `Redis::Future#==` as it's likely to be a mistake. See #876.
25
+ * Support `KEEPTTL` option for SET command. See #913.
26
+
27
+ # 4.1.3
4
28
 
5
29
  * Fix the client hanging forever when connecting with SSL to a non-SSL server. See #835.
30
+
31
+ # 4.1.2
32
+
6
33
  * Fix several authentication problems with sentinel. See #850 and #856.
7
34
  * Explicitly drop Ruby 2.2 support.
8
35
 
data/README.md CHANGED
@@ -1,8 +1,9 @@
1
- # redis-rb [![Build Status][travis-image]][travis-link] [![Inline docs][inchpages-image]][inchpages-link]
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)
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.
@@ -142,12 +146,13 @@ redis.mget('{key}1', '{key}2')
142
146
  ```
143
147
 
144
148
  * The client automatically reconnects after a failover occurred, but the caller is responsible for handling errors while it is happening.
149
+ * The client support permanent node failures, and will reroute requests to promoted slaves.
145
150
  * The client supports `MOVED` and `ASK` redirections transparently.
146
151
 
147
152
  ## Storing objects
148
153
 
149
- Redis only stores strings as values. If you want to store an object, you
150
- can use a serialization mechanism such as JSON:
154
+ Redis "string" types can be used to store serialized Ruby objects, for
155
+ example with JSON:
151
156
 
152
157
  ```ruby
153
158
  require "json"
@@ -321,7 +326,7 @@ This library supports natively terminating client side SSL/TLS connections
321
326
  when talking to Redis via a server-side proxy such as [stunnel], [hitch],
322
327
  or [ghostunnel].
323
328
 
324
- To enable SSL support, pass the `:ssl => :true` option when configuring the
329
+ To enable SSL support, pass the `:ssl => true` option when configuring the
325
330
  Redis client, or pass in `:url => "rediss://..."` (like HTTPS for Redis).
326
331
  You will also need to pass in an `:ssl_params => { ... }` hash used to
327
332
  configure the `OpenSSL::SSL::SSLContext` object used for the connection:
@@ -436,6 +441,10 @@ redis = Redis.new(:driver => :synchrony)
436
441
  This library is tested against recent Ruby and Redis versions.
437
442
  Check [Travis][travis-link] for the exact versions supported.
438
443
 
444
+ ## See Also
445
+
446
+ - [async-redis](https://github.com/socketry/async-redis) — An [async](https://github.com/socketry/async) compatible Redis client.
447
+
439
448
  ## Contributors
440
449
 
441
450
  Several people contributed to redis-rb, but we would like to especially
@@ -446,7 +455,7 @@ client and evangelized Redis in Rubyland. Thank you, Ezra.
446
455
  ## Contributing
447
456
 
448
457
  [Fork the project](https://github.com/redis/redis-rb) and send pull
449
- requests. You can also ask for help at `#redis-rb` on Freenode.
458
+ requests.
450
459
 
451
460
 
452
461
  [inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg
data/lib/redis/client.rb CHANGED
@@ -6,24 +6,25 @@ require "cgi"
6
6
 
7
7
  class Redis
8
8
  class Client
9
-
10
9
  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
- }
10
+ url: -> { ENV["REDIS_URL"] },
11
+ scheme: "redis",
12
+ host: "127.0.0.1",
13
+ port: 6379,
14
+ path: nil,
15
+ timeout: 5.0,
16
+ password: nil,
17
+ db: 0,
18
+ driver: nil,
19
+ id: nil,
20
+ tcp_keepalive: 0,
21
+ reconnect_attempts: 1,
22
+ reconnect_delay: 0,
23
+ reconnect_delay_max: 0.5,
24
+ inherit_socket: false,
25
+ sentinels: nil,
26
+ role: nil
27
+ }.freeze
27
28
 
28
29
  attr_reader :options
29
30
 
@@ -89,7 +90,7 @@ class Redis
89
90
  @pending_reads = 0
90
91
 
91
92
  @connector =
92
- if options.include?(:sentinels)
93
+ if !@options[:sentinels].nil?
93
94
  Connector::Sentinel.new(@options)
94
95
  elsif options.include?(:connector) && options[:connector].respond_to?(:new)
95
96
  options.delete(:connector).new(@options)
@@ -166,6 +167,7 @@ class Redis
166
167
  end
167
168
  rescue ConnectionError => e
168
169
  return nil if pipeline.shutdown?
170
+
169
171
  # Assume the pipeline was sent in one piece, but execution of
170
172
  # SHUTDOWN caused none of the replies for commands that were executed
171
173
  # prior to it from coming back around.
@@ -244,12 +246,13 @@ class Redis
244
246
  end
245
247
 
246
248
  def connected?
247
- !! (connection && connection.connected?)
249
+ !!(connection && connection.connected?)
248
250
  end
249
251
 
250
252
  def disconnect
251
253
  connection.disconnect if connected?
252
254
  end
255
+ alias close disconnect
253
256
 
254
257
  def reconnect
255
258
  disconnect
@@ -300,30 +303,27 @@ class Redis
300
303
  with_socket_timeout(0, &blk)
301
304
  end
302
305
 
303
- def with_reconnect(val=true)
304
- begin
305
- original, @reconnect = @reconnect, val
306
- yield
307
- ensure
308
- @reconnect = original
309
- end
306
+ def with_reconnect(val = true)
307
+ original, @reconnect = @reconnect, val
308
+ yield
309
+ ensure
310
+ @reconnect = original
310
311
  end
311
312
 
312
313
  def without_reconnect(&blk)
313
314
  with_reconnect(false, &blk)
314
315
  end
315
316
 
316
- protected
317
+ protected
317
318
 
318
319
  def logging(commands)
319
- return yield unless @logger && @logger.debug?
320
+ return yield unless @logger&.debug?
320
321
 
321
322
  begin
322
323
  commands.each do |name, *args|
323
324
  logged_args = args.map do |a|
324
- case
325
- when a.respond_to?(:inspect) then a.inspect
326
- when a.respond_to?(:to_s) then a.to_s
325
+ if a.respond_to?(:inspect) then a.inspect
326
+ elsif a.respond_to?(:to_s) then a.to_s
327
327
  else
328
328
  # handle poorly-behaved descendants of BasicObject
329
329
  klass = a.instance_exec { (class << self; self end).superclass }
@@ -357,9 +357,9 @@ class Redis
357
357
  Errno::ENETUNREACH,
358
358
  Errno::ENOENT,
359
359
  Errno::ETIMEDOUT,
360
- Errno::EINVAL
360
+ Errno::EINVAL => error
361
361
 
362
- raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
362
+ raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
363
363
  end
364
364
 
365
365
  def ensure_connected
@@ -373,9 +373,9 @@ class Redis
373
373
  if connected?
374
374
  unless inherit_socket? || Process.pid == @pid
375
375
  raise InheritedError,
376
- "Tried to use a connection from a child process without reconnecting. " +
377
- "You need to reconnect to Redis after forking " +
378
- "or set :inherit_socket to true."
376
+ "Tried to use a connection from a child process without reconnecting. " \
377
+ "You need to reconnect to Redis after forking " \
378
+ "or set :inherit_socket to true."
379
379
  end
380
380
  else
381
381
  connect
@@ -386,7 +386,7 @@ class Redis
386
386
  disconnect
387
387
 
388
388
  if attempts <= @options[:reconnect_attempts] && @reconnect
389
- sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
389
+ sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
390
390
  @options[:reconnect_delay_max]].min
391
391
 
392
392
  Kernel.sleep(sleep_t)
@@ -408,16 +408,14 @@ class Redis
408
408
 
409
409
  defaults.keys.each do |key|
410
410
  # Fill in defaults if needed
411
- if defaults[key].respond_to?(:call)
412
- defaults[key] = defaults[key].call
413
- end
411
+ defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
414
412
 
415
413
  # Symbolize only keys that are needed
416
- options[key] = options[key.to_s] if options.has_key?(key.to_s)
414
+ options[key] = options[key.to_s] if options.key?(key.to_s)
417
415
  end
418
416
 
419
417
  url = options[:url]
420
- url = defaults[:url] if url == nil
418
+ url = defaults[:url] if url.nil?
421
419
 
422
420
  # Override defaults from URL if given
423
421
  if url
@@ -426,7 +424,7 @@ class Redis
426
424
  uri = URI(url)
427
425
 
428
426
  if uri.scheme == "unix"
429
- defaults[:path] = uri.path
427
+ defaults[:path] = uri.path
430
428
  elsif uri.scheme == "redis" || uri.scheme == "rediss"
431
429
  defaults[:scheme] = uri.scheme
432
430
  defaults[:host] = uri.host if uri.host
@@ -457,7 +455,7 @@ class Redis
457
455
  options[:port] = options[:port].to_i
458
456
  end
459
457
 
460
- if options.has_key?(:timeout)
458
+ if options.key?(:timeout)
461
459
  options[:connect_timeout] ||= options[:timeout]
462
460
  options[:read_timeout] ||= options[:timeout]
463
461
  options[:write_timeout] ||= options[:timeout]
@@ -476,7 +474,7 @@ class Redis
476
474
 
477
475
  case options[:tcp_keepalive]
478
476
  when Hash
479
- [:time, :intvl, :probes].each do |key|
477
+ %i[time intvl probes].each do |key|
480
478
  unless options[:tcp_keepalive][key].is_a?(Integer)
481
479
  raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
482
480
  end
@@ -484,13 +482,13 @@ class Redis
484
482
 
485
483
  when Integer
486
484
  if options[:tcp_keepalive] >= 60
487
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 20, :intvl => 10, :probes => 2}
485
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
488
486
 
489
487
  elsif options[:tcp_keepalive] >= 30
490
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 10, :intvl => 5, :probes => 2}
488
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
491
489
 
492
490
  elsif options[:tcp_keepalive] >= 5
493
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 2, :intvl => 2, :probes => 1}
491
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
494
492
  end
495
493
  end
496
494
 
@@ -502,14 +500,14 @@ class Redis
502
500
  def _parse_driver(driver)
503
501
  driver = driver.to_s if driver.is_a?(Symbol)
504
502
 
505
- if driver.kind_of?(String)
503
+ if driver.is_a?(String)
506
504
  begin
507
505
  require_relative "connection/#{driver}"
508
- rescue LoadError, NameError => e
506
+ rescue LoadError, NameError
509
507
  begin
510
508
  require "connection/#{driver}"
511
- rescue LoadError, NameError => e
512
- raise RuntimeError, "Cannot load driver #{driver.inspect}: #{e.message}"
509
+ rescue LoadError, NameError => error
510
+ raise "Cannot load driver #{driver.inspect}: #{error.message}"
513
511
  end
514
512
  end
515
513
 
@@ -528,8 +526,7 @@ class Redis
528
526
  @options
529
527
  end
530
528
 
531
- def check(client)
532
- end
529
+ def check(client); end
533
530
 
534
531
  class Sentinel < Connector
535
532
  def initialize(options)
@@ -538,7 +535,7 @@ class Redis
538
535
  @options[:db] = DEFAULTS.fetch(:db)
539
536
 
540
537
  @sentinels = @options.delete(:sentinels).dup
541
- @role = @options.fetch(:role, "master").to_s
538
+ @role = (@options[:role] || "master").to_s
542
539
  @master = @options[:host]
543
540
  end
544
541
 
@@ -561,13 +558,13 @@ class Redis
561
558
 
562
559
  def resolve
563
560
  result = case @role
564
- when "master"
565
- resolve_master
566
- when "slave"
567
- resolve_slave
568
- else
569
- raise ArgumentError, "Unknown instance role #{@role}"
570
- end
561
+ when "master"
562
+ resolve_master
563
+ when "slave"
564
+ resolve_slave
565
+ else
566
+ raise ArgumentError, "Unknown instance role #{@role}"
567
+ end
571
568
 
572
569
  result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
573
570
  end
@@ -575,11 +572,11 @@ class Redis
575
572
  def sentinel_detect
576
573
  @sentinels.each do |sentinel|
577
574
  client = Client.new(@options.merge({
578
- :host => sentinel[:host],
579
- :port => sentinel[:port],
580
- password: sentinel[:password],
581
- :reconnect_attempts => 0,
582
- }))
575
+ host: sentinel[:host] || sentinel["host"],
576
+ port: sentinel[:port] || sentinel["port"],
577
+ password: sentinel[:password] || sentinel["password"],
578
+ reconnect_attempts: 0
579
+ }))
583
580
 
584
581
  begin
585
582
  if result = yield(client)
@@ -601,7 +598,7 @@ class Redis
601
598
  def resolve_master
602
599
  sentinel_detect do |client|
603
600
  if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
604
- {:host => reply[0], :port => reply[1]}
601
+ { host: reply[0], port: reply[1] }
605
602
  end
606
603
  end
607
604
  end
@@ -619,7 +616,7 @@ class Redis
619
616
  slave = slaves.sample
620
617
  {
621
618
  host: slave.fetch('ip'),
622
- port: slave.fetch('port'),
619
+ port: slave.fetch('port')
623
620
  }
624
621
  end
625
622
  end
@@ -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
@@ -97,6 +99,7 @@ class Redis
97
99
  end
98
100
 
99
101
  return results if errors.empty?
102
+
100
103
  raise CommandErrorCollection, errors
101
104
  end
102
105
  end
@@ -6,17 +6,13 @@ class Redis
6
6
  # It is different from node id.
7
7
  # Node id is internal identifying code in Redis Cluster.
8
8
  module NodeKey
9
- DEFAULT_SCHEME = 'redis'
10
- SECURE_SCHEME = 'rediss'
11
9
  DELIMITER = ':'
12
10
 
13
11
  module_function
14
12
 
15
- def to_node_urls(node_keys, secure:)
16
- scheme = secure ? SECURE_SCHEME : DEFAULT_SCHEME
17
- node_keys
18
- .map { |k| k.split(DELIMITER) }
19
- .map { |k| URI::Generic.build(scheme: scheme, host: k[0], port: k[1].to_i).to_s }
13
+ def optionize(node_key)
14
+ host, port = split(node_key)
15
+ { host: host, port: port }
20
16
  end
21
17
 
22
18
  def split(node_key)
@@ -15,36 +15,35 @@ class Redis
15
15
  def initialize(options)
16
16
  options = options.dup
17
17
  node_addrs = options.delete(:cluster)
18
- @node_uris = build_node_uris(node_addrs)
18
+ @node_opts = build_node_options(node_addrs)
19
19
  @replica = options.delete(:replica) == true
20
+ add_common_node_option_if_needed(options, @node_opts, :scheme)
21
+ add_common_node_option_if_needed(options, @node_opts, :password)
20
22
  @options = options
21
23
  end
22
24
 
23
25
  def per_node_key
24
- @node_uris.map { |uri| [NodeKey.build_from_uri(uri), @options.merge(url: uri.to_s)] }
26
+ @node_opts.map { |opt| [NodeKey.build_from_host_port(opt[:host], opt[:port]), @options.merge(opt)] }
25
27
  .to_h
26
28
  end
27
29
 
28
- def secure?
29
- @node_uris.any? { |uri| uri.scheme == SECURE_SCHEME } || @options[:ssl_params] || false
30
- end
31
-
32
30
  def use_replica?
33
31
  @replica
34
32
  end
35
33
 
36
34
  def update_node(addrs)
37
- @node_uris = build_node_uris(addrs)
35
+ @node_opts = build_node_options(addrs)
38
36
  end
39
37
 
40
38
  def add_node(host, port)
41
- @node_uris << parse_node_hash(host: host, port: port)
39
+ @node_opts << { host: host, port: port }
42
40
  end
43
41
 
44
42
  private
45
43
 
46
- def build_node_uris(addrs)
44
+ def build_node_options(addrs)
47
45
  raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
46
+
48
47
  addrs.map { |addr| parse_node_addr(addr) }
49
48
  end
50
49
 
@@ -53,7 +52,7 @@ class Redis
53
52
  when String
54
53
  parse_node_url(addr)
55
54
  when Hash
56
- parse_node_hash(addr)
55
+ parse_node_option(addr)
57
56
  else
58
57
  raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash'
59
58
  end
@@ -62,15 +61,29 @@ class Redis
62
61
  def parse_node_url(addr)
63
62
  uri = URI(addr)
64
63
  raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
65
- uri
64
+
65
+ 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? }
66
67
  rescue URI::InvalidURIError => err
67
68
  raise InvalidClientOptionError, err.message
68
69
  end
69
70
 
70
- def parse_node_hash(addr)
71
+ def parse_node_option(addr)
71
72
  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?)
73
- URI::Generic.build(scheme: DEFAULT_SCHEME, host: addr[:host], port: addr[:port].to_i)
73
+ if addr.values_at(:host, :port).any?(&:nil?)
74
+ raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
75
+ end
76
+
77
+ addr
78
+ end
79
+
80
+ # Redis cluster node returns only host and port information.
81
+ # So we should complement additional information such as:
82
+ # scheme, password and so on.
83
+ def add_common_node_option_if_needed(options, node_opts, key)
84
+ return options if options[key].nil? && node_opts.first[key].nil?
85
+
86
+ options[key] ||= node_opts.first[key]
74
87
  end
75
88
  end
76
89
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
-
5
3
  class Redis
6
4
  class Cluster
7
5
  # Keep slot and node key map for Redis Cluster Client
@@ -28,11 +26,20 @@ class Redis
28
26
  return nil unless exists?(slot)
29
27
  return find_node_key_of_master(slot) if replica_disabled?
30
28
 
31
- @map[slot][:slaves].to_a.sample
29
+ @map[slot][:slaves].sample
32
30
  end
33
31
 
34
32
  def put(slot, node_key)
35
- assign_node_key(@map, slot, node_key)
33
+ # Since we're sharing a hash for build_slot_node_key_map, duplicate it
34
+ # if it already exists instead of preserving as-is.
35
+ @map[slot] = @map[slot] ? @map[slot].dup : { master: nil, slaves: [] }
36
+
37
+ if master?(node_key)
38
+ @map[slot][:master] = node_key
39
+ elsif !@map[slot][:slaves].include?(node_key)
40
+ @map[slot][:slaves] << node_key
41
+ end
42
+
36
43
  nil
37
44
  end
38
45
 
@@ -50,19 +57,29 @@ class Redis
50
57
  @node_flags[node_key] == ROLE_SLAVE
51
58
  end
52
59
 
60
+ # available_slots is mapping of node_key to list of slot ranges
53
61
  def build_slot_node_key_map(available_slots)
54
- available_slots.each_with_object({}) do |(node_key, slots), acc|
55
- slots.each { |slot| assign_node_key(acc, slot, node_key) }
62
+ by_ranges = {}
63
+ available_slots.each do |node_key, slots_arr|
64
+ by_ranges[slots_arr] ||= { master: nil, slaves: [] }
65
+
66
+ if master?(node_key)
67
+ by_ranges[slots_arr][:master] = node_key
68
+ elsif !by_ranges[slots_arr][:slaves].include?(node_key)
69
+ by_ranges[slots_arr][:slaves] << node_key
70
+ end
56
71
  end
57
- end
58
72
 
59
- def assign_node_key(mappings, slot, node_key)
60
- mappings[slot] ||= { master: nil, slaves: ::Set.new }
61
- if master?(node_key)
62
- mappings[slot][:master] = node_key
63
- else
64
- mappings[slot][:slaves].add(node_key)
73
+ by_slot = {}
74
+ by_ranges.each do |slots_arr, nodes|
75
+ slots_arr.each do |slots|
76
+ slots.each do |slot|
77
+ by_slot[slot] = nodes
78
+ end
79
+ end
65
80
  end
81
+
82
+ by_slot
66
83
  end
67
84
  end
68
85
  end
@@ -13,7 +13,7 @@ class Redis
13
13
  info = {}
14
14
 
15
15
  nodes.each do |node|
16
- info = Hash[*fetch_slot_info(node)]
16
+ info = fetch_slot_info(node)
17
17
  info.empty? ? next : break
18
18
  end
19
19
 
@@ -23,9 +23,10 @@ class Redis
23
23
  end
24
24
 
25
25
  def fetch_slot_info(node)
26
+ hash_with_default_arr = Hash.new { |h, k| h[k] = [] }
26
27
  node.call(%i[cluster slots])
27
- .map { |arr| parse_slot_info(arr, default_ip: node.host) }
28
- .flatten
28
+ .flat_map { |arr| parse_slot_info(arr, default_ip: node.host) }
29
+ .each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] }
29
30
  rescue CannotConnectError, ConnectionError, CommandError
30
31
  {} # can retry on another node
31
32
  end
@@ -34,7 +35,6 @@ class Redis
34
35
  first_slot, last_slot = arr[0..1]
35
36
  slot_range = (first_slot..last_slot).freeze
36
37
  arr[2..-1].map { |addr| [stringify_node_key(addr, default_ip), slot_range] }
37
- .flatten
38
38
  end
39
39
 
40
40
  def stringify_node_key(arr, default_ip)