redis 4.1.1 → 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
- SHA1:
3
- metadata.gz: fba6921160b00d34cc11548defceaaee4fc9c181
4
- data.tar.gz: e5eac2ace205355e4890084cc96fe98c651e8d30
2
+ SHA256:
3
+ metadata.gz: 9796f6646b7d3aaeeb5ef37629fb1a43422285724d8d2901a219ef4f2882eff5
4
+ data.tar.gz: b89f4f1d6a3c9ee93202ce08cdcd3ed184694b5693f7461be7cf7e517139a278
5
5
  SHA512:
6
- metadata.gz: 5e88bba869f876bc046935479283f108b46107ab00e7ab60487ca524977635dbdc9b0dcfd037a9b057e43765975c26cdc3a0ceea95aefacf608041a18b50f14e
7
- data.tar.gz: 91360e891b269c8ecb962414f29d023ace19af98e929d7f7680590dcb8520a87deb6c2a47ba27da0ea16eb47ee90d4c8bdf2be894a40c00820f9c07479188cc2
6
+ metadata.gz: be709e1aad1acee8d7c3e121e946060ce9b693ac80e25af200e2a579988f3952d6522cb9855917dc76628ae2038365bb8754b49a7cc6e90395706ba30fc87a86
7
+ data.tar.gz: 00102b01b4b37daab76fc90990f980a4710bec0e797a70d65a7544ef304f01f9b9babbdcc680da71513dd8f79649bd5f529e6804e71297b26978625bea746367
data/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Unreleased
2
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
28
+
29
+ * Fix the client hanging forever when connecting with SSL to a non-SSL server. See #835.
30
+
31
+ # 4.1.2
32
+
33
+ * Fix several authentication problems with sentinel. See #850 and #856.
34
+ * Explicitly drop Ruby 2.2 support.
35
+
36
+
37
+ # 4.1.1
38
+
39
+ * Fix error handling in multi blocks. See #754.
40
+ * Fix geoadd to accept arrays like georadius and georadiusbymember. See #841.
41
+ * Fix georadius command failing when long == lat. See #841.
42
+ * Fix timeout error in xread block: 0. See #837.
43
+ * Fix incompatibility issue with redis-objects. See #834.
44
+ * Properly handle Errno::EADDRNOTAVAIL on connect.
45
+ * Fix password authentication to sentinel instances. See #813.
46
+
3
47
  # 4.1.0
4
48
 
5
49
  * Add Redis Cluster support. See #716.
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.
@@ -95,6 +99,15 @@ but a few so that if one is down the client will try the next one. The client
95
99
  is able to remember the last Sentinel that was able to reply correctly and will
96
100
  use it for the next requests.
97
101
 
102
+ If you want to [authenticate](https://redis.io/topics/sentinel#configuring-sentinel-instances-with-authentication) Sentinel itself, you must specify the `password` option per instance.
103
+
104
+ ```ruby
105
+ SENTINELS = [{ host: '127.0.0.1', port: 26380, password: 'mysecret' },
106
+ { host: '127.0.0.1', port: 26381, password: 'mysecret' }]
107
+
108
+ redis = Redis.new(host: 'mymaster', sentinels: SENTINELS, role: :master)
109
+ ```
110
+
98
111
  ## Cluster support
99
112
 
100
113
  `redis-rb` supports [clustering](https://redis.io/topics/cluster-spec).
@@ -133,12 +146,13 @@ redis.mget('{key}1', '{key}2')
133
146
  ```
134
147
 
135
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.
136
150
  * The client supports `MOVED` and `ASK` redirections transparently.
137
151
 
138
152
  ## Storing objects
139
153
 
140
- Redis only stores strings as values. If you want to store an object, you
141
- 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:
142
156
 
143
157
  ```ruby
144
158
  require "json"
@@ -312,7 +326,7 @@ This library supports natively terminating client side SSL/TLS connections
312
326
  when talking to Redis via a server-side proxy such as [stunnel], [hitch],
313
327
  or [ghostunnel].
314
328
 
315
- 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
316
330
  Redis client, or pass in `:url => "rediss://..."` (like HTTPS for Redis).
317
331
  You will also need to pass in an `:ssl_params => { ... }` hash used to
318
332
  configure the `OpenSSL::SSL::SSLContext` object used for the connection:
@@ -427,6 +441,10 @@ redis = Redis.new(:driver => :synchrony)
427
441
  This library is tested against recent Ruby and Redis versions.
428
442
  Check [Travis][travis-link] for the exact versions supported.
429
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
+
430
448
  ## Contributors
431
449
 
432
450
  Several people contributed to redis-rb, but we would like to especially
@@ -437,7 +455,7 @@ client and evangelized Redis in Rubyland. Thank you, Ezra.
437
455
  ## Contributing
438
456
 
439
457
  [Fork the project](https://github.com/redis/redis-rb) and send pull
440
- requests. You can also ask for help at `#redis-rb` on Freenode.
458
+ requests.
441
459
 
442
460
 
443
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,10 +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
- :reconnect_attempts => 0,
581
- }))
575
+ host: sentinel[:host] || sentinel["host"],
576
+ port: sentinel[:port] || sentinel["port"],
577
+ password: sentinel[:password] || sentinel["password"],
578
+ reconnect_attempts: 0
579
+ }))
582
580
 
583
581
  begin
584
582
  if result = yield(client)
@@ -595,17 +593,12 @@ class Redis
595
593
  end
596
594
 
597
595
  raise CannotConnectError, "No sentinels available."
598
- rescue Redis::CommandError => err
599
- # this feature is only available starting with Redis 5.0.1
600
- raise unless err.message.start_with?('ERR unknown command `auth`')
601
- @options[:password] = DEFAULTS.fetch(:password)
602
- retry
603
596
  end
604
597
 
605
598
  def resolve_master
606
599
  sentinel_detect do |client|
607
600
  if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
608
- {:host => reply[0], :port => reply[1]}
601
+ { host: reply[0], port: reply[1] }
609
602
  end
610
603
  end
611
604
  end
@@ -623,7 +616,7 @@ class Redis
623
616
  slave = slaves.sample
624
617
  {
625
618
  host: slave.fetch('ip'),
626
- port: slave.fetch('port'),
619
+ port: slave.fetch('port')
627
620
  }
628
621
  end
629
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