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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +44 -0
- data/README.md +63 -5
- data/lib/redis.rb +473 -459
- data/lib/redis/client.rb +95 -78
- 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 +69 -59
- data/lib/redis/connection/synchrony.rb +9 -4
- data/lib/redis/distributed.rb +81 -55
- data/lib/redis/errors.rb +2 -0
- data/lib/redis/hash_ring.rb +15 -14
- data/lib/redis/pipeline.rb +46 -8
- data/lib/redis/subscribe.rb +11 -12
- data/lib/redis/version.rb +3 -1
- metadata +6 -21
data/lib/redis/client.rb
CHANGED
@@ -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
|
-
:
|
10
|
-
:
|
11
|
-
:
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:
|
15
|
-
:
|
16
|
-
:
|
17
|
-
:
|
18
|
-
:
|
19
|
-
:
|
20
|
-
:
|
21
|
-
:
|
22
|
-
:
|
23
|
-
:
|
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.
|
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
|
-
|
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(
|
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(
|
177
|
-
return [] if
|
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
|
-
|
195
|
-
reply =
|
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
|
-
!!
|
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
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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
|
-
|
317
|
+
protected
|
310
318
|
|
311
319
|
def logging(commands)
|
312
|
-
return yield unless @logger
|
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
|
-
|
318
|
-
|
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} (#{
|
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
|
-
|
368
|
-
|
369
|
-
|
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.
|
414
|
+
options[key] = options[key.to_s] if options.key?(key.to_s)
|
408
415
|
end
|
409
416
|
|
410
|
-
url = options[: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]
|
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.
|
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
|
-
[
|
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] = {:
|
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] = {:
|
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] = {:
|
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.
|
503
|
+
if driver.is_a?(String)
|
496
504
|
begin
|
497
505
|
require_relative "connection/#{driver}"
|
498
|
-
rescue LoadError, NameError
|
506
|
+
rescue LoadError, NameError
|
499
507
|
begin
|
500
508
|
require "connection/#{driver}"
|
501
|
-
rescue LoadError, NameError =>
|
502
|
-
raise
|
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
|
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
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
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
|
-
|
570
|
-
|
571
|
-
|
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
|
-
{:
|
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
|
-
|
603
|
-
|
604
|
-
{
|
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
|
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)
|