redis 4.1.4 → 4.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +8 -4
- data/lib/redis.rb +275 -237
- data/lib/redis/client.rb +66 -70
- data/lib/redis/cluster.rb +4 -0
- data/lib/redis/cluster/node.rb +3 -0
- data/lib/redis/cluster/option.rb +4 -1
- data/lib/redis/cluster/slot.rb +28 -14
- data/lib/redis/cluster/slot_loader.rb +2 -3
- data/lib/redis/connection.rb +1 -0
- data/lib/redis/connection/command_helper.rb +2 -2
- data/lib/redis/connection/hiredis.rb +3 -3
- data/lib/redis/connection/registry.rb +1 -1
- data/lib/redis/connection/ruby.rb +38 -61
- data/lib/redis/connection/synchrony.rb +8 -4
- data/lib/redis/distributed.rb +72 -50
- data/lib/redis/errors.rb +1 -0
- data/lib/redis/hash_ring.rb +14 -14
- data/lib/redis/pipeline.rb +6 -8
- data/lib/redis/subscribe.rb +10 -12
- data/lib/redis/version.rb +2 -1
- metadata +10 -5
data/lib/redis/client.rb
CHANGED
@@ -6,24 +6,25 @@ require "cgi"
|
|
6
6
|
|
7
7
|
class Redis
|
8
8
|
class Client
|
9
|
-
|
10
9
|
DEFAULTS = {
|
11
|
-
:
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:
|
15
|
-
:
|
16
|
-
:
|
17
|
-
:
|
18
|
-
:
|
19
|
-
:
|
20
|
-
:
|
21
|
-
:
|
22
|
-
:
|
23
|
-
:
|
24
|
-
:
|
25
|
-
:
|
26
|
-
|
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
|
27
28
|
|
28
29
|
attr_reader :options
|
29
30
|
|
@@ -89,7 +90,7 @@ class Redis
|
|
89
90
|
@pending_reads = 0
|
90
91
|
|
91
92
|
@connector =
|
92
|
-
if options.
|
93
|
+
if !@options[:sentinels].nil?
|
93
94
|
Connector::Sentinel.new(@options)
|
94
95
|
elsif options.include?(:connector) && options[:connector].respond_to?(:new)
|
95
96
|
options.delete(:connector).new(@options)
|
@@ -166,6 +167,7 @@ class Redis
|
|
166
167
|
end
|
167
168
|
rescue ConnectionError => e
|
168
169
|
return nil if pipeline.shutdown?
|
170
|
+
|
169
171
|
# Assume the pipeline was sent in one piece, but execution of
|
170
172
|
# SHUTDOWN caused none of the replies for commands that were executed
|
171
173
|
# prior to it from coming back around.
|
@@ -244,13 +246,13 @@ class Redis
|
|
244
246
|
end
|
245
247
|
|
246
248
|
def connected?
|
247
|
-
!!
|
249
|
+
!!(connection && connection.connected?)
|
248
250
|
end
|
249
251
|
|
250
252
|
def disconnect
|
251
253
|
connection.disconnect if connected?
|
252
254
|
end
|
253
|
-
|
255
|
+
alias close disconnect
|
254
256
|
|
255
257
|
def reconnect
|
256
258
|
disconnect
|
@@ -301,30 +303,27 @@ class Redis
|
|
301
303
|
with_socket_timeout(0, &blk)
|
302
304
|
end
|
303
305
|
|
304
|
-
def with_reconnect(val=true)
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
@reconnect = original
|
310
|
-
end
|
306
|
+
def with_reconnect(val = true)
|
307
|
+
original, @reconnect = @reconnect, val
|
308
|
+
yield
|
309
|
+
ensure
|
310
|
+
@reconnect = original
|
311
311
|
end
|
312
312
|
|
313
313
|
def without_reconnect(&blk)
|
314
314
|
with_reconnect(false, &blk)
|
315
315
|
end
|
316
316
|
|
317
|
-
|
317
|
+
protected
|
318
318
|
|
319
319
|
def logging(commands)
|
320
|
-
return yield unless @logger
|
320
|
+
return yield unless @logger&.debug?
|
321
321
|
|
322
322
|
begin
|
323
323
|
commands.each do |name, *args|
|
324
324
|
logged_args = args.map do |a|
|
325
|
-
|
326
|
-
|
327
|
-
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
|
328
327
|
else
|
329
328
|
# handle poorly-behaved descendants of BasicObject
|
330
329
|
klass = a.instance_exec { (class << self; self end).superclass }
|
@@ -358,9 +357,9 @@ class Redis
|
|
358
357
|
Errno::ENETUNREACH,
|
359
358
|
Errno::ENOENT,
|
360
359
|
Errno::ETIMEDOUT,
|
361
|
-
Errno::EINVAL
|
360
|
+
Errno::EINVAL => error
|
362
361
|
|
363
|
-
raise CannotConnectError, "Error connecting to Redis on #{location} (#{
|
362
|
+
raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
|
364
363
|
end
|
365
364
|
|
366
365
|
def ensure_connected
|
@@ -374,9 +373,9 @@ class Redis
|
|
374
373
|
if connected?
|
375
374
|
unless inherit_socket? || Process.pid == @pid
|
376
375
|
raise InheritedError,
|
377
|
-
|
378
|
-
|
379
|
-
|
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."
|
380
379
|
end
|
381
380
|
else
|
382
381
|
connect
|
@@ -387,7 +386,7 @@ class Redis
|
|
387
386
|
disconnect
|
388
387
|
|
389
388
|
if attempts <= @options[:reconnect_attempts] && @reconnect
|
390
|
-
sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
|
389
|
+
sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
|
391
390
|
@options[:reconnect_delay_max]].min
|
392
391
|
|
393
392
|
Kernel.sleep(sleep_t)
|
@@ -409,16 +408,14 @@ class Redis
|
|
409
408
|
|
410
409
|
defaults.keys.each do |key|
|
411
410
|
# Fill in defaults if needed
|
412
|
-
if defaults[key].respond_to?(:call)
|
413
|
-
defaults[key] = defaults[key].call
|
414
|
-
end
|
411
|
+
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
415
412
|
|
416
413
|
# Symbolize only keys that are needed
|
417
|
-
options[key] = options[key.to_s] if options.
|
414
|
+
options[key] = options[key.to_s] if options.key?(key.to_s)
|
418
415
|
end
|
419
416
|
|
420
417
|
url = options[:url]
|
421
|
-
url = defaults[:url] if url
|
418
|
+
url = defaults[:url] if url.nil?
|
422
419
|
|
423
420
|
# Override defaults from URL if given
|
424
421
|
if url
|
@@ -427,7 +424,7 @@ class Redis
|
|
427
424
|
uri = URI(url)
|
428
425
|
|
429
426
|
if uri.scheme == "unix"
|
430
|
-
defaults[:path]
|
427
|
+
defaults[:path] = uri.path
|
431
428
|
elsif uri.scheme == "redis" || uri.scheme == "rediss"
|
432
429
|
defaults[:scheme] = uri.scheme
|
433
430
|
defaults[:host] = uri.host if uri.host
|
@@ -458,7 +455,7 @@ class Redis
|
|
458
455
|
options[:port] = options[:port].to_i
|
459
456
|
end
|
460
457
|
|
461
|
-
if options.
|
458
|
+
if options.key?(:timeout)
|
462
459
|
options[:connect_timeout] ||= options[:timeout]
|
463
460
|
options[:read_timeout] ||= options[:timeout]
|
464
461
|
options[:write_timeout] ||= options[:timeout]
|
@@ -477,7 +474,7 @@ class Redis
|
|
477
474
|
|
478
475
|
case options[:tcp_keepalive]
|
479
476
|
when Hash
|
480
|
-
[
|
477
|
+
%i[time intvl probes].each do |key|
|
481
478
|
unless options[:tcp_keepalive][key].is_a?(Integer)
|
482
479
|
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
483
480
|
end
|
@@ -485,13 +482,13 @@ class Redis
|
|
485
482
|
|
486
483
|
when Integer
|
487
484
|
if options[:tcp_keepalive] >= 60
|
488
|
-
options[:tcp_keepalive] = {:
|
485
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
489
486
|
|
490
487
|
elsif options[:tcp_keepalive] >= 30
|
491
|
-
options[:tcp_keepalive] = {:
|
488
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
492
489
|
|
493
490
|
elsif options[:tcp_keepalive] >= 5
|
494
|
-
options[:tcp_keepalive] = {:
|
491
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
495
492
|
end
|
496
493
|
end
|
497
494
|
|
@@ -503,14 +500,14 @@ class Redis
|
|
503
500
|
def _parse_driver(driver)
|
504
501
|
driver = driver.to_s if driver.is_a?(Symbol)
|
505
502
|
|
506
|
-
if driver.
|
503
|
+
if driver.is_a?(String)
|
507
504
|
begin
|
508
505
|
require_relative "connection/#{driver}"
|
509
|
-
rescue LoadError, NameError
|
506
|
+
rescue LoadError, NameError
|
510
507
|
begin
|
511
508
|
require "connection/#{driver}"
|
512
|
-
rescue LoadError, NameError =>
|
513
|
-
raise
|
509
|
+
rescue LoadError, NameError => error
|
510
|
+
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
514
511
|
end
|
515
512
|
end
|
516
513
|
|
@@ -529,8 +526,7 @@ class Redis
|
|
529
526
|
@options
|
530
527
|
end
|
531
528
|
|
532
|
-
def check(client)
|
533
|
-
end
|
529
|
+
def check(client); end
|
534
530
|
|
535
531
|
class Sentinel < Connector
|
536
532
|
def initialize(options)
|
@@ -539,7 +535,7 @@ class Redis
|
|
539
535
|
@options[:db] = DEFAULTS.fetch(:db)
|
540
536
|
|
541
537
|
@sentinels = @options.delete(:sentinels).dup
|
542
|
-
@role = @options
|
538
|
+
@role = (@options[:role] || "master").to_s
|
543
539
|
@master = @options[:host]
|
544
540
|
end
|
545
541
|
|
@@ -562,13 +558,13 @@ class Redis
|
|
562
558
|
|
563
559
|
def resolve
|
564
560
|
result = case @role
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
561
|
+
when "master"
|
562
|
+
resolve_master
|
563
|
+
when "slave"
|
564
|
+
resolve_slave
|
565
|
+
else
|
566
|
+
raise ArgumentError, "Unknown instance role #{@role}"
|
567
|
+
end
|
572
568
|
|
573
569
|
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
574
570
|
end
|
@@ -576,11 +572,11 @@ class Redis
|
|
576
572
|
def sentinel_detect
|
577
573
|
@sentinels.each do |sentinel|
|
578
574
|
client = Client.new(@options.merge({
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
575
|
+
host: sentinel[:host] || sentinel["host"],
|
576
|
+
port: sentinel[:port] || sentinel["port"],
|
577
|
+
password: sentinel[:password] || sentinel["password"],
|
578
|
+
reconnect_attempts: 0
|
579
|
+
}))
|
584
580
|
|
585
581
|
begin
|
586
582
|
if result = yield(client)
|
@@ -602,7 +598,7 @@ class Redis
|
|
602
598
|
def resolve_master
|
603
599
|
sentinel_detect do |client|
|
604
600
|
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
605
|
-
{:
|
601
|
+
{ host: reply[0], port: reply[1] }
|
606
602
|
end
|
607
603
|
end
|
608
604
|
end
|
@@ -620,7 +616,7 @@ class Redis
|
|
620
616
|
slave = slaves.sample
|
621
617
|
{
|
622
618
|
host: slave.fetch('ip'),
|
623
|
-
port: slave.fetch('port')
|
619
|
+
port: slave.fetch('port')
|
624
620
|
}
|
625
621
|
end
|
626
622
|
end
|
data/lib/redis/cluster.rb
CHANGED
@@ -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
|
@@ -216,11 +217,13 @@ class Redis
|
|
216
217
|
rescue CommandError => err
|
217
218
|
if err.message.start_with?('MOVED')
|
218
219
|
raise if retry_count <= 0
|
220
|
+
|
219
221
|
node = assign_redirection_node(err.message)
|
220
222
|
retry_count -= 1
|
221
223
|
retry
|
222
224
|
elsif err.message.start_with?('ASK')
|
223
225
|
raise if retry_count <= 0
|
226
|
+
|
224
227
|
node = assign_asking_node(err.message)
|
225
228
|
node.call(%i[asking])
|
226
229
|
retry_count -= 1
|
@@ -266,6 +269,7 @@ class Redis
|
|
266
269
|
|
267
270
|
def find_node(node_key)
|
268
271
|
return @node.sample if node_key.nil?
|
272
|
+
|
269
273
|
@node.find_by(node_key)
|
270
274
|
rescue Node::ReloadNeeded
|
271
275
|
update_cluster_info!(node_key)
|
data/lib/redis/cluster/node.rb
CHANGED
@@ -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
|
data/lib/redis/cluster/option.rb
CHANGED
@@ -43,6 +43,7 @@ class Redis
|
|
43
43
|
|
44
44
|
def build_node_options(addrs)
|
45
45
|
raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
|
46
|
+
|
46
47
|
addrs.map { |addr| parse_node_addr(addr) }
|
47
48
|
end
|
48
49
|
|
@@ -69,7 +70,9 @@ class Redis
|
|
69
70
|
|
70
71
|
def parse_node_option(addr)
|
71
72
|
addr = addr.map { |k, v| [k.to_sym, v] }.to_h
|
72
|
-
|
73
|
+
if addr.values_at(:host, :port).any?(&:nil?)
|
74
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
|
75
|
+
end
|
73
76
|
|
74
77
|
addr
|
75
78
|
end
|
data/lib/redis/cluster/slot.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'set'
|
4
|
-
|
5
3
|
class Redis
|
6
4
|
class Cluster
|
7
5
|
# Keep slot and node key map for Redis Cluster Client
|
@@ -28,11 +26,20 @@ class Redis
|
|
28
26
|
return nil unless exists?(slot)
|
29
27
|
return find_node_key_of_master(slot) if replica_disabled?
|
30
28
|
|
31
|
-
@map[slot][:slaves].
|
29
|
+
@map[slot][:slaves].sample
|
32
30
|
end
|
33
31
|
|
34
32
|
def put(slot, node_key)
|
35
|
-
|
33
|
+
# Since we're sharing a hash for build_slot_node_key_map, duplicate it
|
34
|
+
# if it already exists instead of preserving as-is.
|
35
|
+
@map[slot] = @map[slot] ? @map[slot].dup : { master: nil, slaves: [] }
|
36
|
+
|
37
|
+
if master?(node_key)
|
38
|
+
@map[slot][:master] = node_key
|
39
|
+
elsif !@map[slot][:slaves].include?(node_key)
|
40
|
+
@map[slot][:slaves] << node_key
|
41
|
+
end
|
42
|
+
|
36
43
|
nil
|
37
44
|
end
|
38
45
|
|
@@ -52,20 +59,27 @@ class Redis
|
|
52
59
|
|
53
60
|
# available_slots is mapping of node_key to list of slot ranges
|
54
61
|
def build_slot_node_key_map(available_slots)
|
55
|
-
|
56
|
-
|
57
|
-
|
62
|
+
by_ranges = {}
|
63
|
+
available_slots.each do |node_key, slots_arr|
|
64
|
+
by_ranges[slots_arr] ||= { master: nil, slaves: [] }
|
65
|
+
|
66
|
+
if master?(node_key)
|
67
|
+
by_ranges[slots_arr][:master] = node_key
|
68
|
+
elsif !by_ranges[slots_arr][:slaves].include?(node_key)
|
69
|
+
by_ranges[slots_arr][:slaves] << node_key
|
58
70
|
end
|
59
71
|
end
|
60
|
-
end
|
61
72
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
73
|
+
by_slot = {}
|
74
|
+
by_ranges.each do |slots_arr, nodes|
|
75
|
+
slots_arr.each do |slots|
|
76
|
+
slots.each do |slot|
|
77
|
+
by_slot[slot] = nodes
|
78
|
+
end
|
79
|
+
end
|
68
80
|
end
|
81
|
+
|
82
|
+
by_slot
|
69
83
|
end
|
70
84
|
end
|
71
85
|
end
|
@@ -25,9 +25,8 @@ class Redis
|
|
25
25
|
def fetch_slot_info(node)
|
26
26
|
hash_with_default_arr = Hash.new { |h, k| h[k] = [] }
|
27
27
|
node.call(%i[cluster slots])
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
.flat_map { |arr| parse_slot_info(arr, default_ip: node.host) }
|
29
|
+
.each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] }
|
31
30
|
rescue CannotConnectError, ConnectionError, CommandError
|
32
31
|
{} # can retry on another node
|
33
32
|
end
|