redis 4.1.4 → 4.7.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +141 -0
  3. data/README.md +52 -27
  4. data/lib/redis/client.rb +122 -87
  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 +17 -1
  8. data/lib/redis/cluster/node_loader.rb +8 -11
  9. data/lib/redis/cluster/option.rb +18 -5
  10. data/lib/redis/cluster/slot.rb +28 -14
  11. data/lib/redis/cluster/slot_loader.rb +11 -15
  12. data/lib/redis/cluster.rb +37 -13
  13. data/lib/redis/commands/bitmaps.rb +63 -0
  14. data/lib/redis/commands/cluster.rb +45 -0
  15. data/lib/redis/commands/connection.rb +58 -0
  16. data/lib/redis/commands/geo.rb +84 -0
  17. data/lib/redis/commands/hashes.rb +251 -0
  18. data/lib/redis/commands/hyper_log_log.rb +37 -0
  19. data/lib/redis/commands/keys.rb +411 -0
  20. data/lib/redis/commands/lists.rb +289 -0
  21. data/lib/redis/commands/pubsub.rb +72 -0
  22. data/lib/redis/commands/scripting.rb +114 -0
  23. data/lib/redis/commands/server.rb +188 -0
  24. data/lib/redis/commands/sets.rb +207 -0
  25. data/lib/redis/commands/sorted_sets.rb +812 -0
  26. data/lib/redis/commands/streams.rb +382 -0
  27. data/lib/redis/commands/strings.rb +313 -0
  28. data/lib/redis/commands/transactions.rb +139 -0
  29. data/lib/redis/commands.rb +242 -0
  30. data/lib/redis/connection/command_helper.rb +4 -2
  31. data/lib/redis/connection/hiredis.rb +6 -7
  32. data/lib/redis/connection/registry.rb +1 -1
  33. data/lib/redis/connection/ruby.rb +106 -114
  34. data/lib/redis/connection/synchrony.rb +16 -10
  35. data/lib/redis/connection.rb +2 -1
  36. data/lib/redis/distributed.rb +200 -65
  37. data/lib/redis/errors.rb +10 -0
  38. data/lib/redis/hash_ring.rb +14 -14
  39. data/lib/redis/pipeline.rb +133 -10
  40. data/lib/redis/subscribe.rb +10 -12
  41. data/lib/redis/version.rb +2 -1
  42. data/lib/redis.rb +158 -3358
  43. metadata +32 -10
data/lib/redis/client.rb CHANGED
@@ -1,31 +1,38 @@
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
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
- }
27
-
28
- attr_reader :options
11
+ url: -> { ENV["REDIS_URL"] },
12
+ scheme: "redis",
13
+ host: "127.0.0.1",
14
+ port: 6379,
15
+ path: nil,
16
+ read_timeout: nil,
17
+ write_timeout: nil,
18
+ connect_timeout: nil,
19
+ timeout: 5.0,
20
+ username: nil,
21
+ password: nil,
22
+ db: 0,
23
+ driver: nil,
24
+ id: nil,
25
+ tcp_keepalive: 0,
26
+ reconnect_attempts: 1,
27
+ reconnect_delay: 0,
28
+ reconnect_delay_max: 0.5,
29
+ inherit_socket: false,
30
+ logger: nil,
31
+ sentinels: nil,
32
+ role: nil
33
+ }.freeze
34
+
35
+ attr_reader :options, :connection, :command_map
29
36
 
30
37
  def scheme
31
38
  @options[:scheme]
@@ -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
@@ -76,8 +87,6 @@ class Redis
76
87
  end
77
88
 
78
89
  attr_accessor :logger
79
- attr_reader :connection
80
- attr_reader :command_map
81
90
 
82
91
  def initialize(options = {})
83
92
  @options = _parse_options(options)
@@ -89,7 +98,7 @@ class Redis
89
98
  @pending_reads = 0
90
99
 
91
100
  @connector =
92
- if options.include?(:sentinels)
101
+ if !@options[:sentinels].nil?
93
102
  Connector::Sentinel.new(@options)
94
103
  elsif options.include?(:connector) && options[:connector].respond_to?(:new)
95
104
  options.delete(:connector).new(@options)
@@ -104,7 +113,34 @@ class Redis
104
113
  # Don't try to reconnect when the connection is fresh
105
114
  with_reconnect(false) do
106
115
  establish_connection
107
- 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]
108
144
  call [:select, db] if db != 0
109
145
  call [:client, :setname, @options[:id]] if @options[:id]
110
146
  @connector.check(self)
@@ -114,7 +150,7 @@ class Redis
114
150
  end
115
151
 
116
152
  def id
117
- @options[:id] || "redis://#{location}/#{db}"
153
+ @options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
118
154
  end
119
155
 
120
156
  def location
@@ -125,7 +161,7 @@ class Redis
125
161
  reply = process([command]) { read }
126
162
  raise reply if reply.is_a?(CommandError)
127
163
 
128
- if block_given?
164
+ if block_given? && reply != 'QUEUED'
129
165
  yield reply
130
166
  else
131
167
  reply
@@ -166,6 +202,7 @@ class Redis
166
202
  end
167
203
  rescue ConnectionError => e
168
204
  return nil if pipeline.shutdown?
205
+
169
206
  # Assume the pipeline was sent in one piece, but execution of
170
207
  # SHUTDOWN caused none of the replies for commands that were executed
171
208
  # prior to it from coming back around.
@@ -214,7 +251,8 @@ class Redis
214
251
  result
215
252
  end
216
253
 
217
- 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
218
256
  with_socket_timeout(timeout) do
219
257
  call(command, &blk)
220
258
  end
@@ -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
@@ -264,7 +302,7 @@ class Redis
264
302
  e2 = TimeoutError.new("Connection timed out")
265
303
  e2.set_backtrace(e1.backtrace)
266
304
  raise e2
267
- 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
268
306
  raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
269
307
  end
270
308
 
@@ -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)
@@ -407,18 +442,16 @@ class Redis
407
442
  defaults = DEFAULTS.dup
408
443
  options = options.dup
409
444
 
410
- defaults.keys.each do |key|
445
+ defaults.each_key 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
@@ -426,13 +459,15 @@ class Redis
426
459
 
427
460
  uri = URI(url)
428
461
 
429
- if uri.scheme == "unix"
430
- defaults[:path] = uri.path
431
- elsif uri.scheme == "redis" || uri.scheme == "rediss"
462
+ case uri.scheme
463
+ when "unix"
464
+ defaults[:path] = uri.path
465
+ when "redis", "rediss"
432
466
  defaults[:scheme] = uri.scheme
433
- defaults[:host] = uri.host if uri.host
467
+ defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
434
468
  defaults[:port] = uri.port if uri.port
435
- defaults[:password] = CGI.unescape(uri.password) if uri.password
469
+ defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
470
+ defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
436
471
  defaults[:db] = uri.path[1..-1].to_i if uri.path
437
472
  defaults[:role] = :master
438
473
  else
@@ -443,7 +478,7 @@ class Redis
443
478
  end
444
479
 
445
480
  # Use default when option is not specified or nil
446
- defaults.keys.each do |key|
481
+ defaults.each_key do |key|
447
482
  options[key] = defaults[key] if options[key].nil?
448
483
  end
449
484
 
@@ -458,7 +493,7 @@ class Redis
458
493
  options[:port] = options[:port].to_i
459
494
  end
460
495
 
461
- if options.has_key?(:timeout)
496
+ if options.key?(:timeout)
462
497
  options[:connect_timeout] ||= options[:timeout]
463
498
  options[:read_timeout] ||= options[:timeout]
464
499
  options[:write_timeout] ||= options[:timeout]
@@ -477,7 +512,7 @@ class Redis
477
512
 
478
513
  case options[:tcp_keepalive]
479
514
  when Hash
480
- [:time, :intvl, :probes].each do |key|
515
+ %i[time intvl probes].each do |key|
481
516
  unless options[:tcp_keepalive][key].is_a?(Integer)
482
517
  raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
483
518
  end
@@ -485,13 +520,13 @@ class Redis
485
520
 
486
521
  when Integer
487
522
  if options[:tcp_keepalive] >= 60
488
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 20, :intvl => 10, :probes => 2}
523
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
489
524
 
490
525
  elsif options[:tcp_keepalive] >= 30
491
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 10, :intvl => 5, :probes => 2}
526
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
492
527
 
493
528
  elsif options[:tcp_keepalive] >= 5
494
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 2, :intvl => 2, :probes => 1}
529
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
495
530
  end
496
531
  end
497
532
 
@@ -503,14 +538,14 @@ class Redis
503
538
  def _parse_driver(driver)
504
539
  driver = driver.to_s if driver.is_a?(Symbol)
505
540
 
506
- if driver.kind_of?(String)
541
+ if driver.is_a?(String)
507
542
  begin
508
543
  require_relative "connection/#{driver}"
509
- rescue LoadError, NameError => e
544
+ rescue LoadError, NameError
510
545
  begin
511
- require "connection/#{driver}"
512
- rescue LoadError, NameError => e
513
- raise RuntimeError, "Cannot load driver #{driver.inspect}: #{e.message}"
546
+ require "redis/connection/#{driver}"
547
+ rescue LoadError, NameError => error
548
+ raise "Cannot load driver #{driver.inspect}: #{error.message}"
514
549
  end
515
550
  end
516
551
 
@@ -529,8 +564,7 @@ class Redis
529
564
  @options
530
565
  end
531
566
 
532
- def check(client)
533
- end
567
+ def check(client); end
534
568
 
535
569
  class Sentinel < Connector
536
570
  def initialize(options)
@@ -539,7 +573,7 @@ class Redis
539
573
  @options[:db] = DEFAULTS.fetch(:db)
540
574
 
541
575
  @sentinels = @options.delete(:sentinels).dup
542
- @role = @options.fetch(:role, "master").to_s
576
+ @role = (@options[:role] || "master").to_s
543
577
  @master = @options[:host]
544
578
  end
545
579
 
@@ -562,13 +596,13 @@ class Redis
562
596
 
563
597
  def resolve
564
598
  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
599
+ when "master"
600
+ resolve_master
601
+ when "slave"
602
+ resolve_slave
603
+ else
604
+ raise ArgumentError, "Unknown instance role #{@role}"
605
+ end
572
606
 
573
607
  result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
574
608
  end
@@ -576,11 +610,12 @@ class Redis
576
610
  def sentinel_detect
577
611
  @sentinels.each do |sentinel|
578
612
  client = Client.new(@options.merge({
579
- :host => sentinel[:host],
580
- :port => sentinel[:port],
581
- password: sentinel[:password],
582
- :reconnect_attempts => 0,
583
- }))
613
+ host: sentinel[:host] || sentinel["host"],
614
+ port: sentinel[:port] || sentinel["port"],
615
+ username: sentinel[:username] || sentinel["username"],
616
+ password: sentinel[:password] || sentinel["password"],
617
+ reconnect_attempts: 0
618
+ }))
584
619
 
585
620
  begin
586
621
  if result = yield(client)
@@ -602,7 +637,7 @@ class Redis
602
637
  def resolve_master
603
638
  sentinel_detect do |client|
604
639
  if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
605
- {:host => reply[0], :port => reply[1]}
640
+ { host: reply[0], port: reply[1] }
606
641
  end
607
642
  end
608
643
  end
@@ -620,7 +655,7 @@ class Redis
620
655
  slave = slaves.sample
621
656
  {
622
657
  host: slave.fetch('ip'),
623
- port: slave.fetch('port'),
658
+ port: slave.fetch('port')
624
659
  }
625
660
  end
626
661
  end
@@ -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
@@ -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
@@ -56,6 +58,18 @@ class Redis
56
58
  try_map { |_, client| client.process(commands, &block) }.values
57
59
  end
58
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
+
59
73
  private
60
74
 
61
75
  def replica_disabled?
@@ -74,8 +88,9 @@ class Redis
74
88
  clients = options.map do |node_key, option|
75
89
  next if replica_disabled? && slave?(node_key)
76
90
 
91
+ option = option.merge(readonly: true) if slave?(node_key)
92
+
77
93
  client = Client.new(option)
78
- client.call(%i[readonly]) if slave?(node_key)
79
94
  [node_key, client]
80
95
  end
81
96
 
@@ -97,6 +112,7 @@ class Redis
97
112
  end
98
113
 
99
114
  return results if errors.empty?
115
+
100
116
  raise CommandErrorCollection, errors
101
117
  end
102
118
  end
@@ -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?
@@ -43,6 +49,7 @@ class Redis
43
49
 
44
50
  def build_node_options(addrs)
45
51
  raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
52
+
46
53
  addrs.map { |addr| parse_node_addr(addr) }
47
54
  end
48
55
 
@@ -62,21 +69,27 @@ class Redis
62
69
  raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
63
70
 
64
71
  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? }
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 == '' }
66
77
  rescue URI::InvalidURIError => err
67
78
  raise InvalidClientOptionError, err.message
68
79
  end
69
80
 
70
81
  def parse_node_option(addr)
71
82
  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?)
83
+ if addr.values_at(:host, :port).any?(&:nil?)
84
+ raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
85
+ end
73
86
 
74
87
  addr
75
88
  end
76
89
 
77
90
  # Redis cluster node returns only host and port information.
78
91
  # So we should complement additional information such as:
79
- # scheme, password and so on.
92
+ # scheme, username, password and so on.
80
93
  def add_common_node_option_if_needed(options, node_opts, key)
81
94
  return options if options[key].nil? && node_opts.first[key].nil?
82
95