redis 4.1.0 → 4.5.1

Sign up to get free protection for your applications and to get access to all the features.
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