redis 4.1.0 → 4.6.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 +158 -0
- data/README.md +91 -27
- data/lib/redis/client.rb +148 -92
- data/lib/redis/cluster/command.rb +4 -6
- data/lib/redis/cluster/command_loader.rb +6 -7
- data/lib/redis/cluster/node.rb +17 -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 +46 -17
- 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 +804 -0
- data/lib/redis/commands/streams.rb +382 -0
- data/lib/redis/commands/strings.rb +313 -0
- data/lib/redis/commands/transactions.rb +92 -0
- data/lib/redis/commands.rb +242 -0
- data/lib/redis/connection/command_helper.rb +5 -2
- data/lib/redis/connection/hiredis.rb +7 -5
- data/lib/redis/connection/registry.rb +2 -1
- data/lib/redis/connection/ruby.rb +129 -110
- data/lib/redis/connection/synchrony.rb +17 -10
- data/lib/redis/connection.rb +3 -1
- data/lib/redis/distributed.rb +209 -70
- data/lib/redis/errors.rb +2 -0
- data/lib/redis/hash_ring.rb +15 -14
- data/lib/redis/pipeline.rb +139 -8
- data/lib/redis/subscribe.rb +11 -12
- data/lib/redis/version.rb +3 -1
- data/lib/redis.rb +167 -3377
- metadata +32 -25
data/lib/redis/client.rb
CHANGED
@@ -1,29 +1,38 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "socket"
|
3
4
|
require "cgi"
|
5
|
+
require "redis/errors"
|
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
|
-
|
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
|
34
|
+
|
35
|
+
attr_reader :options, :connection, :command_map
|
27
36
|
|
28
37
|
def scheme
|
29
38
|
@options[:scheme]
|
@@ -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
|
@@ -74,8 +87,6 @@ class Redis
|
|
74
87
|
end
|
75
88
|
|
76
89
|
attr_accessor :logger
|
77
|
-
attr_reader :connection
|
78
|
-
attr_reader :command_map
|
79
90
|
|
80
91
|
def initialize(options = {})
|
81
92
|
@options = _parse_options(options)
|
@@ -87,7 +98,7 @@ class Redis
|
|
87
98
|
@pending_reads = 0
|
88
99
|
|
89
100
|
@connector =
|
90
|
-
if options.
|
101
|
+
if !@options[:sentinels].nil?
|
91
102
|
Connector::Sentinel.new(@options)
|
92
103
|
elsif options.include?(:connector) && options[:connector].respond_to?(:new)
|
93
104
|
options.delete(:connector).new(@options)
|
@@ -102,7 +113,34 @@ class Redis
|
|
102
113
|
# Don't try to reconnect when the connection is fresh
|
103
114
|
with_reconnect(false) do
|
104
115
|
establish_connection
|
105
|
-
|
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]
|
106
144
|
call [:select, db] if db != 0
|
107
145
|
call [:client, :setname, @options[:id]] if @options[:id]
|
108
146
|
@connector.check(self)
|
@@ -123,7 +161,7 @@ class Redis
|
|
123
161
|
reply = process([command]) { read }
|
124
162
|
raise reply if reply.is_a?(CommandError)
|
125
163
|
|
126
|
-
if block_given?
|
164
|
+
if block_given? && reply != 'QUEUED'
|
127
165
|
yield reply
|
128
166
|
else
|
129
167
|
reply
|
@@ -155,16 +193,16 @@ class Redis
|
|
155
193
|
end
|
156
194
|
|
157
195
|
def call_pipeline(pipeline)
|
158
|
-
|
159
|
-
return [] if commands.empty?
|
196
|
+
return [] if pipeline.futures.empty?
|
160
197
|
|
161
198
|
with_reconnect pipeline.with_reconnect? do
|
162
199
|
begin
|
163
|
-
pipeline.finish(call_pipelined(
|
200
|
+
pipeline.finish(call_pipelined(pipeline)).tap do
|
164
201
|
self.db = pipeline.db if pipeline.db
|
165
202
|
end
|
166
203
|
rescue ConnectionError => e
|
167
204
|
return nil if pipeline.shutdown?
|
205
|
+
|
168
206
|
# Assume the pipeline was sent in one piece, but execution of
|
169
207
|
# SHUTDOWN caused none of the replies for commands that were executed
|
170
208
|
# prior to it from coming back around.
|
@@ -173,8 +211,8 @@ class Redis
|
|
173
211
|
end
|
174
212
|
end
|
175
213
|
|
176
|
-
def call_pipelined(
|
177
|
-
return [] if
|
214
|
+
def call_pipelined(pipeline)
|
215
|
+
return [] if pipeline.futures.empty?
|
178
216
|
|
179
217
|
# The method #ensure_connected (called from #process) reconnects once on
|
180
218
|
# I/O errors. To make an effort in making sure that commands are not
|
@@ -184,6 +222,8 @@ class Redis
|
|
184
222
|
# already successfully executed commands. To circumvent this, don't retry
|
185
223
|
# after the first reply has been read successfully.
|
186
224
|
|
225
|
+
commands = pipeline.commands
|
226
|
+
|
187
227
|
result = Array.new(commands.size)
|
188
228
|
reconnect = @reconnect
|
189
229
|
|
@@ -191,8 +231,12 @@ class Redis
|
|
191
231
|
exception = nil
|
192
232
|
|
193
233
|
process(commands) do
|
194
|
-
|
195
|
-
reply =
|
234
|
+
pipeline.timeouts.each_with_index do |timeout, i|
|
235
|
+
reply = if timeout
|
236
|
+
with_socket_timeout(timeout) { read }
|
237
|
+
else
|
238
|
+
read
|
239
|
+
end
|
196
240
|
result[i] = reply
|
197
241
|
@reconnect = false
|
198
242
|
exception = reply if exception.nil? && reply.is_a?(CommandError)
|
@@ -207,7 +251,8 @@ class Redis
|
|
207
251
|
result
|
208
252
|
end
|
209
253
|
|
210
|
-
def call_with_timeout(command,
|
254
|
+
def call_with_timeout(command, extra_timeout, &blk)
|
255
|
+
timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout
|
211
256
|
with_socket_timeout(timeout) do
|
212
257
|
call(command, &blk)
|
213
258
|
end
|
@@ -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)
|
@@ -397,17 +442,16 @@ class Redis
|
|
397
442
|
defaults = DEFAULTS.dup
|
398
443
|
options = options.dup
|
399
444
|
|
400
|
-
defaults.
|
445
|
+
defaults.each_key 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
|
@@ -415,13 +459,15 @@ class Redis
|
|
415
459
|
|
416
460
|
uri = URI(url)
|
417
461
|
|
418
|
-
|
419
|
-
|
420
|
-
|
462
|
+
case uri.scheme
|
463
|
+
when "unix"
|
464
|
+
defaults[:path] = uri.path
|
465
|
+
when "redis", "rediss"
|
421
466
|
defaults[:scheme] = uri.scheme
|
422
467
|
defaults[:host] = uri.host if uri.host
|
423
468
|
defaults[:port] = uri.port if uri.port
|
424
|
-
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?
|
425
471
|
defaults[:db] = uri.path[1..-1].to_i if uri.path
|
426
472
|
defaults[:role] = :master
|
427
473
|
else
|
@@ -432,7 +478,7 @@ class Redis
|
|
432
478
|
end
|
433
479
|
|
434
480
|
# Use default when option is not specified or nil
|
435
|
-
defaults.
|
481
|
+
defaults.each_key do |key|
|
436
482
|
options[key] = defaults[key] if options[key].nil?
|
437
483
|
end
|
438
484
|
|
@@ -447,7 +493,7 @@ class Redis
|
|
447
493
|
options[:port] = options[:port].to_i
|
448
494
|
end
|
449
495
|
|
450
|
-
if options.
|
496
|
+
if options.key?(:timeout)
|
451
497
|
options[:connect_timeout] ||= options[:timeout]
|
452
498
|
options[:read_timeout] ||= options[:timeout]
|
453
499
|
options[:write_timeout] ||= options[:timeout]
|
@@ -466,7 +512,7 @@ class Redis
|
|
466
512
|
|
467
513
|
case options[:tcp_keepalive]
|
468
514
|
when Hash
|
469
|
-
[
|
515
|
+
%i[time intvl probes].each do |key|
|
470
516
|
unless options[:tcp_keepalive][key].is_a?(Integer)
|
471
517
|
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
472
518
|
end
|
@@ -474,13 +520,13 @@ class Redis
|
|
474
520
|
|
475
521
|
when Integer
|
476
522
|
if options[:tcp_keepalive] >= 60
|
477
|
-
options[:tcp_keepalive] = {:
|
523
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
478
524
|
|
479
525
|
elsif options[:tcp_keepalive] >= 30
|
480
|
-
options[:tcp_keepalive] = {:
|
526
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
481
527
|
|
482
528
|
elsif options[:tcp_keepalive] >= 5
|
483
|
-
options[:tcp_keepalive] = {:
|
529
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
484
530
|
end
|
485
531
|
end
|
486
532
|
|
@@ -492,14 +538,14 @@ class Redis
|
|
492
538
|
def _parse_driver(driver)
|
493
539
|
driver = driver.to_s if driver.is_a?(Symbol)
|
494
540
|
|
495
|
-
if driver.
|
541
|
+
if driver.is_a?(String)
|
496
542
|
begin
|
497
543
|
require_relative "connection/#{driver}"
|
498
|
-
rescue LoadError, NameError
|
544
|
+
rescue LoadError, NameError
|
499
545
|
begin
|
500
|
-
require "connection/#{driver}"
|
501
|
-
rescue LoadError, NameError =>
|
502
|
-
raise
|
546
|
+
require "redis/connection/#{driver}"
|
547
|
+
rescue LoadError, NameError => error
|
548
|
+
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
503
549
|
end
|
504
550
|
end
|
505
551
|
|
@@ -518,18 +564,16 @@ class Redis
|
|
518
564
|
@options
|
519
565
|
end
|
520
566
|
|
521
|
-
def check(client)
|
522
|
-
end
|
567
|
+
def check(client); end
|
523
568
|
|
524
569
|
class Sentinel < Connector
|
525
570
|
def initialize(options)
|
526
571
|
super(options)
|
527
572
|
|
528
|
-
@options[:password] = DEFAULTS.fetch(:password)
|
529
573
|
@options[:db] = DEFAULTS.fetch(:db)
|
530
574
|
|
531
575
|
@sentinels = @options.delete(:sentinels).dup
|
532
|
-
@role = @options
|
576
|
+
@role = (@options[:role] || "master").to_s
|
533
577
|
@master = @options[:host]
|
534
578
|
end
|
535
579
|
|
@@ -552,13 +596,13 @@ class Redis
|
|
552
596
|
|
553
597
|
def resolve
|
554
598
|
result = case @role
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
599
|
+
when "master"
|
600
|
+
resolve_master
|
601
|
+
when "slave"
|
602
|
+
resolve_slave
|
603
|
+
else
|
604
|
+
raise ArgumentError, "Unknown instance role #{@role}"
|
605
|
+
end
|
562
606
|
|
563
607
|
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
564
608
|
end
|
@@ -566,10 +610,12 @@ class Redis
|
|
566
610
|
def sentinel_detect
|
567
611
|
@sentinels.each do |sentinel|
|
568
612
|
client = Client.new(@options.merge({
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
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
|
+
}))
|
573
619
|
|
574
620
|
begin
|
575
621
|
if result = yield(client)
|
@@ -591,7 +637,7 @@ class Redis
|
|
591
637
|
def resolve_master
|
592
638
|
sentinel_detect do |client|
|
593
639
|
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
594
|
-
{:
|
640
|
+
{ host: reply[0], port: reply[1] }
|
595
641
|
end
|
596
642
|
end
|
597
643
|
end
|
@@ -599,9 +645,19 @@ class Redis
|
|
599
645
|
def resolve_slave
|
600
646
|
sentinel_detect do |client|
|
601
647
|
if reply = client.call(["sentinel", "slaves", @master])
|
602
|
-
|
603
|
-
|
604
|
-
{
|
648
|
+
slaves = reply.map { |s| s.each_slice(2).to_h }
|
649
|
+
slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
|
650
|
+
slaves.reject! { |s| s.fetch('flags').include?('s_down') }
|
651
|
+
|
652
|
+
if slaves.empty?
|
653
|
+
raise CannotConnectError, 'No slaves available.'
|
654
|
+
else
|
655
|
+
slave = slaves.sample
|
656
|
+
{
|
657
|
+
host: slave.fetch('ip'),
|
658
|
+
port: slave.fetch('port')
|
659
|
+
}
|
660
|
+
end
|
605
661
|
end
|
606
662
|
end
|
607
663
|
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
|
@@ -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
|
@@ -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
|
@@ -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)
|