redis 4.1.0 → 4.2.0

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.
@@ -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)