redis 4.1.0 → 4.5.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 +5 -5
- data/CHANGELOG.md +107 -0
- data/README.md +81 -17
- data/lib/redis/client.rb +137 -82
- data/lib/redis/cluster/command_loader.rb +6 -7
- data/lib/redis/cluster/node.rb +5 -1
- data/lib/redis/cluster/node_key.rb +3 -7
- data/lib/redis/cluster/option.rb +30 -14
- data/lib/redis/cluster/slot.rb +30 -13
- data/lib/redis/cluster/slot_loader.rb +4 -4
- data/lib/redis/cluster.rb +22 -17
- data/lib/redis/connection/command_helper.rb +5 -2
- data/lib/redis/connection/hiredis.rb +4 -3
- data/lib/redis/connection/registry.rb +2 -1
- data/lib/redis/connection/ruby.rb +121 -105
- data/lib/redis/connection/synchrony.rb +9 -4
- data/lib/redis/connection.rb +2 -0
- data/lib/redis/distributed.rb +170 -68
- 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
- data/lib/redis.rb +808 -476
- metadata +15 -25
data/lib/redis/client.rb
CHANGED
@@ -1,27 +1,36 @@
|
|
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
|
-
|
9
|
+
# Defaults are also used for converting string keys to symbols.
|
8
10
|
DEFAULTS = {
|
9
|
-
:
|
10
|
-
:
|
11
|
-
:
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:
|
15
|
-
:
|
16
|
-
:
|
17
|
-
:
|
18
|
-
:
|
19
|
-
:
|
20
|
-
:
|
21
|
-
:
|
22
|
-
:
|
23
|
-
:
|
24
|
-
|
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
|
25
34
|
|
26
35
|
attr_reader :options
|
27
36
|
|
@@ -53,6 +62,10 @@ class Redis
|
|
53
62
|
@options[:read_timeout]
|
54
63
|
end
|
55
64
|
|
65
|
+
def username
|
66
|
+
@options[:username]
|
67
|
+
end
|
68
|
+
|
56
69
|
def password
|
57
70
|
@options[:password]
|
58
71
|
end
|
@@ -87,7 +100,7 @@ class Redis
|
|
87
100
|
@pending_reads = 0
|
88
101
|
|
89
102
|
@connector =
|
90
|
-
if options.
|
103
|
+
if !@options[:sentinels].nil?
|
91
104
|
Connector::Sentinel.new(@options)
|
92
105
|
elsif options.include?(:connector) && options[:connector].respond_to?(:new)
|
93
106
|
options.delete(:connector).new(@options)
|
@@ -102,7 +115,33 @@ class Redis
|
|
102
115
|
# Don't try to reconnect when the connection is fresh
|
103
116
|
with_reconnect(false) do
|
104
117
|
establish_connection
|
105
|
-
|
118
|
+
if password
|
119
|
+
if username
|
120
|
+
begin
|
121
|
+
call [:auth, username, password]
|
122
|
+
rescue CommandError => err # Likely on Redis < 6
|
123
|
+
if err.message.match?(/ERR wrong number of arguments for \'auth\' command/)
|
124
|
+
call [:auth, password]
|
125
|
+
elsif err.message.match?(/WRONGPASS invalid username-password pair/)
|
126
|
+
begin
|
127
|
+
call [:auth, password]
|
128
|
+
rescue CommandError
|
129
|
+
raise err
|
130
|
+
end
|
131
|
+
::Kernel.warn(
|
132
|
+
"[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
|
133
|
+
" the provided password was for the default user. This will start failing in redis-rb 4.6."
|
134
|
+
)
|
135
|
+
else
|
136
|
+
raise
|
137
|
+
end
|
138
|
+
end
|
139
|
+
else
|
140
|
+
call [:auth, password]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
call [:readonly] if @options[:readonly]
|
106
145
|
call [:select, db] if db != 0
|
107
146
|
call [:client, :setname, @options[:id]] if @options[:id]
|
108
147
|
@connector.check(self)
|
@@ -123,7 +162,7 @@ class Redis
|
|
123
162
|
reply = process([command]) { read }
|
124
163
|
raise reply if reply.is_a?(CommandError)
|
125
164
|
|
126
|
-
if block_given?
|
165
|
+
if block_given? && reply != 'QUEUED'
|
127
166
|
yield reply
|
128
167
|
else
|
129
168
|
reply
|
@@ -155,16 +194,16 @@ class Redis
|
|
155
194
|
end
|
156
195
|
|
157
196
|
def call_pipeline(pipeline)
|
158
|
-
|
159
|
-
return [] if commands.empty?
|
197
|
+
return [] if pipeline.futures.empty?
|
160
198
|
|
161
199
|
with_reconnect pipeline.with_reconnect? do
|
162
200
|
begin
|
163
|
-
pipeline.finish(call_pipelined(
|
201
|
+
pipeline.finish(call_pipelined(pipeline)).tap do
|
164
202
|
self.db = pipeline.db if pipeline.db
|
165
203
|
end
|
166
204
|
rescue ConnectionError => e
|
167
205
|
return nil if pipeline.shutdown?
|
206
|
+
|
168
207
|
# Assume the pipeline was sent in one piece, but execution of
|
169
208
|
# SHUTDOWN caused none of the replies for commands that were executed
|
170
209
|
# prior to it from coming back around.
|
@@ -173,8 +212,8 @@ class Redis
|
|
173
212
|
end
|
174
213
|
end
|
175
214
|
|
176
|
-
def call_pipelined(
|
177
|
-
return [] if
|
215
|
+
def call_pipelined(pipeline)
|
216
|
+
return [] if pipeline.futures.empty?
|
178
217
|
|
179
218
|
# The method #ensure_connected (called from #process) reconnects once on
|
180
219
|
# I/O errors. To make an effort in making sure that commands are not
|
@@ -184,6 +223,8 @@ class Redis
|
|
184
223
|
# already successfully executed commands. To circumvent this, don't retry
|
185
224
|
# after the first reply has been read successfully.
|
186
225
|
|
226
|
+
commands = pipeline.commands
|
227
|
+
|
187
228
|
result = Array.new(commands.size)
|
188
229
|
reconnect = @reconnect
|
189
230
|
|
@@ -191,8 +232,12 @@ class Redis
|
|
191
232
|
exception = nil
|
192
233
|
|
193
234
|
process(commands) do
|
194
|
-
|
195
|
-
reply =
|
235
|
+
pipeline.timeouts.each_with_index do |timeout, i|
|
236
|
+
reply = if timeout
|
237
|
+
with_socket_timeout(timeout) { read }
|
238
|
+
else
|
239
|
+
read
|
240
|
+
end
|
196
241
|
result[i] = reply
|
197
242
|
@reconnect = false
|
198
243
|
exception = reply if exception.nil? && reply.is_a?(CommandError)
|
@@ -237,12 +282,13 @@ class Redis
|
|
237
282
|
end
|
238
283
|
|
239
284
|
def connected?
|
240
|
-
!!
|
285
|
+
!!(connection && connection.connected?)
|
241
286
|
end
|
242
287
|
|
243
288
|
def disconnect
|
244
289
|
connection.disconnect if connected?
|
245
290
|
end
|
291
|
+
alias close disconnect
|
246
292
|
|
247
293
|
def reconnect
|
248
294
|
disconnect
|
@@ -293,30 +339,27 @@ class Redis
|
|
293
339
|
with_socket_timeout(0, &blk)
|
294
340
|
end
|
295
341
|
|
296
|
-
def with_reconnect(val=true)
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
@reconnect = original
|
302
|
-
end
|
342
|
+
def with_reconnect(val = true)
|
343
|
+
original, @reconnect = @reconnect, val
|
344
|
+
yield
|
345
|
+
ensure
|
346
|
+
@reconnect = original
|
303
347
|
end
|
304
348
|
|
305
349
|
def without_reconnect(&blk)
|
306
350
|
with_reconnect(false, &blk)
|
307
351
|
end
|
308
352
|
|
309
|
-
|
353
|
+
protected
|
310
354
|
|
311
355
|
def logging(commands)
|
312
|
-
return yield unless @logger
|
356
|
+
return yield unless @logger&.debug?
|
313
357
|
|
314
358
|
begin
|
315
359
|
commands.each do |name, *args|
|
316
360
|
logged_args = args.map do |a|
|
317
|
-
|
318
|
-
|
319
|
-
when a.respond_to?(:to_s) then a.to_s
|
361
|
+
if a.respond_to?(:inspect) then a.inspect
|
362
|
+
elsif a.respond_to?(:to_s) then a.to_s
|
320
363
|
else
|
321
364
|
# handle poorly-behaved descendants of BasicObject
|
322
365
|
klass = a.instance_exec { (class << self; self end).superclass }
|
@@ -343,14 +386,16 @@ class Redis
|
|
343
386
|
@pending_reads = 0
|
344
387
|
rescue TimeoutError,
|
345
388
|
SocketError,
|
389
|
+
Errno::EADDRNOTAVAIL,
|
346
390
|
Errno::ECONNREFUSED,
|
347
391
|
Errno::EHOSTDOWN,
|
348
392
|
Errno::EHOSTUNREACH,
|
349
393
|
Errno::ENETUNREACH,
|
350
394
|
Errno::ENOENT,
|
351
|
-
Errno::ETIMEDOUT
|
395
|
+
Errno::ETIMEDOUT,
|
396
|
+
Errno::EINVAL => error
|
352
397
|
|
353
|
-
raise CannotConnectError, "Error connecting to Redis on #{location} (#{
|
398
|
+
raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
|
354
399
|
end
|
355
400
|
|
356
401
|
def ensure_connected
|
@@ -364,9 +409,9 @@ class Redis
|
|
364
409
|
if connected?
|
365
410
|
unless inherit_socket? || Process.pid == @pid
|
366
411
|
raise InheritedError,
|
367
|
-
|
368
|
-
|
369
|
-
|
412
|
+
"Tried to use a connection from a child process without reconnecting. " \
|
413
|
+
"You need to reconnect to Redis after forking " \
|
414
|
+
"or set :inherit_socket to true."
|
370
415
|
end
|
371
416
|
else
|
372
417
|
connect
|
@@ -377,7 +422,7 @@ class Redis
|
|
377
422
|
disconnect
|
378
423
|
|
379
424
|
if attempts <= @options[:reconnect_attempts] && @reconnect
|
380
|
-
sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
|
425
|
+
sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
|
381
426
|
@options[:reconnect_delay_max]].min
|
382
427
|
|
383
428
|
Kernel.sleep(sleep_t)
|
@@ -399,15 +444,14 @@ class Redis
|
|
399
444
|
|
400
445
|
defaults.keys.each do |key|
|
401
446
|
# Fill in defaults if needed
|
402
|
-
if defaults[key].respond_to?(:call)
|
403
|
-
defaults[key] = defaults[key].call
|
404
|
-
end
|
447
|
+
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
405
448
|
|
406
449
|
# Symbolize only keys that are needed
|
407
|
-
options[key] = options[key.to_s] if options.
|
450
|
+
options[key] = options[key.to_s] if options.key?(key.to_s)
|
408
451
|
end
|
409
452
|
|
410
|
-
url = options[:url]
|
453
|
+
url = options[:url]
|
454
|
+
url = defaults[:url] if url.nil?
|
411
455
|
|
412
456
|
# Override defaults from URL if given
|
413
457
|
if url
|
@@ -416,12 +460,13 @@ class Redis
|
|
416
460
|
uri = URI(url)
|
417
461
|
|
418
462
|
if uri.scheme == "unix"
|
419
|
-
defaults[:path]
|
463
|
+
defaults[:path] = uri.path
|
420
464
|
elsif uri.scheme == "redis" || uri.scheme == "rediss"
|
421
465
|
defaults[:scheme] = uri.scheme
|
422
466
|
defaults[:host] = uri.host if uri.host
|
423
467
|
defaults[:port] = uri.port if uri.port
|
424
|
-
defaults[:
|
468
|
+
defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
|
469
|
+
defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
|
425
470
|
defaults[:db] = uri.path[1..-1].to_i if uri.path
|
426
471
|
defaults[:role] = :master
|
427
472
|
else
|
@@ -447,7 +492,7 @@ class Redis
|
|
447
492
|
options[:port] = options[:port].to_i
|
448
493
|
end
|
449
494
|
|
450
|
-
if options.
|
495
|
+
if options.key?(:timeout)
|
451
496
|
options[:connect_timeout] ||= options[:timeout]
|
452
497
|
options[:read_timeout] ||= options[:timeout]
|
453
498
|
options[:write_timeout] ||= options[:timeout]
|
@@ -466,7 +511,7 @@ class Redis
|
|
466
511
|
|
467
512
|
case options[:tcp_keepalive]
|
468
513
|
when Hash
|
469
|
-
[
|
514
|
+
%i[time intvl probes].each do |key|
|
470
515
|
unless options[:tcp_keepalive][key].is_a?(Integer)
|
471
516
|
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
472
517
|
end
|
@@ -474,13 +519,13 @@ class Redis
|
|
474
519
|
|
475
520
|
when Integer
|
476
521
|
if options[:tcp_keepalive] >= 60
|
477
|
-
options[:tcp_keepalive] = {:
|
522
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
478
523
|
|
479
524
|
elsif options[:tcp_keepalive] >= 30
|
480
|
-
options[:tcp_keepalive] = {:
|
525
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
481
526
|
|
482
527
|
elsif options[:tcp_keepalive] >= 5
|
483
|
-
options[:tcp_keepalive] = {:
|
528
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
484
529
|
end
|
485
530
|
end
|
486
531
|
|
@@ -492,14 +537,14 @@ class Redis
|
|
492
537
|
def _parse_driver(driver)
|
493
538
|
driver = driver.to_s if driver.is_a?(Symbol)
|
494
539
|
|
495
|
-
if driver.
|
540
|
+
if driver.is_a?(String)
|
496
541
|
begin
|
497
542
|
require_relative "connection/#{driver}"
|
498
|
-
rescue LoadError, NameError
|
543
|
+
rescue LoadError, NameError
|
499
544
|
begin
|
500
|
-
require "connection/#{driver}"
|
501
|
-
rescue LoadError, NameError =>
|
502
|
-
raise
|
545
|
+
require "redis/connection/#{driver}"
|
546
|
+
rescue LoadError, NameError => error
|
547
|
+
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
503
548
|
end
|
504
549
|
end
|
505
550
|
|
@@ -518,18 +563,16 @@ class Redis
|
|
518
563
|
@options
|
519
564
|
end
|
520
565
|
|
521
|
-
def check(client)
|
522
|
-
end
|
566
|
+
def check(client); end
|
523
567
|
|
524
568
|
class Sentinel < Connector
|
525
569
|
def initialize(options)
|
526
570
|
super(options)
|
527
571
|
|
528
|
-
@options[:password] = DEFAULTS.fetch(:password)
|
529
572
|
@options[:db] = DEFAULTS.fetch(:db)
|
530
573
|
|
531
574
|
@sentinels = @options.delete(:sentinels).dup
|
532
|
-
@role = @options
|
575
|
+
@role = (@options[:role] || "master").to_s
|
533
576
|
@master = @options[:host]
|
534
577
|
end
|
535
578
|
|
@@ -552,13 +595,13 @@ class Redis
|
|
552
595
|
|
553
596
|
def resolve
|
554
597
|
result = case @role
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
598
|
+
when "master"
|
599
|
+
resolve_master
|
600
|
+
when "slave"
|
601
|
+
resolve_slave
|
602
|
+
else
|
603
|
+
raise ArgumentError, "Unknown instance role #{@role}"
|
604
|
+
end
|
562
605
|
|
563
606
|
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
564
607
|
end
|
@@ -566,10 +609,12 @@ class Redis
|
|
566
609
|
def sentinel_detect
|
567
610
|
@sentinels.each do |sentinel|
|
568
611
|
client = Client.new(@options.merge({
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
612
|
+
host: sentinel[:host] || sentinel["host"],
|
613
|
+
port: sentinel[:port] || sentinel["port"],
|
614
|
+
username: sentinel[:username] || sentinel["username"],
|
615
|
+
password: sentinel[:password] || sentinel["password"],
|
616
|
+
reconnect_attempts: 0
|
617
|
+
}))
|
573
618
|
|
574
619
|
begin
|
575
620
|
if result = yield(client)
|
@@ -591,7 +636,7 @@ class Redis
|
|
591
636
|
def resolve_master
|
592
637
|
sentinel_detect do |client|
|
593
638
|
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
594
|
-
{:
|
639
|
+
{ host: reply[0], port: reply[1] }
|
595
640
|
end
|
596
641
|
end
|
597
642
|
end
|
@@ -599,9 +644,19 @@ class Redis
|
|
599
644
|
def resolve_slave
|
600
645
|
sentinel_detect do |client|
|
601
646
|
if reply = client.call(["sentinel", "slaves", @master])
|
602
|
-
|
603
|
-
|
604
|
-
{
|
647
|
+
slaves = reply.map { |s| s.each_slice(2).to_h }
|
648
|
+
slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
|
649
|
+
slaves.reject! { |s| s.fetch('flags').include?('s_down') }
|
650
|
+
|
651
|
+
if slaves.empty?
|
652
|
+
raise CannotConnectError, 'No slaves available.'
|
653
|
+
else
|
654
|
+
slave = slaves.sample
|
655
|
+
{
|
656
|
+
host: slave.fetch('ip'),
|
657
|
+
port: slave.fetch('port')
|
658
|
+
}
|
659
|
+
end
|
605
660
|
end
|
606
661
|
end
|
607
662
|
end
|
@@ -10,22 +10,21 @@ class Redis
|
|
10
10
|
module_function
|
11
11
|
|
12
12
|
def load(nodes)
|
13
|
-
details = {}
|
14
|
-
|
15
13
|
nodes.each do |node|
|
16
|
-
|
17
|
-
|
14
|
+
begin
|
15
|
+
return fetch_command_details(node)
|
16
|
+
rescue CannotConnectError, ConnectionError, CommandError
|
17
|
+
next # can retry on another node
|
18
|
+
end
|
18
19
|
end
|
19
20
|
|
20
|
-
|
21
|
+
raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
|
21
22
|
end
|
22
23
|
|
23
24
|
def fetch_command_details(node)
|
24
25
|
node.call(%i[command]).map do |reply|
|
25
26
|
[reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
|
26
27
|
end.to_h
|
27
|
-
rescue CannotConnectError, ConnectionError, CommandError
|
28
|
-
{} # can retry on another node
|
29
28
|
end
|
30
29
|
|
31
30
|
private_class_method :fetch_command_details
|
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
|
@@ -74,8 +76,9 @@ class Redis
|
|
74
76
|
clients = options.map do |node_key, option|
|
75
77
|
next if replica_disabled? && slave?(node_key)
|
76
78
|
|
79
|
+
option = option.merge(readonly: true) if slave?(node_key)
|
80
|
+
|
77
81
|
client = Client.new(option)
|
78
|
-
client.call(%i[readonly]) if slave?(node_key)
|
79
82
|
[node_key, client]
|
80
83
|
end
|
81
84
|
|
@@ -97,6 +100,7 @@ class Redis
|
|
97
100
|
end
|
98
101
|
|
99
102
|
return results if errors.empty?
|
103
|
+
|
100
104
|
raise CommandErrorCollection, errors
|
101
105
|
end
|
102
106
|
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,36 @@ 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, :username)
|
22
|
+
add_common_node_option_if_needed(options, @node_opts, :password)
|
20
23
|
@options = options
|
21
24
|
end
|
22
25
|
|
23
26
|
def per_node_key
|
24
|
-
@
|
27
|
+
@node_opts.map { |opt| [NodeKey.build_from_host_port(opt[:host], opt[:port]), @options.merge(opt)] }
|
25
28
|
.to_h
|
26
29
|
end
|
27
30
|
|
28
|
-
def secure?
|
29
|
-
@node_uris.any? { |uri| uri.scheme == SECURE_SCHEME } || @options[:ssl_params] || false
|
30
|
-
end
|
31
|
-
|
32
31
|
def use_replica?
|
33
32
|
@replica
|
34
33
|
end
|
35
34
|
|
36
35
|
def update_node(addrs)
|
37
|
-
@
|
36
|
+
@node_opts = build_node_options(addrs)
|
38
37
|
end
|
39
38
|
|
40
39
|
def add_node(host, port)
|
41
|
-
@
|
40
|
+
@node_opts << { host: host, port: port }
|
42
41
|
end
|
43
42
|
|
44
43
|
private
|
45
44
|
|
46
|
-
def
|
45
|
+
def build_node_options(addrs)
|
47
46
|
raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
|
47
|
+
|
48
48
|
addrs.map { |addr| parse_node_addr(addr) }
|
49
49
|
end
|
50
50
|
|
@@ -53,7 +53,7 @@ class Redis
|
|
53
53
|
when String
|
54
54
|
parse_node_url(addr)
|
55
55
|
when Hash
|
56
|
-
|
56
|
+
parse_node_option(addr)
|
57
57
|
else
|
58
58
|
raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash'
|
59
59
|
end
|
@@ -62,15 +62,31 @@ class Redis
|
|
62
62
|
def parse_node_url(addr)
|
63
63
|
uri = URI(addr)
|
64
64
|
raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
|
65
|
-
|
65
|
+
|
66
|
+
db = uri.path.split('/')[1]&.to_i
|
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
|
-
def
|
74
|
+
def parse_node_option(addr)
|
71
75
|
addr = addr.map { |k, v| [k.to_sym, v] }.to_h
|
72
|
-
|
73
|
-
|
76
|
+
if addr.values_at(:host, :port).any?(&:nil?)
|
77
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
|
78
|
+
end
|
79
|
+
|
80
|
+
addr
|
81
|
+
end
|
82
|
+
|
83
|
+
# Redis cluster node returns only host and port information.
|
84
|
+
# So we should complement additional information such as:
|
85
|
+
# scheme, username, password and so on.
|
86
|
+
def add_common_node_option_if_needed(options, node_opts, key)
|
87
|
+
return options if options[key].nil? && node_opts.first[key].nil?
|
88
|
+
|
89
|
+
options[key] ||= node_opts.first[key]
|
74
90
|
end
|
75
91
|
end
|
76
92
|
end
|