redis 4.1.0 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/redis/client.rb CHANGED
@@ -1,27 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "errors"
2
4
  require "socket"
3
5
  require "cgi"
4
6
 
5
7
  class Redis
6
8
  class Client
7
-
9
+ # Defaults are also used for converting string keys to symbols.
8
10
  DEFAULTS = {
9
- :url => lambda { ENV["REDIS_URL"] },
10
- :scheme => "redis",
11
- :host => "127.0.0.1",
12
- :port => 6379,
13
- :path => nil,
14
- :timeout => 5.0,
15
- :password => nil,
16
- :db => 0,
17
- :driver => nil,
18
- :id => nil,
19
- :tcp_keepalive => 0,
20
- :reconnect_attempts => 1,
21
- :reconnect_delay => 0,
22
- :reconnect_delay_max => 0.5,
23
- :inherit_socket => false
24
- }
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
25
34
 
26
35
  attr_reader :options
27
36
 
@@ -53,6 +62,10 @@ class Redis
53
62
  @options[:read_timeout]
54
63
  end
55
64
 
65
+ def username
66
+ @options[:username]
67
+ end
68
+
56
69
  def password
57
70
  @options[:password]
58
71
  end
@@ -87,7 +100,7 @@ class Redis
87
100
  @pending_reads = 0
88
101
 
89
102
  @connector =
90
- if options.include?(:sentinels)
103
+ if !@options[:sentinels].nil?
91
104
  Connector::Sentinel.new(@options)
92
105
  elsif options.include?(:connector) && options[:connector].respond_to?(:new)
93
106
  options.delete(:connector).new(@options)
@@ -102,7 +115,33 @@ class Redis
102
115
  # Don't try to reconnect when the connection is fresh
103
116
  with_reconnect(false) do
104
117
  establish_connection
105
- call [:auth, password] if password
118
+ if password
119
+ if username
120
+ begin
121
+ call [:auth, username, password]
122
+ rescue CommandError => err # Likely on Redis < 6
123
+ if err.message.match?(/ERR wrong number of arguments for \'auth\' command/)
124
+ call [:auth, password]
125
+ elsif err.message.match?(/WRONGPASS invalid username-password pair/)
126
+ begin
127
+ call [:auth, password]
128
+ rescue CommandError
129
+ raise err
130
+ end
131
+ ::Kernel.warn(
132
+ "[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
133
+ " the provided password was for the default user. This will start failing in redis-rb 4.6."
134
+ )
135
+ else
136
+ raise
137
+ end
138
+ end
139
+ else
140
+ call [:auth, password]
141
+ end
142
+ end
143
+
144
+ call [:readonly] if @options[:readonly]
106
145
  call [:select, db] if db != 0
107
146
  call [:client, :setname, @options[:id]] if @options[:id]
108
147
  @connector.check(self)
@@ -123,7 +162,7 @@ class Redis
123
162
  reply = process([command]) { read }
124
163
  raise reply if reply.is_a?(CommandError)
125
164
 
126
- if block_given?
165
+ if block_given? && reply != 'QUEUED'
127
166
  yield reply
128
167
  else
129
168
  reply
@@ -155,16 +194,16 @@ class Redis
155
194
  end
156
195
 
157
196
  def call_pipeline(pipeline)
158
- commands = pipeline.commands
159
- return [] if commands.empty?
197
+ return [] if pipeline.futures.empty?
160
198
 
161
199
  with_reconnect pipeline.with_reconnect? do
162
200
  begin
163
- pipeline.finish(call_pipelined(commands)).tap do
201
+ pipeline.finish(call_pipelined(pipeline)).tap do
164
202
  self.db = pipeline.db if pipeline.db
165
203
  end
166
204
  rescue ConnectionError => e
167
205
  return nil if pipeline.shutdown?
206
+
168
207
  # Assume the pipeline was sent in one piece, but execution of
169
208
  # SHUTDOWN caused none of the replies for commands that were executed
170
209
  # prior to it from coming back around.
@@ -173,8 +212,8 @@ class Redis
173
212
  end
174
213
  end
175
214
 
176
- def call_pipelined(commands)
177
- return [] if commands.empty?
215
+ def call_pipelined(pipeline)
216
+ return [] if pipeline.futures.empty?
178
217
 
179
218
  # The method #ensure_connected (called from #process) reconnects once on
180
219
  # I/O errors. To make an effort in making sure that commands are not
@@ -184,6 +223,8 @@ class Redis
184
223
  # already successfully executed commands. To circumvent this, don't retry
185
224
  # after the first reply has been read successfully.
186
225
 
226
+ commands = pipeline.commands
227
+
187
228
  result = Array.new(commands.size)
188
229
  reconnect = @reconnect
189
230
 
@@ -191,8 +232,12 @@ class Redis
191
232
  exception = nil
192
233
 
193
234
  process(commands) do
194
- commands.size.times do |i|
195
- reply = read
235
+ pipeline.timeouts.each_with_index do |timeout, i|
236
+ reply = if timeout
237
+ with_socket_timeout(timeout) { read }
238
+ else
239
+ read
240
+ end
196
241
  result[i] = reply
197
242
  @reconnect = false
198
243
  exception = reply if exception.nil? && reply.is_a?(CommandError)
@@ -237,12 +282,13 @@ class Redis
237
282
  end
238
283
 
239
284
  def connected?
240
- !! (connection && connection.connected?)
285
+ !!(connection && connection.connected?)
241
286
  end
242
287
 
243
288
  def disconnect
244
289
  connection.disconnect if connected?
245
290
  end
291
+ alias close disconnect
246
292
 
247
293
  def reconnect
248
294
  disconnect
@@ -293,30 +339,27 @@ class Redis
293
339
  with_socket_timeout(0, &blk)
294
340
  end
295
341
 
296
- def with_reconnect(val=true)
297
- begin
298
- original, @reconnect = @reconnect, val
299
- yield
300
- ensure
301
- @reconnect = original
302
- end
342
+ def with_reconnect(val = true)
343
+ original, @reconnect = @reconnect, val
344
+ yield
345
+ ensure
346
+ @reconnect = original
303
347
  end
304
348
 
305
349
  def without_reconnect(&blk)
306
350
  with_reconnect(false, &blk)
307
351
  end
308
352
 
309
- protected
353
+ protected
310
354
 
311
355
  def logging(commands)
312
- return yield unless @logger && @logger.debug?
356
+ return yield unless @logger&.debug?
313
357
 
314
358
  begin
315
359
  commands.each do |name, *args|
316
360
  logged_args = args.map do |a|
317
- case
318
- when a.respond_to?(:inspect) then a.inspect
319
- 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
320
363
  else
321
364
  # handle poorly-behaved descendants of BasicObject
322
365
  klass = a.instance_exec { (class << self; self end).superclass }
@@ -343,14 +386,16 @@ class Redis
343
386
  @pending_reads = 0
344
387
  rescue TimeoutError,
345
388
  SocketError,
389
+ Errno::EADDRNOTAVAIL,
346
390
  Errno::ECONNREFUSED,
347
391
  Errno::EHOSTDOWN,
348
392
  Errno::EHOSTUNREACH,
349
393
  Errno::ENETUNREACH,
350
394
  Errno::ENOENT,
351
- Errno::ETIMEDOUT
395
+ Errno::ETIMEDOUT,
396
+ Errno::EINVAL => error
352
397
 
353
- raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
398
+ raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
354
399
  end
355
400
 
356
401
  def ensure_connected
@@ -364,9 +409,9 @@ class Redis
364
409
  if connected?
365
410
  unless inherit_socket? || Process.pid == @pid
366
411
  raise InheritedError,
367
- "Tried to use a connection from a child process without reconnecting. " +
368
- "You need to reconnect to Redis after forking " +
369
- "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."
370
415
  end
371
416
  else
372
417
  connect
@@ -377,7 +422,7 @@ class Redis
377
422
  disconnect
378
423
 
379
424
  if attempts <= @options[:reconnect_attempts] && @reconnect
380
- sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
425
+ sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
381
426
  @options[:reconnect_delay_max]].min
382
427
 
383
428
  Kernel.sleep(sleep_t)
@@ -399,15 +444,14 @@ class Redis
399
444
 
400
445
  defaults.keys.each do |key|
401
446
  # Fill in defaults if needed
402
- if defaults[key].respond_to?(:call)
403
- defaults[key] = defaults[key].call
404
- end
447
+ defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
405
448
 
406
449
  # Symbolize only keys that are needed
407
- 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)
408
451
  end
409
452
 
410
- url = options[:url] || defaults[:url]
453
+ url = options[:url]
454
+ url = defaults[:url] if url.nil?
411
455
 
412
456
  # Override defaults from URL if given
413
457
  if url
@@ -416,12 +460,13 @@ class Redis
416
460
  uri = URI(url)
417
461
 
418
462
  if uri.scheme == "unix"
419
- defaults[:path] = uri.path
463
+ defaults[:path] = uri.path
420
464
  elsif uri.scheme == "redis" || uri.scheme == "rediss"
421
465
  defaults[:scheme] = uri.scheme
422
466
  defaults[:host] = uri.host if uri.host
423
467
  defaults[:port] = uri.port if uri.port
424
- defaults[:password] = CGI.unescape(uri.password) if uri.password
468
+ defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
469
+ defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
425
470
  defaults[:db] = uri.path[1..-1].to_i if uri.path
426
471
  defaults[:role] = :master
427
472
  else
@@ -447,7 +492,7 @@ class Redis
447
492
  options[:port] = options[:port].to_i
448
493
  end
449
494
 
450
- if options.has_key?(:timeout)
495
+ if options.key?(:timeout)
451
496
  options[:connect_timeout] ||= options[:timeout]
452
497
  options[:read_timeout] ||= options[:timeout]
453
498
  options[:write_timeout] ||= options[:timeout]
@@ -466,7 +511,7 @@ class Redis
466
511
 
467
512
  case options[:tcp_keepalive]
468
513
  when Hash
469
- [:time, :intvl, :probes].each do |key|
514
+ %i[time intvl probes].each do |key|
470
515
  unless options[:tcp_keepalive][key].is_a?(Integer)
471
516
  raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
472
517
  end
@@ -474,13 +519,13 @@ class Redis
474
519
 
475
520
  when Integer
476
521
  if options[:tcp_keepalive] >= 60
477
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 20, :intvl => 10, :probes => 2}
522
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
478
523
 
479
524
  elsif options[:tcp_keepalive] >= 30
480
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 10, :intvl => 5, :probes => 2}
525
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
481
526
 
482
527
  elsif options[:tcp_keepalive] >= 5
483
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 2, :intvl => 2, :probes => 1}
528
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
484
529
  end
485
530
  end
486
531
 
@@ -492,14 +537,14 @@ class Redis
492
537
  def _parse_driver(driver)
493
538
  driver = driver.to_s if driver.is_a?(Symbol)
494
539
 
495
- if driver.kind_of?(String)
540
+ if driver.is_a?(String)
496
541
  begin
497
542
  require_relative "connection/#{driver}"
498
- rescue LoadError, NameError => e
543
+ rescue LoadError, NameError
499
544
  begin
500
- require "connection/#{driver}"
501
- rescue LoadError, NameError => e
502
- raise RuntimeError, "Cannot load driver #{driver.inspect}: #{e.message}"
545
+ require "redis/connection/#{driver}"
546
+ rescue LoadError, NameError => error
547
+ raise "Cannot load driver #{driver.inspect}: #{error.message}"
503
548
  end
504
549
  end
505
550
 
@@ -518,18 +563,16 @@ class Redis
518
563
  @options
519
564
  end
520
565
 
521
- def check(client)
522
- end
566
+ def check(client); end
523
567
 
524
568
  class Sentinel < Connector
525
569
  def initialize(options)
526
570
  super(options)
527
571
 
528
- @options[:password] = DEFAULTS.fetch(:password)
529
572
  @options[:db] = DEFAULTS.fetch(:db)
530
573
 
531
574
  @sentinels = @options.delete(:sentinels).dup
532
- @role = @options.fetch(:role, "master").to_s
575
+ @role = (@options[:role] || "master").to_s
533
576
  @master = @options[:host]
534
577
  end
535
578
 
@@ -552,13 +595,13 @@ class Redis
552
595
 
553
596
  def resolve
554
597
  result = case @role
555
- when "master"
556
- resolve_master
557
- when "slave"
558
- resolve_slave
559
- else
560
- raise ArgumentError, "Unknown instance role #{@role}"
561
- end
598
+ when "master"
599
+ resolve_master
600
+ when "slave"
601
+ resolve_slave
602
+ else
603
+ raise ArgumentError, "Unknown instance role #{@role}"
604
+ end
562
605
 
563
606
  result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
564
607
  end
@@ -566,10 +609,12 @@ class Redis
566
609
  def sentinel_detect
567
610
  @sentinels.each do |sentinel|
568
611
  client = Client.new(@options.merge({
569
- :host => sentinel[:host],
570
- :port => sentinel[:port],
571
- :reconnect_attempts => 0,
572
- }))
612
+ host: sentinel[:host] || sentinel["host"],
613
+ port: sentinel[:port] || sentinel["port"],
614
+ username: sentinel[:username] || sentinel["username"],
615
+ password: sentinel[:password] || sentinel["password"],
616
+ reconnect_attempts: 0
617
+ }))
573
618
 
574
619
  begin
575
620
  if result = yield(client)
@@ -591,7 +636,7 @@ class Redis
591
636
  def resolve_master
592
637
  sentinel_detect do |client|
593
638
  if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
594
- {:host => reply[0], :port => reply[1]}
639
+ { host: reply[0], port: reply[1] }
595
640
  end
596
641
  end
597
642
  end
@@ -599,9 +644,19 @@ class Redis
599
644
  def resolve_slave
600
645
  sentinel_detect do |client|
601
646
  if reply = client.call(["sentinel", "slaves", @master])
602
- slave = Hash[*reply.sample]
603
-
604
- {:host => slave.fetch("ip"), :port => slave.fetch("port")}
647
+ slaves = reply.map { |s| s.each_slice(2).to_h }
648
+ slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
649
+ slaves.reject! { |s| s.fetch('flags').include?('s_down') }
650
+
651
+ if slaves.empty?
652
+ raise CannotConnectError, 'No slaves available.'
653
+ else
654
+ slave = slaves.sample
655
+ {
656
+ host: slave.fetch('ip'),
657
+ port: slave.fetch('port')
658
+ }
659
+ end
605
660
  end
606
661
  end
607
662
  end
@@ -10,22 +10,21 @@ class Redis
10
10
  module_function
11
11
 
12
12
  def load(nodes)
13
- details = {}
14
-
15
13
  nodes.each do |node|
16
- details = fetch_command_details(node)
17
- details.empty? ? next : break
14
+ begin
15
+ return fetch_command_details(node)
16
+ rescue CannotConnectError, ConnectionError, CommandError
17
+ next # can retry on another node
18
+ end
18
19
  end
19
20
 
20
- details
21
+ raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
21
22
  end
22
23
 
23
24
  def fetch_command_details(node)
24
25
  node.call(%i[command]).map do |reply|
25
26
  [reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
26
27
  end.to_h
27
- rescue CannotConnectError, ConnectionError, CommandError
28
- {} # can retry on another node
29
28
  end
30
29
 
31
30
  private_class_method :fetch_command_details
@@ -39,6 +39,7 @@ class Redis
39
39
  def call_master(command, &block)
40
40
  try_map do |node_key, client|
41
41
  next if slave?(node_key)
42
+
42
43
  client.call(command, &block)
43
44
  end.values
44
45
  end
@@ -48,6 +49,7 @@ class Redis
48
49
 
49
50
  try_map do |node_key, client|
50
51
  next if master?(node_key)
52
+
51
53
  client.call(command, &block)
52
54
  end.values
53
55
  end
@@ -74,8 +76,9 @@ class Redis
74
76
  clients = options.map do |node_key, option|
75
77
  next if replica_disabled? && slave?(node_key)
76
78
 
79
+ option = option.merge(readonly: true) if slave?(node_key)
80
+
77
81
  client = Client.new(option)
78
- client.call(%i[readonly]) if slave?(node_key)
79
82
  [node_key, client]
80
83
  end
81
84
 
@@ -97,6 +100,7 @@ class Redis
97
100
  end
98
101
 
99
102
  return results if errors.empty?
103
+
100
104
  raise CommandErrorCollection, errors
101
105
  end
102
106
  end
@@ -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,36 @@ 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, :username)
22
+ add_common_node_option_if_needed(options, @node_opts, :password)
20
23
  @options = options
21
24
  end
22
25
 
23
26
  def per_node_key
24
- @node_uris.map { |uri| [NodeKey.build_from_uri(uri), @options.merge(url: uri.to_s)] }
27
+ @node_opts.map { |opt| [NodeKey.build_from_host_port(opt[:host], opt[:port]), @options.merge(opt)] }
25
28
  .to_h
26
29
  end
27
30
 
28
- def secure?
29
- @node_uris.any? { |uri| uri.scheme == SECURE_SCHEME } || @options[:ssl_params] || false
30
- end
31
-
32
31
  def use_replica?
33
32
  @replica
34
33
  end
35
34
 
36
35
  def update_node(addrs)
37
- @node_uris = build_node_uris(addrs)
36
+ @node_opts = build_node_options(addrs)
38
37
  end
39
38
 
40
39
  def add_node(host, port)
41
- @node_uris << parse_node_hash(host: host, port: port)
40
+ @node_opts << { host: host, port: port }
42
41
  end
43
42
 
44
43
  private
45
44
 
46
- def build_node_uris(addrs)
45
+ def build_node_options(addrs)
47
46
  raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
47
+
48
48
  addrs.map { |addr| parse_node_addr(addr) }
49
49
  end
50
50
 
@@ -53,7 +53,7 @@ class Redis
53
53
  when String
54
54
  parse_node_url(addr)
55
55
  when Hash
56
- parse_node_hash(addr)
56
+ parse_node_option(addr)
57
57
  else
58
58
  raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash'
59
59
  end
@@ -62,15 +62,31 @@ class Redis
62
62
  def parse_node_url(addr)
63
63
  uri = URI(addr)
64
64
  raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
65
- uri
65
+
66
+ db = uri.path.split('/')[1]&.to_i
67
+
68
+ { scheme: uri.scheme, username: uri.user, password: uri.password, host: uri.host, port: uri.port, db: db }
69
+ .reject { |_, v| v.nil? || v == '' }
66
70
  rescue URI::InvalidURIError => err
67
71
  raise InvalidClientOptionError, err.message
68
72
  end
69
73
 
70
- def parse_node_hash(addr)
74
+ def parse_node_option(addr)
71
75
  addr = addr.map { |k, v| [k.to_sym, v] }.to_h
72
- raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys' if addr.values_at(:host, :port).any?(&:nil?)
73
- URI::Generic.build(scheme: DEFAULT_SCHEME, host: addr[:host], port: addr[:port].to_i)
76
+ if addr.values_at(:host, :port).any?(&:nil?)
77
+ raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
78
+ end
79
+
80
+ addr
81
+ end
82
+
83
+ # Redis cluster node returns only host and port information.
84
+ # So we should complement additional information such as:
85
+ # scheme, username, password and so on.
86
+ def add_common_node_option_if_needed(options, node_opts, key)
87
+ return options if options[key].nil? && node_opts.first[key].nil?
88
+
89
+ options[key] ||= node_opts.first[key]
74
90
  end
75
91
  end
76
92
  end