redis 4.1.4 → 4.7.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 +141 -0
- data/README.md +52 -27
- data/lib/redis/client.rb +122 -87
- data/lib/redis/cluster/command.rb +4 -6
- data/lib/redis/cluster/command_loader.rb +8 -9
- data/lib/redis/cluster/node.rb +17 -1
- data/lib/redis/cluster/node_loader.rb +8 -11
- data/lib/redis/cluster/option.rb +18 -5
- data/lib/redis/cluster/slot.rb +28 -14
- data/lib/redis/cluster/slot_loader.rb +11 -15
- data/lib/redis/cluster.rb +37 -13
- data/lib/redis/commands/bitmaps.rb +63 -0
- data/lib/redis/commands/cluster.rb +45 -0
- data/lib/redis/commands/connection.rb +58 -0
- data/lib/redis/commands/geo.rb +84 -0
- data/lib/redis/commands/hashes.rb +251 -0
- data/lib/redis/commands/hyper_log_log.rb +37 -0
- data/lib/redis/commands/keys.rb +411 -0
- data/lib/redis/commands/lists.rb +289 -0
- data/lib/redis/commands/pubsub.rb +72 -0
- data/lib/redis/commands/scripting.rb +114 -0
- data/lib/redis/commands/server.rb +188 -0
- data/lib/redis/commands/sets.rb +207 -0
- data/lib/redis/commands/sorted_sets.rb +812 -0
- data/lib/redis/commands/streams.rb +382 -0
- data/lib/redis/commands/strings.rb +313 -0
- data/lib/redis/commands/transactions.rb +139 -0
- data/lib/redis/commands.rb +242 -0
- data/lib/redis/connection/command_helper.rb +4 -2
- data/lib/redis/connection/hiredis.rb +6 -7
- data/lib/redis/connection/registry.rb +1 -1
- data/lib/redis/connection/ruby.rb +106 -114
- data/lib/redis/connection/synchrony.rb +16 -10
- data/lib/redis/connection.rb +2 -1
- data/lib/redis/distributed.rb +200 -65
- data/lib/redis/errors.rb +10 -0
- data/lib/redis/hash_ring.rb +14 -14
- data/lib/redis/pipeline.rb +133 -10
- data/lib/redis/subscribe.rb +10 -12
- data/lib/redis/version.rb +2 -1
- data/lib/redis.rb +158 -3358
- metadata +32 -10
data/lib/redis/client.rb
CHANGED
@@ -1,31 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "errors"
|
4
3
|
require "socket"
|
5
4
|
require "cgi"
|
5
|
+
require "redis/errors"
|
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
|
-
|
27
|
-
|
28
|
-
|
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
|
34
|
+
|
35
|
+
attr_reader :options, :connection, :command_map
|
29
36
|
|
30
37
|
def scheme
|
31
38
|
@options[:scheme]
|
@@ -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
|
@@ -76,8 +87,6 @@ class Redis
|
|
76
87
|
end
|
77
88
|
|
78
89
|
attr_accessor :logger
|
79
|
-
attr_reader :connection
|
80
|
-
attr_reader :command_map
|
81
90
|
|
82
91
|
def initialize(options = {})
|
83
92
|
@options = _parse_options(options)
|
@@ -89,7 +98,7 @@ class Redis
|
|
89
98
|
@pending_reads = 0
|
90
99
|
|
91
100
|
@connector =
|
92
|
-
if options.
|
101
|
+
if !@options[:sentinels].nil?
|
93
102
|
Connector::Sentinel.new(@options)
|
94
103
|
elsif options.include?(:connector) && options[:connector].respond_to?(:new)
|
95
104
|
options.delete(:connector).new(@options)
|
@@ -104,7 +113,34 @@ class Redis
|
|
104
113
|
# Don't try to reconnect when the connection is fresh
|
105
114
|
with_reconnect(false) do
|
106
115
|
establish_connection
|
107
|
-
|
116
|
+
if password
|
117
|
+
if username
|
118
|
+
begin
|
119
|
+
call [:auth, username, password]
|
120
|
+
rescue CommandError => err # Likely on Redis < 6
|
121
|
+
case err.message
|
122
|
+
when /ERR wrong number of arguments for 'auth' command/
|
123
|
+
call [:auth, password]
|
124
|
+
when /WRONGPASS invalid username-password pair/
|
125
|
+
begin
|
126
|
+
call [:auth, password]
|
127
|
+
rescue CommandError
|
128
|
+
raise err
|
129
|
+
end
|
130
|
+
::Redis.deprecate!(
|
131
|
+
"[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
|
132
|
+
" the provided password was for the default user. This will start failing in redis-rb 5.0.0."
|
133
|
+
)
|
134
|
+
else
|
135
|
+
raise
|
136
|
+
end
|
137
|
+
end
|
138
|
+
else
|
139
|
+
call [:auth, password]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
call [:readonly] if @options[:readonly]
|
108
144
|
call [:select, db] if db != 0
|
109
145
|
call [:client, :setname, @options[:id]] if @options[:id]
|
110
146
|
@connector.check(self)
|
@@ -114,7 +150,7 @@ class Redis
|
|
114
150
|
end
|
115
151
|
|
116
152
|
def id
|
117
|
-
@options[:id] || "
|
153
|
+
@options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
|
118
154
|
end
|
119
155
|
|
120
156
|
def location
|
@@ -125,7 +161,7 @@ class Redis
|
|
125
161
|
reply = process([command]) { read }
|
126
162
|
raise reply if reply.is_a?(CommandError)
|
127
163
|
|
128
|
-
if block_given?
|
164
|
+
if block_given? && reply != 'QUEUED'
|
129
165
|
yield reply
|
130
166
|
else
|
131
167
|
reply
|
@@ -166,6 +202,7 @@ class Redis
|
|
166
202
|
end
|
167
203
|
rescue ConnectionError => e
|
168
204
|
return nil if pipeline.shutdown?
|
205
|
+
|
169
206
|
# Assume the pipeline was sent in one piece, but execution of
|
170
207
|
# SHUTDOWN caused none of the replies for commands that were executed
|
171
208
|
# prior to it from coming back around.
|
@@ -214,7 +251,8 @@ class Redis
|
|
214
251
|
result
|
215
252
|
end
|
216
253
|
|
217
|
-
def call_with_timeout(command,
|
254
|
+
def call_with_timeout(command, extra_timeout, &blk)
|
255
|
+
timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout
|
218
256
|
with_socket_timeout(timeout) do
|
219
257
|
call(command, &blk)
|
220
258
|
end
|
@@ -244,13 +282,13 @@ class Redis
|
|
244
282
|
end
|
245
283
|
|
246
284
|
def connected?
|
247
|
-
!!
|
285
|
+
!!(connection && connection.connected?)
|
248
286
|
end
|
249
287
|
|
250
288
|
def disconnect
|
251
289
|
connection.disconnect if connected?
|
252
290
|
end
|
253
|
-
|
291
|
+
alias close disconnect
|
254
292
|
|
255
293
|
def reconnect
|
256
294
|
disconnect
|
@@ -264,7 +302,7 @@ class Redis
|
|
264
302
|
e2 = TimeoutError.new("Connection timed out")
|
265
303
|
e2.set_backtrace(e1.backtrace)
|
266
304
|
raise e2
|
267
|
-
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL => e
|
305
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL, EOFError => e
|
268
306
|
raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
|
269
307
|
end
|
270
308
|
|
@@ -301,30 +339,27 @@ class Redis
|
|
301
339
|
with_socket_timeout(0, &blk)
|
302
340
|
end
|
303
341
|
|
304
|
-
def with_reconnect(val=true)
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
@reconnect = original
|
310
|
-
end
|
342
|
+
def with_reconnect(val = true)
|
343
|
+
original, @reconnect = @reconnect, val
|
344
|
+
yield
|
345
|
+
ensure
|
346
|
+
@reconnect = original
|
311
347
|
end
|
312
348
|
|
313
349
|
def without_reconnect(&blk)
|
314
350
|
with_reconnect(false, &blk)
|
315
351
|
end
|
316
352
|
|
317
|
-
|
353
|
+
protected
|
318
354
|
|
319
355
|
def logging(commands)
|
320
|
-
return yield unless @logger
|
356
|
+
return yield unless @logger&.debug?
|
321
357
|
|
322
358
|
begin
|
323
359
|
commands.each do |name, *args|
|
324
360
|
logged_args = args.map do |a|
|
325
|
-
|
326
|
-
|
327
|
-
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
|
328
363
|
else
|
329
364
|
# handle poorly-behaved descendants of BasicObject
|
330
365
|
klass = a.instance_exec { (class << self; self end).superclass }
|
@@ -358,9 +393,9 @@ class Redis
|
|
358
393
|
Errno::ENETUNREACH,
|
359
394
|
Errno::ENOENT,
|
360
395
|
Errno::ETIMEDOUT,
|
361
|
-
Errno::EINVAL
|
396
|
+
Errno::EINVAL => error
|
362
397
|
|
363
|
-
raise CannotConnectError, "Error connecting to Redis on #{location} (#{
|
398
|
+
raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
|
364
399
|
end
|
365
400
|
|
366
401
|
def ensure_connected
|
@@ -374,9 +409,9 @@ class Redis
|
|
374
409
|
if connected?
|
375
410
|
unless inherit_socket? || Process.pid == @pid
|
376
411
|
raise InheritedError,
|
377
|
-
|
378
|
-
|
379
|
-
|
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."
|
380
415
|
end
|
381
416
|
else
|
382
417
|
connect
|
@@ -387,7 +422,7 @@ class Redis
|
|
387
422
|
disconnect
|
388
423
|
|
389
424
|
if attempts <= @options[:reconnect_attempts] && @reconnect
|
390
|
-
sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
|
425
|
+
sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
|
391
426
|
@options[:reconnect_delay_max]].min
|
392
427
|
|
393
428
|
Kernel.sleep(sleep_t)
|
@@ -407,18 +442,16 @@ class Redis
|
|
407
442
|
defaults = DEFAULTS.dup
|
408
443
|
options = options.dup
|
409
444
|
|
410
|
-
defaults.
|
445
|
+
defaults.each_key do |key|
|
411
446
|
# Fill in defaults if needed
|
412
|
-
if defaults[key].respond_to?(:call)
|
413
|
-
defaults[key] = defaults[key].call
|
414
|
-
end
|
447
|
+
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
415
448
|
|
416
449
|
# Symbolize only keys that are needed
|
417
|
-
options[key] = options[key.to_s] if options.
|
450
|
+
options[key] = options[key.to_s] if options.key?(key.to_s)
|
418
451
|
end
|
419
452
|
|
420
453
|
url = options[:url]
|
421
|
-
url = defaults[:url] if url
|
454
|
+
url = defaults[:url] if url.nil?
|
422
455
|
|
423
456
|
# Override defaults from URL if given
|
424
457
|
if url
|
@@ -426,13 +459,15 @@ class Redis
|
|
426
459
|
|
427
460
|
uri = URI(url)
|
428
461
|
|
429
|
-
|
430
|
-
|
431
|
-
|
462
|
+
case uri.scheme
|
463
|
+
when "unix"
|
464
|
+
defaults[:path] = uri.path
|
465
|
+
when "redis", "rediss"
|
432
466
|
defaults[:scheme] = uri.scheme
|
433
|
-
defaults[:host] = uri.host if uri.host
|
467
|
+
defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
|
434
468
|
defaults[:port] = uri.port if uri.port
|
435
|
-
defaults[:
|
469
|
+
defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
|
470
|
+
defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
|
436
471
|
defaults[:db] = uri.path[1..-1].to_i if uri.path
|
437
472
|
defaults[:role] = :master
|
438
473
|
else
|
@@ -443,7 +478,7 @@ class Redis
|
|
443
478
|
end
|
444
479
|
|
445
480
|
# Use default when option is not specified or nil
|
446
|
-
defaults.
|
481
|
+
defaults.each_key do |key|
|
447
482
|
options[key] = defaults[key] if options[key].nil?
|
448
483
|
end
|
449
484
|
|
@@ -458,7 +493,7 @@ class Redis
|
|
458
493
|
options[:port] = options[:port].to_i
|
459
494
|
end
|
460
495
|
|
461
|
-
if options.
|
496
|
+
if options.key?(:timeout)
|
462
497
|
options[:connect_timeout] ||= options[:timeout]
|
463
498
|
options[:read_timeout] ||= options[:timeout]
|
464
499
|
options[:write_timeout] ||= options[:timeout]
|
@@ -477,7 +512,7 @@ class Redis
|
|
477
512
|
|
478
513
|
case options[:tcp_keepalive]
|
479
514
|
when Hash
|
480
|
-
[
|
515
|
+
%i[time intvl probes].each do |key|
|
481
516
|
unless options[:tcp_keepalive][key].is_a?(Integer)
|
482
517
|
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
483
518
|
end
|
@@ -485,13 +520,13 @@ class Redis
|
|
485
520
|
|
486
521
|
when Integer
|
487
522
|
if options[:tcp_keepalive] >= 60
|
488
|
-
options[:tcp_keepalive] = {:
|
523
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
489
524
|
|
490
525
|
elsif options[:tcp_keepalive] >= 30
|
491
|
-
options[:tcp_keepalive] = {:
|
526
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
492
527
|
|
493
528
|
elsif options[:tcp_keepalive] >= 5
|
494
|
-
options[:tcp_keepalive] = {:
|
529
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
495
530
|
end
|
496
531
|
end
|
497
532
|
|
@@ -503,14 +538,14 @@ class Redis
|
|
503
538
|
def _parse_driver(driver)
|
504
539
|
driver = driver.to_s if driver.is_a?(Symbol)
|
505
540
|
|
506
|
-
if driver.
|
541
|
+
if driver.is_a?(String)
|
507
542
|
begin
|
508
543
|
require_relative "connection/#{driver}"
|
509
|
-
rescue LoadError, NameError
|
544
|
+
rescue LoadError, NameError
|
510
545
|
begin
|
511
|
-
require "connection/#{driver}"
|
512
|
-
rescue LoadError, NameError =>
|
513
|
-
raise
|
546
|
+
require "redis/connection/#{driver}"
|
547
|
+
rescue LoadError, NameError => error
|
548
|
+
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
514
549
|
end
|
515
550
|
end
|
516
551
|
|
@@ -529,8 +564,7 @@ class Redis
|
|
529
564
|
@options
|
530
565
|
end
|
531
566
|
|
532
|
-
def check(client)
|
533
|
-
end
|
567
|
+
def check(client); end
|
534
568
|
|
535
569
|
class Sentinel < Connector
|
536
570
|
def initialize(options)
|
@@ -539,7 +573,7 @@ class Redis
|
|
539
573
|
@options[:db] = DEFAULTS.fetch(:db)
|
540
574
|
|
541
575
|
@sentinels = @options.delete(:sentinels).dup
|
542
|
-
@role = @options
|
576
|
+
@role = (@options[:role] || "master").to_s
|
543
577
|
@master = @options[:host]
|
544
578
|
end
|
545
579
|
|
@@ -562,13 +596,13 @@ class Redis
|
|
562
596
|
|
563
597
|
def resolve
|
564
598
|
result = case @role
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
599
|
+
when "master"
|
600
|
+
resolve_master
|
601
|
+
when "slave"
|
602
|
+
resolve_slave
|
603
|
+
else
|
604
|
+
raise ArgumentError, "Unknown instance role #{@role}"
|
605
|
+
end
|
572
606
|
|
573
607
|
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
574
608
|
end
|
@@ -576,11 +610,12 @@ class Redis
|
|
576
610
|
def sentinel_detect
|
577
611
|
@sentinels.each do |sentinel|
|
578
612
|
client = Client.new(@options.merge({
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
613
|
+
host: sentinel[:host] || sentinel["host"],
|
614
|
+
port: sentinel[:port] || sentinel["port"],
|
615
|
+
username: sentinel[:username] || sentinel["username"],
|
616
|
+
password: sentinel[:password] || sentinel["password"],
|
617
|
+
reconnect_attempts: 0
|
618
|
+
}))
|
584
619
|
|
585
620
|
begin
|
586
621
|
if result = yield(client)
|
@@ -602,7 +637,7 @@ class Redis
|
|
602
637
|
def resolve_master
|
603
638
|
sentinel_detect do |client|
|
604
639
|
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
605
|
-
{:
|
640
|
+
{ host: reply[0], port: reply[1] }
|
606
641
|
end
|
607
642
|
end
|
608
643
|
end
|
@@ -620,7 +655,7 @@ class Redis
|
|
620
655
|
slave = slaves.sample
|
621
656
|
{
|
622
657
|
host: slave.fetch('ip'),
|
623
|
-
port: slave.fetch('port')
|
658
|
+
port: slave.fetch('port')
|
624
659
|
}
|
625
660
|
end
|
626
661
|
end
|
@@ -31,13 +31,13 @@ class Redis
|
|
31
31
|
private
|
32
32
|
|
33
33
|
def pick_details(details)
|
34
|
-
details.
|
35
|
-
|
34
|
+
details.transform_values do |detail|
|
35
|
+
{
|
36
36
|
first_key_position: detail[:first],
|
37
37
|
write: detail[:flags].include?('write'),
|
38
38
|
readonly: detail[:flags].include?('readonly')
|
39
|
-
}
|
40
|
-
end
|
39
|
+
}
|
40
|
+
end
|
41
41
|
end
|
42
42
|
|
43
43
|
def dig_details(command, key)
|
@@ -53,8 +53,6 @@ class Redis
|
|
53
53
|
when 'object' then 2
|
54
54
|
when 'memory'
|
55
55
|
command[1].to_s.casecmp('usage').zero? ? 2 : 0
|
56
|
-
when 'scan', 'sscan', 'hscan', 'zscan'
|
57
|
-
determine_optional_key_position(command, 'match')
|
58
56
|
when 'xread', 'xreadgroup'
|
59
57
|
determine_optional_key_position(command, 'streams')
|
60
58
|
else
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require 'redis/errors'
|
4
4
|
|
5
5
|
class Redis
|
6
6
|
class Cluster
|
@@ -10,22 +10,21 @@ class Redis
|
|
10
10
|
module_function
|
11
11
|
|
12
12
|
def load(nodes)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
errors = nodes.map do |node|
|
14
|
+
begin
|
15
|
+
return fetch_command_details(node)
|
16
|
+
rescue CannotConnectError, ConnectionError, CommandError => error
|
17
|
+
error
|
18
|
+
end
|
18
19
|
end
|
19
20
|
|
20
|
-
|
21
|
+
raise InitialSetupError, errors
|
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
|
@@ -56,6 +58,18 @@ class Redis
|
|
56
58
|
try_map { |_, client| client.process(commands, &block) }.values
|
57
59
|
end
|
58
60
|
|
61
|
+
def scale_reading_clients
|
62
|
+
reading_clients = []
|
63
|
+
|
64
|
+
@clients.each do |node_key, client|
|
65
|
+
next unless replica_disabled? ? master?(node_key) : slave?(node_key)
|
66
|
+
|
67
|
+
reading_clients << client
|
68
|
+
end
|
69
|
+
|
70
|
+
reading_clients
|
71
|
+
end
|
72
|
+
|
59
73
|
private
|
60
74
|
|
61
75
|
def replica_disabled?
|
@@ -74,8 +88,9 @@ class Redis
|
|
74
88
|
clients = options.map do |node_key, option|
|
75
89
|
next if replica_disabled? && slave?(node_key)
|
76
90
|
|
91
|
+
option = option.merge(readonly: true) if slave?(node_key)
|
92
|
+
|
77
93
|
client = Client.new(option)
|
78
|
-
client.call(%i[readonly]) if slave?(node_key)
|
79
94
|
[node_key, client]
|
80
95
|
end
|
81
96
|
|
@@ -97,6 +112,7 @@ class Redis
|
|
97
112
|
end
|
98
113
|
|
99
114
|
return results if errors.empty?
|
115
|
+
|
100
116
|
raise CommandErrorCollection, errors
|
101
117
|
end
|
102
118
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require 'redis/errors'
|
4
4
|
|
5
5
|
class Redis
|
6
6
|
class Cluster
|
@@ -9,16 +9,15 @@ class Redis
|
|
9
9
|
module_function
|
10
10
|
|
11
11
|
def load_flags(nodes)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
errors = nodes.map do |node|
|
13
|
+
begin
|
14
|
+
return fetch_node_info(node)
|
15
|
+
rescue CannotConnectError, ConnectionError, CommandError => error
|
16
|
+
error
|
17
|
+
end
|
17
18
|
end
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
|
20
|
+
raise InitialSetupError, errors
|
22
21
|
end
|
23
22
|
|
24
23
|
def fetch_node_info(node)
|
@@ -27,8 +26,6 @@ class Redis
|
|
27
26
|
.map { |str| str.split(' ') }
|
28
27
|
.map { |arr| [arr[1].split('@').first, (arr[2].split(',') & %w[master slave]).first] }
|
29
28
|
.to_h
|
30
|
-
rescue CannotConnectError, ConnectionError, CommandError
|
31
|
-
{} # can retry on another node
|
32
29
|
end
|
33
30
|
|
34
31
|
private_class_method :fetch_node_info
|
data/lib/redis/cluster/option.rb
CHANGED
@@ -17,14 +17,20 @@ class Redis
|
|
17
17
|
node_addrs = options.delete(:cluster)
|
18
18
|
@node_opts = build_node_options(node_addrs)
|
19
19
|
@replica = options.delete(:replica) == true
|
20
|
+
@fixed_hostname = options.delete(:fixed_hostname)
|
20
21
|
add_common_node_option_if_needed(options, @node_opts, :scheme)
|
22
|
+
add_common_node_option_if_needed(options, @node_opts, :username)
|
21
23
|
add_common_node_option_if_needed(options, @node_opts, :password)
|
22
24
|
@options = options
|
23
25
|
end
|
24
26
|
|
25
27
|
def per_node_key
|
26
|
-
@node_opts.map
|
27
|
-
|
28
|
+
@node_opts.map do |opt|
|
29
|
+
node_key = NodeKey.build_from_host_port(opt[:host], opt[:port])
|
30
|
+
options = @options.merge(opt)
|
31
|
+
options = options.merge(host: @fixed_hostname) if @fixed_hostname && !@fixed_hostname.empty?
|
32
|
+
[node_key, options]
|
33
|
+
end.to_h
|
28
34
|
end
|
29
35
|
|
30
36
|
def use_replica?
|
@@ -43,6 +49,7 @@ class Redis
|
|
43
49
|
|
44
50
|
def build_node_options(addrs)
|
45
51
|
raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
|
52
|
+
|
46
53
|
addrs.map { |addr| parse_node_addr(addr) }
|
47
54
|
end
|
48
55
|
|
@@ -62,21 +69,27 @@ class Redis
|
|
62
69
|
raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
|
63
70
|
|
64
71
|
db = uri.path.split('/')[1]&.to_i
|
65
|
-
|
72
|
+
username = uri.user ? URI.decode_www_form_component(uri.user) : nil
|
73
|
+
password = uri.password ? URI.decode_www_form_component(uri.password) : nil
|
74
|
+
|
75
|
+
{ scheme: uri.scheme, username: username, password: password, host: uri.host, port: uri.port, db: db }
|
76
|
+
.reject { |_, v| v.nil? || v == '' }
|
66
77
|
rescue URI::InvalidURIError => err
|
67
78
|
raise InvalidClientOptionError, err.message
|
68
79
|
end
|
69
80
|
|
70
81
|
def parse_node_option(addr)
|
71
82
|
addr = addr.map { |k, v| [k.to_sym, v] }.to_h
|
72
|
-
|
83
|
+
if addr.values_at(:host, :port).any?(&:nil?)
|
84
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
|
85
|
+
end
|
73
86
|
|
74
87
|
addr
|
75
88
|
end
|
76
89
|
|
77
90
|
# Redis cluster node returns only host and port information.
|
78
91
|
# So we should complement additional information such as:
|
79
|
-
# scheme, password and so on.
|
92
|
+
# scheme, username, password and so on.
|
80
93
|
def add_common_node_option_if_needed(options, node_opts, key)
|
81
94
|
return options if options[key].nil? && node_opts.first[key].nil?
|
82
95
|
|