redis 4.1.0 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,27 +1,30 @@
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
-
8
9
  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
- }
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
25
28
 
26
29
  attr_reader :options
27
30
 
@@ -87,7 +90,7 @@ class Redis
87
90
  @pending_reads = 0
88
91
 
89
92
  @connector =
90
- if options.include?(:sentinels)
93
+ if !@options[:sentinels].nil?
91
94
  Connector::Sentinel.new(@options)
92
95
  elsif options.include?(:connector) && options[:connector].respond_to?(:new)
93
96
  options.delete(:connector).new(@options)
@@ -155,16 +158,16 @@ class Redis
155
158
  end
156
159
 
157
160
  def call_pipeline(pipeline)
158
- commands = pipeline.commands
159
- return [] if commands.empty?
161
+ return [] if pipeline.futures.empty?
160
162
 
161
163
  with_reconnect pipeline.with_reconnect? do
162
164
  begin
163
- pipeline.finish(call_pipelined(commands)).tap do
165
+ pipeline.finish(call_pipelined(pipeline)).tap do
164
166
  self.db = pipeline.db if pipeline.db
165
167
  end
166
168
  rescue ConnectionError => e
167
169
  return nil if pipeline.shutdown?
170
+
168
171
  # Assume the pipeline was sent in one piece, but execution of
169
172
  # SHUTDOWN caused none of the replies for commands that were executed
170
173
  # prior to it from coming back around.
@@ -173,8 +176,8 @@ class Redis
173
176
  end
174
177
  end
175
178
 
176
- def call_pipelined(commands)
177
- return [] if commands.empty?
179
+ def call_pipelined(pipeline)
180
+ return [] if pipeline.futures.empty?
178
181
 
179
182
  # The method #ensure_connected (called from #process) reconnects once on
180
183
  # I/O errors. To make an effort in making sure that commands are not
@@ -184,6 +187,8 @@ class Redis
184
187
  # already successfully executed commands. To circumvent this, don't retry
185
188
  # after the first reply has been read successfully.
186
189
 
190
+ commands = pipeline.commands
191
+
187
192
  result = Array.new(commands.size)
188
193
  reconnect = @reconnect
189
194
 
@@ -191,8 +196,12 @@ class Redis
191
196
  exception = nil
192
197
 
193
198
  process(commands) do
194
- commands.size.times do |i|
195
- reply = read
199
+ pipeline.timeouts.each_with_index do |timeout, i|
200
+ reply = if timeout
201
+ with_socket_timeout(timeout) { read }
202
+ else
203
+ read
204
+ end
196
205
  result[i] = reply
197
206
  @reconnect = false
198
207
  exception = reply if exception.nil? && reply.is_a?(CommandError)
@@ -237,12 +246,13 @@ class Redis
237
246
  end
238
247
 
239
248
  def connected?
240
- !! (connection && connection.connected?)
249
+ !!(connection && connection.connected?)
241
250
  end
242
251
 
243
252
  def disconnect
244
253
  connection.disconnect if connected?
245
254
  end
255
+ alias close disconnect
246
256
 
247
257
  def reconnect
248
258
  disconnect
@@ -293,30 +303,27 @@ class Redis
293
303
  with_socket_timeout(0, &blk)
294
304
  end
295
305
 
296
- def with_reconnect(val=true)
297
- begin
298
- original, @reconnect = @reconnect, val
299
- yield
300
- ensure
301
- @reconnect = original
302
- end
306
+ def with_reconnect(val = true)
307
+ original, @reconnect = @reconnect, val
308
+ yield
309
+ ensure
310
+ @reconnect = original
303
311
  end
304
312
 
305
313
  def without_reconnect(&blk)
306
314
  with_reconnect(false, &blk)
307
315
  end
308
316
 
309
- protected
317
+ protected
310
318
 
311
319
  def logging(commands)
312
- return yield unless @logger && @logger.debug?
320
+ return yield unless @logger&.debug?
313
321
 
314
322
  begin
315
323
  commands.each do |name, *args|
316
324
  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
325
+ if a.respond_to?(:inspect) then a.inspect
326
+ elsif a.respond_to?(:to_s) then a.to_s
320
327
  else
321
328
  # handle poorly-behaved descendants of BasicObject
322
329
  klass = a.instance_exec { (class << self; self end).superclass }
@@ -343,14 +350,16 @@ class Redis
343
350
  @pending_reads = 0
344
351
  rescue TimeoutError,
345
352
  SocketError,
353
+ Errno::EADDRNOTAVAIL,
346
354
  Errno::ECONNREFUSED,
347
355
  Errno::EHOSTDOWN,
348
356
  Errno::EHOSTUNREACH,
349
357
  Errno::ENETUNREACH,
350
358
  Errno::ENOENT,
351
- Errno::ETIMEDOUT
359
+ Errno::ETIMEDOUT,
360
+ Errno::EINVAL => error
352
361
 
353
- raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
362
+ raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
354
363
  end
355
364
 
356
365
  def ensure_connected
@@ -364,9 +373,9 @@ class Redis
364
373
  if connected?
365
374
  unless inherit_socket? || Process.pid == @pid
366
375
  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."
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."
370
379
  end
371
380
  else
372
381
  connect
@@ -377,7 +386,7 @@ class Redis
377
386
  disconnect
378
387
 
379
388
  if attempts <= @options[:reconnect_attempts] && @reconnect
380
- sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
389
+ sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
381
390
  @options[:reconnect_delay_max]].min
382
391
 
383
392
  Kernel.sleep(sleep_t)
@@ -399,15 +408,14 @@ class Redis
399
408
 
400
409
  defaults.keys.each do |key|
401
410
  # Fill in defaults if needed
402
- if defaults[key].respond_to?(:call)
403
- defaults[key] = defaults[key].call
404
- end
411
+ defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
405
412
 
406
413
  # Symbolize only keys that are needed
407
- 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)
408
415
  end
409
416
 
410
- url = options[:url] || defaults[:url]
417
+ url = options[:url]
418
+ url = defaults[:url] if url.nil?
411
419
 
412
420
  # Override defaults from URL if given
413
421
  if url
@@ -416,7 +424,7 @@ class Redis
416
424
  uri = URI(url)
417
425
 
418
426
  if uri.scheme == "unix"
419
- defaults[:path] = uri.path
427
+ defaults[:path] = uri.path
420
428
  elsif uri.scheme == "redis" || uri.scheme == "rediss"
421
429
  defaults[:scheme] = uri.scheme
422
430
  defaults[:host] = uri.host if uri.host
@@ -447,7 +455,7 @@ class Redis
447
455
  options[:port] = options[:port].to_i
448
456
  end
449
457
 
450
- if options.has_key?(:timeout)
458
+ if options.key?(:timeout)
451
459
  options[:connect_timeout] ||= options[:timeout]
452
460
  options[:read_timeout] ||= options[:timeout]
453
461
  options[:write_timeout] ||= options[:timeout]
@@ -466,7 +474,7 @@ class Redis
466
474
 
467
475
  case options[:tcp_keepalive]
468
476
  when Hash
469
- [:time, :intvl, :probes].each do |key|
477
+ %i[time intvl probes].each do |key|
470
478
  unless options[:tcp_keepalive][key].is_a?(Integer)
471
479
  raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
472
480
  end
@@ -474,13 +482,13 @@ class Redis
474
482
 
475
483
  when Integer
476
484
  if options[:tcp_keepalive] >= 60
477
- 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 }
478
486
 
479
487
  elsif options[:tcp_keepalive] >= 30
480
- 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 }
481
489
 
482
490
  elsif options[:tcp_keepalive] >= 5
483
- 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 }
484
492
  end
485
493
  end
486
494
 
@@ -492,14 +500,14 @@ class Redis
492
500
  def _parse_driver(driver)
493
501
  driver = driver.to_s if driver.is_a?(Symbol)
494
502
 
495
- if driver.kind_of?(String)
503
+ if driver.is_a?(String)
496
504
  begin
497
505
  require_relative "connection/#{driver}"
498
- rescue LoadError, NameError => e
506
+ rescue LoadError, NameError
499
507
  begin
500
508
  require "connection/#{driver}"
501
- rescue LoadError, NameError => e
502
- raise RuntimeError, "Cannot load driver #{driver.inspect}: #{e.message}"
509
+ rescue LoadError, NameError => error
510
+ raise "Cannot load driver #{driver.inspect}: #{error.message}"
503
511
  end
504
512
  end
505
513
 
@@ -518,18 +526,16 @@ class Redis
518
526
  @options
519
527
  end
520
528
 
521
- def check(client)
522
- end
529
+ def check(client); end
523
530
 
524
531
  class Sentinel < Connector
525
532
  def initialize(options)
526
533
  super(options)
527
534
 
528
- @options[:password] = DEFAULTS.fetch(:password)
529
535
  @options[:db] = DEFAULTS.fetch(:db)
530
536
 
531
537
  @sentinels = @options.delete(:sentinels).dup
532
- @role = @options.fetch(:role, "master").to_s
538
+ @role = (@options[:role] || "master").to_s
533
539
  @master = @options[:host]
534
540
  end
535
541
 
@@ -552,13 +558,13 @@ class Redis
552
558
 
553
559
  def resolve
554
560
  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
561
+ when "master"
562
+ resolve_master
563
+ when "slave"
564
+ resolve_slave
565
+ else
566
+ raise ArgumentError, "Unknown instance role #{@role}"
567
+ end
562
568
 
563
569
  result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
564
570
  end
@@ -566,10 +572,11 @@ class Redis
566
572
  def sentinel_detect
567
573
  @sentinels.each do |sentinel|
568
574
  client = Client.new(@options.merge({
569
- :host => sentinel[:host],
570
- :port => sentinel[:port],
571
- :reconnect_attempts => 0,
572
- }))
575
+ host: sentinel[:host] || sentinel["host"],
576
+ port: sentinel[:port] || sentinel["port"],
577
+ password: sentinel[:password] || sentinel["password"],
578
+ reconnect_attempts: 0
579
+ }))
573
580
 
574
581
  begin
575
582
  if result = yield(client)
@@ -591,7 +598,7 @@ class Redis
591
598
  def resolve_master
592
599
  sentinel_detect do |client|
593
600
  if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
594
- {:host => reply[0], :port => reply[1]}
601
+ { host: reply[0], port: reply[1] }
595
602
  end
596
603
  end
597
604
  end
@@ -599,9 +606,19 @@ class Redis
599
606
  def resolve_slave
600
607
  sentinel_detect do |client|
601
608
  if reply = client.call(["sentinel", "slaves", @master])
602
- slave = Hash[*reply.sample]
603
-
604
- {:host => slave.fetch("ip"), :port => slave.fetch("port")}
609
+ slaves = reply.map { |s| s.each_slice(2).to_h }
610
+ slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
611
+ slaves.reject! { |s| s.fetch('flags').include?('s_down') }
612
+
613
+ if slaves.empty?
614
+ raise CannotConnectError, 'No slaves available.'
615
+ else
616
+ slave = slaves.sample
617
+ {
618
+ host: slave.fetch('ip'),
619
+ port: slave.fetch('port')
620
+ }
621
+ end
605
622
  end
606
623
  end
607
624
  end
@@ -80,6 +80,7 @@ class Redis
80
80
  def call_pipeline(pipeline)
81
81
  node_keys, command_keys = extract_keys_in_pipeline(pipeline)
82
82
  raise CrossSlotPipeliningError, command_keys if node_keys.size > 1
83
+
83
84
  node = find_node(node_keys.first)
84
85
  try_send(node, :call_pipeline, pipeline)
85
86
  end
@@ -112,12 +113,11 @@ class Redis
112
113
  node = Node.new(option.per_node_key)
113
114
  available_slots = SlotLoader.load(node)
114
115
  node_flags = NodeLoader.load_flags(node)
115
- available_node_urls = NodeKey.to_node_urls(available_slots.keys, secure: option.secure?)
116
- option.update_node(available_node_urls)
116
+ option.update_node(available_slots.keys.map { |k| NodeKey.optionize(k) })
117
117
  [Node.new(option.per_node_key, node_flags, option.use_replica?),
118
118
  Slot.new(available_slots, node_flags, option.use_replica?)]
119
119
  ensure
120
- node.map(&:disconnect)
120
+ node&.each(&:disconnect)
121
121
  end
122
122
 
123
123
  def fetch_command_details(nodes)
@@ -216,9 +216,14 @@ class Redis
216
216
  node.public_send(method_name, *args, &block)
217
217
  rescue CommandError => err
218
218
  if err.message.start_with?('MOVED')
219
- assign_redirection_node(err.message).public_send(method_name, *args, &block)
219
+ raise if retry_count <= 0
220
+
221
+ node = assign_redirection_node(err.message)
222
+ retry_count -= 1
223
+ retry
220
224
  elsif err.message.start_with?('ASK')
221
225
  raise if retry_count <= 0
226
+
222
227
  node = assign_asking_node(err.message)
223
228
  node.call(%i[asking])
224
229
  retry_count -= 1
@@ -226,6 +231,9 @@ class Redis
226
231
  else
227
232
  raise
228
233
  end
234
+ rescue CannotConnectError
235
+ update_cluster_info!
236
+ raise
229
237
  end
230
238
 
231
239
  def assign_redirection_node(err_msg)
@@ -261,6 +269,7 @@ class Redis
261
269
 
262
270
  def find_node(node_key)
263
271
  return @node.sample if node_key.nil?
272
+
264
273
  @node.find_by(node_key)
265
274
  rescue Node::ReloadNeeded
266
275
  update_cluster_info!(node_key)
@@ -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)