redis 4.1.4 → 4.3.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 +48 -0
- data/README.md +27 -17
- data/lib/redis.rb +317 -260
- data/lib/redis/client.rb +92 -74
- data/lib/redis/cluster.rb +5 -1
- data/lib/redis/cluster/node.rb +3 -0
- data/lib/redis/cluster/option.rb +9 -3
- 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 +89 -107
- data/lib/redis/connection/synchrony.rb +8 -4
- data/lib/redis/distributed.rb +114 -63
- 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,31 @@ 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
|
+
username: nil,
|
21
|
+
password: nil,
|
22
|
+
db: 0,
|
23
|
+
driver: nil,
|
24
|
+
id: nil,
|
25
|
+
tcp_keepalive: 0,
|
26
|
+
reconnect_attempts: 1,
|
27
|
+
reconnect_delay: 0,
|
28
|
+
reconnect_delay_max: 0.5,
|
29
|
+
inherit_socket: false,
|
30
|
+
logger: nil,
|
31
|
+
sentinels: nil,
|
32
|
+
role: nil
|
33
|
+
}.freeze
|
27
34
|
|
28
35
|
attr_reader :options
|
29
36
|
|
@@ -55,6 +62,10 @@ class Redis
|
|
55
62
|
@options[:read_timeout]
|
56
63
|
end
|
57
64
|
|
65
|
+
def username
|
66
|
+
@options[:username]
|
67
|
+
end
|
68
|
+
|
58
69
|
def password
|
59
70
|
@options[:password]
|
60
71
|
end
|
@@ -89,7 +100,7 @@ class Redis
|
|
89
100
|
@pending_reads = 0
|
90
101
|
|
91
102
|
@connector =
|
92
|
-
if options.
|
103
|
+
if !@options[:sentinels].nil?
|
93
104
|
Connector::Sentinel.new(@options)
|
94
105
|
elsif options.include?(:connector) && options[:connector].respond_to?(:new)
|
95
106
|
options.delete(:connector).new(@options)
|
@@ -104,7 +115,17 @@ class Redis
|
|
104
115
|
# Don't try to reconnect when the connection is fresh
|
105
116
|
with_reconnect(false) do
|
106
117
|
establish_connection
|
107
|
-
|
118
|
+
if password
|
119
|
+
if username
|
120
|
+
begin
|
121
|
+
call [:auth, username, password]
|
122
|
+
rescue CommandError # Likely on Redis < 6
|
123
|
+
call [:auth, password]
|
124
|
+
end
|
125
|
+
else
|
126
|
+
call [:auth, password]
|
127
|
+
end
|
128
|
+
end
|
108
129
|
call [:select, db] if db != 0
|
109
130
|
call [:client, :setname, @options[:id]] if @options[:id]
|
110
131
|
@connector.check(self)
|
@@ -125,7 +146,7 @@ class Redis
|
|
125
146
|
reply = process([command]) { read }
|
126
147
|
raise reply if reply.is_a?(CommandError)
|
127
148
|
|
128
|
-
if block_given?
|
149
|
+
if block_given? && reply != 'QUEUED'
|
129
150
|
yield reply
|
130
151
|
else
|
131
152
|
reply
|
@@ -166,6 +187,7 @@ class Redis
|
|
166
187
|
end
|
167
188
|
rescue ConnectionError => e
|
168
189
|
return nil if pipeline.shutdown?
|
190
|
+
|
169
191
|
# Assume the pipeline was sent in one piece, but execution of
|
170
192
|
# SHUTDOWN caused none of the replies for commands that were executed
|
171
193
|
# prior to it from coming back around.
|
@@ -244,13 +266,13 @@ class Redis
|
|
244
266
|
end
|
245
267
|
|
246
268
|
def connected?
|
247
|
-
!!
|
269
|
+
!!(connection && connection.connected?)
|
248
270
|
end
|
249
271
|
|
250
272
|
def disconnect
|
251
273
|
connection.disconnect if connected?
|
252
274
|
end
|
253
|
-
|
275
|
+
alias close disconnect
|
254
276
|
|
255
277
|
def reconnect
|
256
278
|
disconnect
|
@@ -301,30 +323,27 @@ class Redis
|
|
301
323
|
with_socket_timeout(0, &blk)
|
302
324
|
end
|
303
325
|
|
304
|
-
def with_reconnect(val=true)
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
@reconnect = original
|
310
|
-
end
|
326
|
+
def with_reconnect(val = true)
|
327
|
+
original, @reconnect = @reconnect, val
|
328
|
+
yield
|
329
|
+
ensure
|
330
|
+
@reconnect = original
|
311
331
|
end
|
312
332
|
|
313
333
|
def without_reconnect(&blk)
|
314
334
|
with_reconnect(false, &blk)
|
315
335
|
end
|
316
336
|
|
317
|
-
|
337
|
+
protected
|
318
338
|
|
319
339
|
def logging(commands)
|
320
|
-
return yield unless @logger
|
340
|
+
return yield unless @logger&.debug?
|
321
341
|
|
322
342
|
begin
|
323
343
|
commands.each do |name, *args|
|
324
344
|
logged_args = args.map do |a|
|
325
|
-
|
326
|
-
|
327
|
-
when a.respond_to?(:to_s) then a.to_s
|
345
|
+
if a.respond_to?(:inspect) then a.inspect
|
346
|
+
elsif a.respond_to?(:to_s) then a.to_s
|
328
347
|
else
|
329
348
|
# handle poorly-behaved descendants of BasicObject
|
330
349
|
klass = a.instance_exec { (class << self; self end).superclass }
|
@@ -358,9 +377,9 @@ class Redis
|
|
358
377
|
Errno::ENETUNREACH,
|
359
378
|
Errno::ENOENT,
|
360
379
|
Errno::ETIMEDOUT,
|
361
|
-
Errno::EINVAL
|
380
|
+
Errno::EINVAL => error
|
362
381
|
|
363
|
-
raise CannotConnectError, "Error connecting to Redis on #{location} (#{
|
382
|
+
raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
|
364
383
|
end
|
365
384
|
|
366
385
|
def ensure_connected
|
@@ -374,9 +393,9 @@ class Redis
|
|
374
393
|
if connected?
|
375
394
|
unless inherit_socket? || Process.pid == @pid
|
376
395
|
raise InheritedError,
|
377
|
-
|
378
|
-
|
379
|
-
|
396
|
+
"Tried to use a connection from a child process without reconnecting. " \
|
397
|
+
"You need to reconnect to Redis after forking " \
|
398
|
+
"or set :inherit_socket to true."
|
380
399
|
end
|
381
400
|
else
|
382
401
|
connect
|
@@ -387,7 +406,7 @@ class Redis
|
|
387
406
|
disconnect
|
388
407
|
|
389
408
|
if attempts <= @options[:reconnect_attempts] && @reconnect
|
390
|
-
sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
|
409
|
+
sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
|
391
410
|
@options[:reconnect_delay_max]].min
|
392
411
|
|
393
412
|
Kernel.sleep(sleep_t)
|
@@ -409,16 +428,14 @@ class Redis
|
|
409
428
|
|
410
429
|
defaults.keys.each do |key|
|
411
430
|
# Fill in defaults if needed
|
412
|
-
if defaults[key].respond_to?(:call)
|
413
|
-
defaults[key] = defaults[key].call
|
414
|
-
end
|
431
|
+
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
415
432
|
|
416
433
|
# Symbolize only keys that are needed
|
417
|
-
options[key] = options[key.to_s] if options.
|
434
|
+
options[key] = options[key.to_s] if options.key?(key.to_s)
|
418
435
|
end
|
419
436
|
|
420
437
|
url = options[:url]
|
421
|
-
url = defaults[:url] if url
|
438
|
+
url = defaults[:url] if url.nil?
|
422
439
|
|
423
440
|
# Override defaults from URL if given
|
424
441
|
if url
|
@@ -427,12 +444,13 @@ class Redis
|
|
427
444
|
uri = URI(url)
|
428
445
|
|
429
446
|
if uri.scheme == "unix"
|
430
|
-
defaults[:path]
|
447
|
+
defaults[:path] = uri.path
|
431
448
|
elsif uri.scheme == "redis" || uri.scheme == "rediss"
|
432
449
|
defaults[:scheme] = uri.scheme
|
433
450
|
defaults[:host] = uri.host if uri.host
|
434
451
|
defaults[:port] = uri.port if uri.port
|
435
|
-
defaults[:
|
452
|
+
defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
|
453
|
+
defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
|
436
454
|
defaults[:db] = uri.path[1..-1].to_i if uri.path
|
437
455
|
defaults[:role] = :master
|
438
456
|
else
|
@@ -458,7 +476,7 @@ class Redis
|
|
458
476
|
options[:port] = options[:port].to_i
|
459
477
|
end
|
460
478
|
|
461
|
-
if options.
|
479
|
+
if options.key?(:timeout)
|
462
480
|
options[:connect_timeout] ||= options[:timeout]
|
463
481
|
options[:read_timeout] ||= options[:timeout]
|
464
482
|
options[:write_timeout] ||= options[:timeout]
|
@@ -477,7 +495,7 @@ class Redis
|
|
477
495
|
|
478
496
|
case options[:tcp_keepalive]
|
479
497
|
when Hash
|
480
|
-
[
|
498
|
+
%i[time intvl probes].each do |key|
|
481
499
|
unless options[:tcp_keepalive][key].is_a?(Integer)
|
482
500
|
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
483
501
|
end
|
@@ -485,13 +503,13 @@ class Redis
|
|
485
503
|
|
486
504
|
when Integer
|
487
505
|
if options[:tcp_keepalive] >= 60
|
488
|
-
options[:tcp_keepalive] = {:
|
506
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
489
507
|
|
490
508
|
elsif options[:tcp_keepalive] >= 30
|
491
|
-
options[:tcp_keepalive] = {:
|
509
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
492
510
|
|
493
511
|
elsif options[:tcp_keepalive] >= 5
|
494
|
-
options[:tcp_keepalive] = {:
|
512
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
495
513
|
end
|
496
514
|
end
|
497
515
|
|
@@ -503,14 +521,14 @@ class Redis
|
|
503
521
|
def _parse_driver(driver)
|
504
522
|
driver = driver.to_s if driver.is_a?(Symbol)
|
505
523
|
|
506
|
-
if driver.
|
524
|
+
if driver.is_a?(String)
|
507
525
|
begin
|
508
526
|
require_relative "connection/#{driver}"
|
509
|
-
rescue LoadError, NameError
|
527
|
+
rescue LoadError, NameError
|
510
528
|
begin
|
511
|
-
require "connection/#{driver}"
|
512
|
-
rescue LoadError, NameError =>
|
513
|
-
raise
|
529
|
+
require "redis/connection/#{driver}"
|
530
|
+
rescue LoadError, NameError => error
|
531
|
+
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
514
532
|
end
|
515
533
|
end
|
516
534
|
|
@@ -529,8 +547,7 @@ class Redis
|
|
529
547
|
@options
|
530
548
|
end
|
531
549
|
|
532
|
-
def check(client)
|
533
|
-
end
|
550
|
+
def check(client); end
|
534
551
|
|
535
552
|
class Sentinel < Connector
|
536
553
|
def initialize(options)
|
@@ -539,7 +556,7 @@ class Redis
|
|
539
556
|
@options[:db] = DEFAULTS.fetch(:db)
|
540
557
|
|
541
558
|
@sentinels = @options.delete(:sentinels).dup
|
542
|
-
@role = @options
|
559
|
+
@role = (@options[:role] || "master").to_s
|
543
560
|
@master = @options[:host]
|
544
561
|
end
|
545
562
|
|
@@ -562,13 +579,13 @@ class Redis
|
|
562
579
|
|
563
580
|
def resolve
|
564
581
|
result = case @role
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
582
|
+
when "master"
|
583
|
+
resolve_master
|
584
|
+
when "slave"
|
585
|
+
resolve_slave
|
586
|
+
else
|
587
|
+
raise ArgumentError, "Unknown instance role #{@role}"
|
588
|
+
end
|
572
589
|
|
573
590
|
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
574
591
|
end
|
@@ -576,11 +593,12 @@ class Redis
|
|
576
593
|
def sentinel_detect
|
577
594
|
@sentinels.each do |sentinel|
|
578
595
|
client = Client.new(@options.merge({
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
596
|
+
host: sentinel[:host] || sentinel["host"],
|
597
|
+
port: sentinel[:port] || sentinel["port"],
|
598
|
+
username: sentinel[:username] || sentinel["username"],
|
599
|
+
password: sentinel[:password] || sentinel["password"],
|
600
|
+
reconnect_attempts: 0
|
601
|
+
}))
|
584
602
|
|
585
603
|
begin
|
586
604
|
if result = yield(client)
|
@@ -602,7 +620,7 @@ class Redis
|
|
602
620
|
def resolve_master
|
603
621
|
sentinel_detect do |client|
|
604
622
|
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
605
|
-
{:
|
623
|
+
{ host: reply[0], port: reply[1] }
|
606
624
|
end
|
607
625
|
end
|
608
626
|
end
|
@@ -620,7 +638,7 @@ class Redis
|
|
620
638
|
slave = slaves.sample
|
621
639
|
{
|
622
640
|
host: slave.fetch('ip'),
|
623
|
-
port: slave.fetch('port')
|
641
|
+
port: slave.fetch('port')
|
624
642
|
}
|
625
643
|
end
|
626
644
|
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
|
@@ -127,7 +128,7 @@ class Redis
|
|
127
128
|
def send_command(command, &block)
|
128
129
|
cmd = command.first.to_s.downcase
|
129
130
|
case cmd
|
130
|
-
when 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
|
131
|
+
when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
|
131
132
|
@node.call_all(command, &block).first
|
132
133
|
when 'flushall', 'flushdb'
|
133
134
|
@node.call_master(command, &block).first
|
@@ -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
@@ -18,6 +18,7 @@ class Redis
|
|
18
18
|
@node_opts = build_node_options(node_addrs)
|
19
19
|
@replica = options.delete(:replica) == true
|
20
20
|
add_common_node_option_if_needed(options, @node_opts, :scheme)
|
21
|
+
add_common_node_option_if_needed(options, @node_opts, :username)
|
21
22
|
add_common_node_option_if_needed(options, @node_opts, :password)
|
22
23
|
@options = options
|
23
24
|
end
|
@@ -43,6 +44,7 @@ class Redis
|
|
43
44
|
|
44
45
|
def build_node_options(addrs)
|
45
46
|
raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
|
47
|
+
|
46
48
|
addrs.map { |addr| parse_node_addr(addr) }
|
47
49
|
end
|
48
50
|
|
@@ -62,21 +64,25 @@ class Redis
|
|
62
64
|
raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
|
63
65
|
|
64
66
|
db = uri.path.split('/')[1]&.to_i
|
65
|
-
|
67
|
+
|
68
|
+
{ scheme: uri.scheme, username: uri.user, password: uri.password, host: uri.host, port: uri.port, db: db }
|
69
|
+
.reject { |_, v| v.nil? || v == '' }
|
66
70
|
rescue URI::InvalidURIError => err
|
67
71
|
raise InvalidClientOptionError, err.message
|
68
72
|
end
|
69
73
|
|
70
74
|
def parse_node_option(addr)
|
71
75
|
addr = addr.map { |k, v| [k.to_sym, v] }.to_h
|
72
|
-
|
76
|
+
if addr.values_at(:host, :port).any?(&:nil?)
|
77
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
|
78
|
+
end
|
73
79
|
|
74
80
|
addr
|
75
81
|
end
|
76
82
|
|
77
83
|
# Redis cluster node returns only host and port information.
|
78
84
|
# So we should complement additional information such as:
|
79
|
-
# scheme, password and so on.
|
85
|
+
# scheme, username, password and so on.
|
80
86
|
def add_common_node_option_if_needed(options, node_opts, key)
|
81
87
|
return options if options[key].nil? && node_opts.first[key].nil?
|
82
88
|
|