redis 4.1.4 → 4.2.4
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 +35 -0
- data/README.md +9 -4
- data/lib/redis.rb +287 -248
- data/lib/redis/client.rb +71 -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 +88 -94
- data/lib/redis/connection/synchrony.rb +8 -4
- data/lib/redis/distributed.rb +108 -57
- 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 +14 -9
data/lib/redis/client.rb
CHANGED
@@ -6,24 +6,30 @@ require "cgi"
|
|
6
6
|
|
7
7
|
class Redis
|
8
8
|
class Client
|
9
|
-
|
9
|
+
# Defaults are also used for converting string keys to symbols.
|
10
10
|
DEFAULTS = {
|
11
|
-
:
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:
|
15
|
-
:
|
16
|
-
:
|
17
|
-
:
|
18
|
-
:
|
19
|
-
:
|
20
|
-
:
|
21
|
-
:
|
22
|
-
:
|
23
|
-
:
|
24
|
-
:
|
25
|
-
:
|
26
|
-
|
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
|
+
password: nil,
|
21
|
+
db: 0,
|
22
|
+
driver: nil,
|
23
|
+
id: nil,
|
24
|
+
tcp_keepalive: 0,
|
25
|
+
reconnect_attempts: 1,
|
26
|
+
reconnect_delay: 0,
|
27
|
+
reconnect_delay_max: 0.5,
|
28
|
+
inherit_socket: false,
|
29
|
+
logger: nil,
|
30
|
+
sentinels: nil,
|
31
|
+
role: nil
|
32
|
+
}.freeze
|
27
33
|
|
28
34
|
attr_reader :options
|
29
35
|
|
@@ -89,7 +95,7 @@ class Redis
|
|
89
95
|
@pending_reads = 0
|
90
96
|
|
91
97
|
@connector =
|
92
|
-
if options.
|
98
|
+
if !@options[:sentinels].nil?
|
93
99
|
Connector::Sentinel.new(@options)
|
94
100
|
elsif options.include?(:connector) && options[:connector].respond_to?(:new)
|
95
101
|
options.delete(:connector).new(@options)
|
@@ -166,6 +172,7 @@ class Redis
|
|
166
172
|
end
|
167
173
|
rescue ConnectionError => e
|
168
174
|
return nil if pipeline.shutdown?
|
175
|
+
|
169
176
|
# Assume the pipeline was sent in one piece, but execution of
|
170
177
|
# SHUTDOWN caused none of the replies for commands that were executed
|
171
178
|
# prior to it from coming back around.
|
@@ -244,13 +251,13 @@ class Redis
|
|
244
251
|
end
|
245
252
|
|
246
253
|
def connected?
|
247
|
-
!!
|
254
|
+
!!(connection && connection.connected?)
|
248
255
|
end
|
249
256
|
|
250
257
|
def disconnect
|
251
258
|
connection.disconnect if connected?
|
252
259
|
end
|
253
|
-
|
260
|
+
alias close disconnect
|
254
261
|
|
255
262
|
def reconnect
|
256
263
|
disconnect
|
@@ -301,30 +308,27 @@ class Redis
|
|
301
308
|
with_socket_timeout(0, &blk)
|
302
309
|
end
|
303
310
|
|
304
|
-
def with_reconnect(val=true)
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
@reconnect = original
|
310
|
-
end
|
311
|
+
def with_reconnect(val = true)
|
312
|
+
original, @reconnect = @reconnect, val
|
313
|
+
yield
|
314
|
+
ensure
|
315
|
+
@reconnect = original
|
311
316
|
end
|
312
317
|
|
313
318
|
def without_reconnect(&blk)
|
314
319
|
with_reconnect(false, &blk)
|
315
320
|
end
|
316
321
|
|
317
|
-
|
322
|
+
protected
|
318
323
|
|
319
324
|
def logging(commands)
|
320
|
-
return yield unless @logger
|
325
|
+
return yield unless @logger&.debug?
|
321
326
|
|
322
327
|
begin
|
323
328
|
commands.each do |name, *args|
|
324
329
|
logged_args = args.map do |a|
|
325
|
-
|
326
|
-
|
327
|
-
when a.respond_to?(:to_s) then a.to_s
|
330
|
+
if a.respond_to?(:inspect) then a.inspect
|
331
|
+
elsif a.respond_to?(:to_s) then a.to_s
|
328
332
|
else
|
329
333
|
# handle poorly-behaved descendants of BasicObject
|
330
334
|
klass = a.instance_exec { (class << self; self end).superclass }
|
@@ -358,9 +362,9 @@ class Redis
|
|
358
362
|
Errno::ENETUNREACH,
|
359
363
|
Errno::ENOENT,
|
360
364
|
Errno::ETIMEDOUT,
|
361
|
-
Errno::EINVAL
|
365
|
+
Errno::EINVAL => error
|
362
366
|
|
363
|
-
raise CannotConnectError, "Error connecting to Redis on #{location} (#{
|
367
|
+
raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
|
364
368
|
end
|
365
369
|
|
366
370
|
def ensure_connected
|
@@ -374,9 +378,9 @@ class Redis
|
|
374
378
|
if connected?
|
375
379
|
unless inherit_socket? || Process.pid == @pid
|
376
380
|
raise InheritedError,
|
377
|
-
|
378
|
-
|
379
|
-
|
381
|
+
"Tried to use a connection from a child process without reconnecting. " \
|
382
|
+
"You need to reconnect to Redis after forking " \
|
383
|
+
"or set :inherit_socket to true."
|
380
384
|
end
|
381
385
|
else
|
382
386
|
connect
|
@@ -387,7 +391,7 @@ class Redis
|
|
387
391
|
disconnect
|
388
392
|
|
389
393
|
if attempts <= @options[:reconnect_attempts] && @reconnect
|
390
|
-
sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
|
394
|
+
sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
|
391
395
|
@options[:reconnect_delay_max]].min
|
392
396
|
|
393
397
|
Kernel.sleep(sleep_t)
|
@@ -409,16 +413,14 @@ class Redis
|
|
409
413
|
|
410
414
|
defaults.keys.each do |key|
|
411
415
|
# Fill in defaults if needed
|
412
|
-
if defaults[key].respond_to?(:call)
|
413
|
-
defaults[key] = defaults[key].call
|
414
|
-
end
|
416
|
+
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
415
417
|
|
416
418
|
# Symbolize only keys that are needed
|
417
|
-
options[key] = options[key.to_s] if options.
|
419
|
+
options[key] = options[key.to_s] if options.key?(key.to_s)
|
418
420
|
end
|
419
421
|
|
420
422
|
url = options[:url]
|
421
|
-
url = defaults[:url] if url
|
423
|
+
url = defaults[:url] if url.nil?
|
422
424
|
|
423
425
|
# Override defaults from URL if given
|
424
426
|
if url
|
@@ -427,7 +429,7 @@ class Redis
|
|
427
429
|
uri = URI(url)
|
428
430
|
|
429
431
|
if uri.scheme == "unix"
|
430
|
-
defaults[:path]
|
432
|
+
defaults[:path] = uri.path
|
431
433
|
elsif uri.scheme == "redis" || uri.scheme == "rediss"
|
432
434
|
defaults[:scheme] = uri.scheme
|
433
435
|
defaults[:host] = uri.host if uri.host
|
@@ -458,7 +460,7 @@ class Redis
|
|
458
460
|
options[:port] = options[:port].to_i
|
459
461
|
end
|
460
462
|
|
461
|
-
if options.
|
463
|
+
if options.key?(:timeout)
|
462
464
|
options[:connect_timeout] ||= options[:timeout]
|
463
465
|
options[:read_timeout] ||= options[:timeout]
|
464
466
|
options[:write_timeout] ||= options[:timeout]
|
@@ -477,7 +479,7 @@ class Redis
|
|
477
479
|
|
478
480
|
case options[:tcp_keepalive]
|
479
481
|
when Hash
|
480
|
-
[
|
482
|
+
%i[time intvl probes].each do |key|
|
481
483
|
unless options[:tcp_keepalive][key].is_a?(Integer)
|
482
484
|
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
483
485
|
end
|
@@ -485,13 +487,13 @@ class Redis
|
|
485
487
|
|
486
488
|
when Integer
|
487
489
|
if options[:tcp_keepalive] >= 60
|
488
|
-
options[:tcp_keepalive] = {:
|
490
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
489
491
|
|
490
492
|
elsif options[:tcp_keepalive] >= 30
|
491
|
-
options[:tcp_keepalive] = {:
|
493
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
492
494
|
|
493
495
|
elsif options[:tcp_keepalive] >= 5
|
494
|
-
options[:tcp_keepalive] = {:
|
496
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
495
497
|
end
|
496
498
|
end
|
497
499
|
|
@@ -503,14 +505,14 @@ class Redis
|
|
503
505
|
def _parse_driver(driver)
|
504
506
|
driver = driver.to_s if driver.is_a?(Symbol)
|
505
507
|
|
506
|
-
if driver.
|
508
|
+
if driver.is_a?(String)
|
507
509
|
begin
|
508
510
|
require_relative "connection/#{driver}"
|
509
|
-
rescue LoadError, NameError
|
511
|
+
rescue LoadError, NameError
|
510
512
|
begin
|
511
513
|
require "connection/#{driver}"
|
512
|
-
rescue LoadError, NameError =>
|
513
|
-
raise
|
514
|
+
rescue LoadError, NameError => error
|
515
|
+
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
514
516
|
end
|
515
517
|
end
|
516
518
|
|
@@ -529,8 +531,7 @@ class Redis
|
|
529
531
|
@options
|
530
532
|
end
|
531
533
|
|
532
|
-
def check(client)
|
533
|
-
end
|
534
|
+
def check(client); end
|
534
535
|
|
535
536
|
class Sentinel < Connector
|
536
537
|
def initialize(options)
|
@@ -539,7 +540,7 @@ class Redis
|
|
539
540
|
@options[:db] = DEFAULTS.fetch(:db)
|
540
541
|
|
541
542
|
@sentinels = @options.delete(:sentinels).dup
|
542
|
-
@role = @options
|
543
|
+
@role = (@options[:role] || "master").to_s
|
543
544
|
@master = @options[:host]
|
544
545
|
end
|
545
546
|
|
@@ -562,13 +563,13 @@ class Redis
|
|
562
563
|
|
563
564
|
def resolve
|
564
565
|
result = case @role
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
566
|
+
when "master"
|
567
|
+
resolve_master
|
568
|
+
when "slave"
|
569
|
+
resolve_slave
|
570
|
+
else
|
571
|
+
raise ArgumentError, "Unknown instance role #{@role}"
|
572
|
+
end
|
572
573
|
|
573
574
|
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
574
575
|
end
|
@@ -576,11 +577,11 @@ class Redis
|
|
576
577
|
def sentinel_detect
|
577
578
|
@sentinels.each do |sentinel|
|
578
579
|
client = Client.new(@options.merge({
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
580
|
+
host: sentinel[:host] || sentinel["host"],
|
581
|
+
port: sentinel[:port] || sentinel["port"],
|
582
|
+
password: sentinel[:password] || sentinel["password"],
|
583
|
+
reconnect_attempts: 0
|
584
|
+
}))
|
584
585
|
|
585
586
|
begin
|
586
587
|
if result = yield(client)
|
@@ -602,7 +603,7 @@ class Redis
|
|
602
603
|
def resolve_master
|
603
604
|
sentinel_detect do |client|
|
604
605
|
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
605
|
-
{:
|
606
|
+
{ host: reply[0], port: reply[1] }
|
606
607
|
end
|
607
608
|
end
|
608
609
|
end
|
@@ -620,7 +621,7 @@ class Redis
|
|
620
621
|
slave = slaves.sample
|
621
622
|
{
|
622
623
|
host: slave.fetch('ip'),
|
623
|
-
port: slave.fetch('port')
|
624
|
+
port: slave.fetch('port')
|
624
625
|
}
|
625
626
|
end
|
626
627
|
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
|