redis 4.1.2 → 4.2.2
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 +39 -1
- data/README.md +14 -5
- data/lib/redis.rb +385 -343
- data/lib/redis/client.rb +66 -69
- data/lib/redis/cluster.rb +13 -4
- data/lib/redis/cluster/node.rb +3 -0
- data/lib/redis/cluster/node_key.rb +3 -7
- data/lib/redis/cluster/option.rb +27 -14
- data/lib/redis/cluster/slot.rb +30 -13
- data/lib/redis/cluster/slot_loader.rb +4 -4
- data/lib/redis/connection.rb +2 -0
- data/lib/redis/connection/command_helper.rb +3 -2
- data/lib/redis/connection/hiredis.rb +4 -3
- data/lib/redis/connection/registry.rb +2 -1
- data/lib/redis/connection/ruby.rb +47 -58
- data/lib/redis/connection/synchrony.rb +9 -4
- data/lib/redis/distributed.rb +117 -62
- data/lib/redis/errors.rb +2 -0
- data/lib/redis/hash_ring.rb +15 -14
- data/lib/redis/pipeline.rb +16 -3
- data/lib/redis/subscribe.rb +11 -12
- data/lib/redis/version.rb +3 -1
- metadata +14 -9
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,12 +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
|
255
|
+
alias close disconnect
|
253
256
|
|
254
257
|
def reconnect
|
255
258
|
disconnect
|
@@ -300,30 +303,27 @@ class Redis
|
|
300
303
|
with_socket_timeout(0, &blk)
|
301
304
|
end
|
302
305
|
|
303
|
-
def with_reconnect(val=true)
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
@reconnect = original
|
309
|
-
end
|
306
|
+
def with_reconnect(val = true)
|
307
|
+
original, @reconnect = @reconnect, val
|
308
|
+
yield
|
309
|
+
ensure
|
310
|
+
@reconnect = original
|
310
311
|
end
|
311
312
|
|
312
313
|
def without_reconnect(&blk)
|
313
314
|
with_reconnect(false, &blk)
|
314
315
|
end
|
315
316
|
|
316
|
-
|
317
|
+
protected
|
317
318
|
|
318
319
|
def logging(commands)
|
319
|
-
return yield unless @logger
|
320
|
+
return yield unless @logger&.debug?
|
320
321
|
|
321
322
|
begin
|
322
323
|
commands.each do |name, *args|
|
323
324
|
logged_args = args.map do |a|
|
324
|
-
|
325
|
-
|
326
|
-
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
|
327
327
|
else
|
328
328
|
# handle poorly-behaved descendants of BasicObject
|
329
329
|
klass = a.instance_exec { (class << self; self end).superclass }
|
@@ -357,9 +357,9 @@ class Redis
|
|
357
357
|
Errno::ENETUNREACH,
|
358
358
|
Errno::ENOENT,
|
359
359
|
Errno::ETIMEDOUT,
|
360
|
-
Errno::EINVAL
|
360
|
+
Errno::EINVAL => error
|
361
361
|
|
362
|
-
raise CannotConnectError, "Error connecting to Redis on #{location} (#{
|
362
|
+
raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
|
363
363
|
end
|
364
364
|
|
365
365
|
def ensure_connected
|
@@ -373,9 +373,9 @@ class Redis
|
|
373
373
|
if connected?
|
374
374
|
unless inherit_socket? || Process.pid == @pid
|
375
375
|
raise InheritedError,
|
376
|
-
|
377
|
-
|
378
|
-
|
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."
|
379
379
|
end
|
380
380
|
else
|
381
381
|
connect
|
@@ -386,7 +386,7 @@ class Redis
|
|
386
386
|
disconnect
|
387
387
|
|
388
388
|
if attempts <= @options[:reconnect_attempts] && @reconnect
|
389
|
-
sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
|
389
|
+
sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
|
390
390
|
@options[:reconnect_delay_max]].min
|
391
391
|
|
392
392
|
Kernel.sleep(sleep_t)
|
@@ -408,16 +408,14 @@ class Redis
|
|
408
408
|
|
409
409
|
defaults.keys.each do |key|
|
410
410
|
# Fill in defaults if needed
|
411
|
-
if defaults[key].respond_to?(:call)
|
412
|
-
defaults[key] = defaults[key].call
|
413
|
-
end
|
411
|
+
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
414
412
|
|
415
413
|
# Symbolize only keys that are needed
|
416
|
-
options[key] = options[key.to_s] if options.
|
414
|
+
options[key] = options[key.to_s] if options.key?(key.to_s)
|
417
415
|
end
|
418
416
|
|
419
417
|
url = options[:url]
|
420
|
-
url = defaults[:url] if url
|
418
|
+
url = defaults[:url] if url.nil?
|
421
419
|
|
422
420
|
# Override defaults from URL if given
|
423
421
|
if url
|
@@ -426,7 +424,7 @@ class Redis
|
|
426
424
|
uri = URI(url)
|
427
425
|
|
428
426
|
if uri.scheme == "unix"
|
429
|
-
defaults[:path]
|
427
|
+
defaults[:path] = uri.path
|
430
428
|
elsif uri.scheme == "redis" || uri.scheme == "rediss"
|
431
429
|
defaults[:scheme] = uri.scheme
|
432
430
|
defaults[:host] = uri.host if uri.host
|
@@ -457,7 +455,7 @@ class Redis
|
|
457
455
|
options[:port] = options[:port].to_i
|
458
456
|
end
|
459
457
|
|
460
|
-
if options.
|
458
|
+
if options.key?(:timeout)
|
461
459
|
options[:connect_timeout] ||= options[:timeout]
|
462
460
|
options[:read_timeout] ||= options[:timeout]
|
463
461
|
options[:write_timeout] ||= options[:timeout]
|
@@ -476,7 +474,7 @@ class Redis
|
|
476
474
|
|
477
475
|
case options[:tcp_keepalive]
|
478
476
|
when Hash
|
479
|
-
[
|
477
|
+
%i[time intvl probes].each do |key|
|
480
478
|
unless options[:tcp_keepalive][key].is_a?(Integer)
|
481
479
|
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
482
480
|
end
|
@@ -484,13 +482,13 @@ class Redis
|
|
484
482
|
|
485
483
|
when Integer
|
486
484
|
if options[:tcp_keepalive] >= 60
|
487
|
-
options[:tcp_keepalive] = {:
|
485
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
488
486
|
|
489
487
|
elsif options[:tcp_keepalive] >= 30
|
490
|
-
options[:tcp_keepalive] = {:
|
488
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
491
489
|
|
492
490
|
elsif options[:tcp_keepalive] >= 5
|
493
|
-
options[:tcp_keepalive] = {:
|
491
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
494
492
|
end
|
495
493
|
end
|
496
494
|
|
@@ -502,14 +500,14 @@ class Redis
|
|
502
500
|
def _parse_driver(driver)
|
503
501
|
driver = driver.to_s if driver.is_a?(Symbol)
|
504
502
|
|
505
|
-
if driver.
|
503
|
+
if driver.is_a?(String)
|
506
504
|
begin
|
507
505
|
require_relative "connection/#{driver}"
|
508
|
-
rescue LoadError, NameError
|
506
|
+
rescue LoadError, NameError
|
509
507
|
begin
|
510
508
|
require "connection/#{driver}"
|
511
|
-
rescue LoadError, NameError =>
|
512
|
-
raise
|
509
|
+
rescue LoadError, NameError => error
|
510
|
+
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
513
511
|
end
|
514
512
|
end
|
515
513
|
|
@@ -528,8 +526,7 @@ class Redis
|
|
528
526
|
@options
|
529
527
|
end
|
530
528
|
|
531
|
-
def check(client)
|
532
|
-
end
|
529
|
+
def check(client); end
|
533
530
|
|
534
531
|
class Sentinel < Connector
|
535
532
|
def initialize(options)
|
@@ -538,7 +535,7 @@ class Redis
|
|
538
535
|
@options[:db] = DEFAULTS.fetch(:db)
|
539
536
|
|
540
537
|
@sentinels = @options.delete(:sentinels).dup
|
541
|
-
@role = @options
|
538
|
+
@role = (@options[:role] || "master").to_s
|
542
539
|
@master = @options[:host]
|
543
540
|
end
|
544
541
|
|
@@ -561,13 +558,13 @@ class Redis
|
|
561
558
|
|
562
559
|
def resolve
|
563
560
|
result = case @role
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
561
|
+
when "master"
|
562
|
+
resolve_master
|
563
|
+
when "slave"
|
564
|
+
resolve_slave
|
565
|
+
else
|
566
|
+
raise ArgumentError, "Unknown instance role #{@role}"
|
567
|
+
end
|
571
568
|
|
572
569
|
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
573
570
|
end
|
@@ -575,11 +572,11 @@ class Redis
|
|
575
572
|
def sentinel_detect
|
576
573
|
@sentinels.each do |sentinel|
|
577
574
|
client = Client.new(@options.merge({
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
575
|
+
host: sentinel[:host] || sentinel["host"],
|
576
|
+
port: sentinel[:port] || sentinel["port"],
|
577
|
+
password: sentinel[:password] || sentinel["password"],
|
578
|
+
reconnect_attempts: 0
|
579
|
+
}))
|
583
580
|
|
584
581
|
begin
|
585
582
|
if result = yield(client)
|
@@ -601,7 +598,7 @@ class Redis
|
|
601
598
|
def resolve_master
|
602
599
|
sentinel_detect do |client|
|
603
600
|
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
604
|
-
{:
|
601
|
+
{ host: reply[0], port: reply[1] }
|
605
602
|
end
|
606
603
|
end
|
607
604
|
end
|
@@ -619,7 +616,7 @@ class Redis
|
|
619
616
|
slave = slaves.sample
|
620
617
|
{
|
621
618
|
host: slave.fetch('ip'),
|
622
|
-
port: slave.fetch('port')
|
619
|
+
port: slave.fetch('port')
|
623
620
|
}
|
624
621
|
end
|
625
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
|
@@ -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
|
-
|
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
|
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
|
-
|
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)
|
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
|
@@ -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
|
16
|
-
|
17
|
-
|
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)
|
data/lib/redis/cluster/option.rb
CHANGED
@@ -15,36 +15,35 @@ class Redis
|
|
15
15
|
def initialize(options)
|
16
16
|
options = options.dup
|
17
17
|
node_addrs = options.delete(:cluster)
|
18
|
-
@
|
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, :password)
|
20
22
|
@options = options
|
21
23
|
end
|
22
24
|
|
23
25
|
def per_node_key
|
24
|
-
@
|
26
|
+
@node_opts.map { |opt| [NodeKey.build_from_host_port(opt[:host], opt[:port]), @options.merge(opt)] }
|
25
27
|
.to_h
|
26
28
|
end
|
27
29
|
|
28
|
-
def secure?
|
29
|
-
@node_uris.any? { |uri| uri.scheme == SECURE_SCHEME } || @options[:ssl_params] || false
|
30
|
-
end
|
31
|
-
|
32
30
|
def use_replica?
|
33
31
|
@replica
|
34
32
|
end
|
35
33
|
|
36
34
|
def update_node(addrs)
|
37
|
-
@
|
35
|
+
@node_opts = build_node_options(addrs)
|
38
36
|
end
|
39
37
|
|
40
38
|
def add_node(host, port)
|
41
|
-
@
|
39
|
+
@node_opts << { host: host, port: port }
|
42
40
|
end
|
43
41
|
|
44
42
|
private
|
45
43
|
|
46
|
-
def
|
44
|
+
def build_node_options(addrs)
|
47
45
|
raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
|
46
|
+
|
48
47
|
addrs.map { |addr| parse_node_addr(addr) }
|
49
48
|
end
|
50
49
|
|
@@ -53,7 +52,7 @@ class Redis
|
|
53
52
|
when String
|
54
53
|
parse_node_url(addr)
|
55
54
|
when Hash
|
56
|
-
|
55
|
+
parse_node_option(addr)
|
57
56
|
else
|
58
57
|
raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash'
|
59
58
|
end
|
@@ -62,15 +61,29 @@ class Redis
|
|
62
61
|
def parse_node_url(addr)
|
63
62
|
uri = URI(addr)
|
64
63
|
raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
|
65
|
-
|
64
|
+
|
65
|
+
db = uri.path.split('/')[1]&.to_i
|
66
|
+
{ scheme: uri.scheme, password: uri.password, host: uri.host, port: uri.port, db: db }.reject { |_, v| v.nil? }
|
66
67
|
rescue URI::InvalidURIError => err
|
67
68
|
raise InvalidClientOptionError, err.message
|
68
69
|
end
|
69
70
|
|
70
|
-
def
|
71
|
+
def parse_node_option(addr)
|
71
72
|
addr = addr.map { |k, v| [k.to_sym, v] }.to_h
|
72
|
-
|
73
|
-
|
73
|
+
if addr.values_at(:host, :port).any?(&:nil?)
|
74
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
|
75
|
+
end
|
76
|
+
|
77
|
+
addr
|
78
|
+
end
|
79
|
+
|
80
|
+
# Redis cluster node returns only host and port information.
|
81
|
+
# So we should complement additional information such as:
|
82
|
+
# scheme, password and so on.
|
83
|
+
def add_common_node_option_if_needed(options, node_opts, key)
|
84
|
+
return options if options[key].nil? && node_opts.first[key].nil?
|
85
|
+
|
86
|
+
options[key] ||= node_opts.first[key]
|
74
87
|
end
|
75
88
|
end
|
76
89
|
end
|