redis 4.1.2 → 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 +4 -4
- data/CHANGELOG.md +28 -1
- data/README.md +14 -5
- data/lib/redis/client.rb +66 -69
- 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/cluster.rb +13 -4
- 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 +47 -58
- data/lib/redis/connection/synchrony.rb +9 -4
- data/lib/redis/connection.rb +2 -0
- 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 +16 -3
- data/lib/redis/subscribe.rb +11 -12
- data/lib/redis/version.rb +3 -1
- data/lib/redis.rb +361 -342
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9796f6646b7d3aaeeb5ef37629fb1a43422285724d8d2901a219ef4f2882eff5
|
4
|
+
data.tar.gz: b89f4f1d6a3c9ee93202ce08cdcd3ed184694b5693f7461be7cf7e517139a278
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be709e1aad1acee8d7c3e121e946060ce9b693ac80e25af200e2a579988f3952d6522cb9855917dc76628ae2038365bb8754b49a7cc6e90395706ba30fc87a86
|
7
|
+
data.tar.gz: 00102b01b4b37daab76fc90990f980a4710bec0e797a70d65a7544ef304f01f9b9babbdcc680da71513dd8f79649bd5f529e6804e71297b26978625bea746367
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,35 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
-
# 4.
|
3
|
+
# 4.2.0
|
4
|
+
|
5
|
+
* Convert commands to accept keyword arguments rather than option hashes. This both help catching typos, and reduce needless allocations.
|
6
|
+
* Deprecate the synchrony driver. It will be removed in 5.0 and hopefully maintained as a separate gem. See #915.
|
7
|
+
* Make `Redis#exists` variadic, will return an Integer if called with multiple keys.
|
8
|
+
* Add `Redis#exists?` to get a Boolean if any of the keys exists.
|
9
|
+
* `Redis#exists` when called with a single key will warn that future versions will return an Integer.
|
10
|
+
Set `Redis.exists_returns_integer = true` to opt-in to the new behavior.
|
11
|
+
* Support `keepttl` ooption in `set`. See #913.
|
12
|
+
* Optimized initialization of Redis::Cluster. See #912.
|
13
|
+
* Accept sentinel options even with string key. See #599.
|
14
|
+
* Verify TLS connections by default. See #900.
|
15
|
+
|
16
|
+
# 4.1.4
|
17
|
+
|
18
|
+
* Alias `Redis#disconnect` as `#close`. See #901.
|
19
|
+
* Handle clusters with multiple slot ranges. See #894.
|
20
|
+
* Fix password authentication to a redis cluster. See #889.
|
21
|
+
* Handle recursive MOVED responses. See #882.
|
22
|
+
* Increase buffer size in the ruby connector. See #880.
|
23
|
+
* Fix thread safety of `Redis.queue`. See #878.
|
24
|
+
* Deprecate `Redis::Future#==` as it's likely to be a mistake. See #876.
|
25
|
+
* Support `KEEPTTL` option for SET command. See #913.
|
26
|
+
|
27
|
+
# 4.1.3
|
4
28
|
|
5
29
|
* Fix the client hanging forever when connecting with SSL to a non-SSL server. See #835.
|
30
|
+
|
31
|
+
# 4.1.2
|
32
|
+
|
6
33
|
* Fix several authentication problems with sentinel. See #850 and #856.
|
7
34
|
* Explicitly drop Ruby 2.2 support.
|
8
35
|
|
data/README.md
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
-
# redis-rb [![Build Status][travis-image]][travis-link] [![Inline docs][inchpages-image]][inchpages-link]
|
1
|
+
# redis-rb [![Build Status][travis-image]][travis-link] [![Inline docs][inchpages-image]][inchpages-link] 
|
2
2
|
|
3
3
|
A Ruby client that tries to match [Redis][redis-home]' API one-to-one, while still
|
4
4
|
providing an idiomatic interface.
|
5
5
|
|
6
|
+
See [RubyDoc.info][rubydoc] for the API docs of the latest published gem.
|
6
7
|
|
7
8
|
## Getting started
|
8
9
|
|
@@ -34,6 +35,9 @@ You can also specify connection options as a [`redis://` URL][redis-url]:
|
|
34
35
|
redis = Redis.new(url: "redis://:p4ssw0rd@10.0.1.1:6380/15")
|
35
36
|
```
|
36
37
|
|
38
|
+
The client expects passwords with special chracters to be URL-encoded (i.e.
|
39
|
+
`CGI.escape(password)`).
|
40
|
+
|
37
41
|
By default, the client will try to read the `REDIS_URL` environment variable
|
38
42
|
and use that as URL to connect to. The above statement is therefore equivalent
|
39
43
|
to setting this environment variable and calling `Redis.new` without arguments.
|
@@ -142,12 +146,13 @@ redis.mget('{key}1', '{key}2')
|
|
142
146
|
```
|
143
147
|
|
144
148
|
* The client automatically reconnects after a failover occurred, but the caller is responsible for handling errors while it is happening.
|
149
|
+
* The client support permanent node failures, and will reroute requests to promoted slaves.
|
145
150
|
* The client supports `MOVED` and `ASK` redirections transparently.
|
146
151
|
|
147
152
|
## Storing objects
|
148
153
|
|
149
|
-
Redis
|
150
|
-
|
154
|
+
Redis "string" types can be used to store serialized Ruby objects, for
|
155
|
+
example with JSON:
|
151
156
|
|
152
157
|
```ruby
|
153
158
|
require "json"
|
@@ -321,7 +326,7 @@ This library supports natively terminating client side SSL/TLS connections
|
|
321
326
|
when talking to Redis via a server-side proxy such as [stunnel], [hitch],
|
322
327
|
or [ghostunnel].
|
323
328
|
|
324
|
-
To enable SSL support, pass the `:ssl =>
|
329
|
+
To enable SSL support, pass the `:ssl => true` option when configuring the
|
325
330
|
Redis client, or pass in `:url => "rediss://..."` (like HTTPS for Redis).
|
326
331
|
You will also need to pass in an `:ssl_params => { ... }` hash used to
|
327
332
|
configure the `OpenSSL::SSL::SSLContext` object used for the connection:
|
@@ -436,6 +441,10 @@ redis = Redis.new(:driver => :synchrony)
|
|
436
441
|
This library is tested against recent Ruby and Redis versions.
|
437
442
|
Check [Travis][travis-link] for the exact versions supported.
|
438
443
|
|
444
|
+
## See Also
|
445
|
+
|
446
|
+
- [async-redis](https://github.com/socketry/async-redis) — An [async](https://github.com/socketry/async) compatible Redis client.
|
447
|
+
|
439
448
|
## Contributors
|
440
449
|
|
441
450
|
Several people contributed to redis-rb, but we would like to especially
|
@@ -446,7 +455,7 @@ client and evangelized Redis in Rubyland. Thank you, Ezra.
|
|
446
455
|
## Contributing
|
447
456
|
|
448
457
|
[Fork the project](https://github.com/redis/redis-rb) and send pull
|
449
|
-
requests.
|
458
|
+
requests.
|
450
459
|
|
451
460
|
|
452
461
|
[inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg
|
data/lib/redis/client.rb
CHANGED
@@ -6,24 +6,25 @@ require "cgi"
|
|
6
6
|
|
7
7
|
class Redis
|
8
8
|
class Client
|
9
|
-
|
10
9
|
DEFAULTS = {
|
11
|
-
:
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:
|
15
|
-
:
|
16
|
-
:
|
17
|
-
:
|
18
|
-
:
|
19
|
-
:
|
20
|
-
:
|
21
|
-
:
|
22
|
-
:
|
23
|
-
:
|
24
|
-
:
|
25
|
-
:
|
26
|
-
|
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
|
27
28
|
|
28
29
|
attr_reader :options
|
29
30
|
|
@@ -89,7 +90,7 @@ class Redis
|
|
89
90
|
@pending_reads = 0
|
90
91
|
|
91
92
|
@connector =
|
92
|
-
if options.
|
93
|
+
if !@options[:sentinels].nil?
|
93
94
|
Connector::Sentinel.new(@options)
|
94
95
|
elsif options.include?(:connector) && options[:connector].respond_to?(:new)
|
95
96
|
options.delete(:connector).new(@options)
|
@@ -166,6 +167,7 @@ class Redis
|
|
166
167
|
end
|
167
168
|
rescue ConnectionError => e
|
168
169
|
return nil if pipeline.shutdown?
|
170
|
+
|
169
171
|
# Assume the pipeline was sent in one piece, but execution of
|
170
172
|
# SHUTDOWN caused none of the replies for commands that were executed
|
171
173
|
# prior to it from coming back around.
|
@@ -244,12 +246,13 @@ class Redis
|
|
244
246
|
end
|
245
247
|
|
246
248
|
def connected?
|
247
|
-
!!
|
249
|
+
!!(connection && connection.connected?)
|
248
250
|
end
|
249
251
|
|
250
252
|
def disconnect
|
251
253
|
connection.disconnect if connected?
|
252
254
|
end
|
255
|
+
alias close disconnect
|
253
256
|
|
254
257
|
def reconnect
|
255
258
|
disconnect
|
@@ -300,30 +303,27 @@ class Redis
|
|
300
303
|
with_socket_timeout(0, &blk)
|
301
304
|
end
|
302
305
|
|
303
|
-
def with_reconnect(val=true)
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
@reconnect = original
|
309
|
-
end
|
306
|
+
def with_reconnect(val = true)
|
307
|
+
original, @reconnect = @reconnect, val
|
308
|
+
yield
|
309
|
+
ensure
|
310
|
+
@reconnect = original
|
310
311
|
end
|
311
312
|
|
312
313
|
def without_reconnect(&blk)
|
313
314
|
with_reconnect(false, &blk)
|
314
315
|
end
|
315
316
|
|
316
|
-
|
317
|
+
protected
|
317
318
|
|
318
319
|
def logging(commands)
|
319
|
-
return yield unless @logger
|
320
|
+
return yield unless @logger&.debug?
|
320
321
|
|
321
322
|
begin
|
322
323
|
commands.each do |name, *args|
|
323
324
|
logged_args = args.map do |a|
|
324
|
-
|
325
|
-
|
326
|
-
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
|
327
327
|
else
|
328
328
|
# handle poorly-behaved descendants of BasicObject
|
329
329
|
klass = a.instance_exec { (class << self; self end).superclass }
|
@@ -357,9 +357,9 @@ class Redis
|
|
357
357
|
Errno::ENETUNREACH,
|
358
358
|
Errno::ENOENT,
|
359
359
|
Errno::ETIMEDOUT,
|
360
|
-
Errno::EINVAL
|
360
|
+
Errno::EINVAL => error
|
361
361
|
|
362
|
-
raise CannotConnectError, "Error connecting to Redis on #{location} (#{
|
362
|
+
raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
|
363
363
|
end
|
364
364
|
|
365
365
|
def ensure_connected
|
@@ -373,9 +373,9 @@ class Redis
|
|
373
373
|
if connected?
|
374
374
|
unless inherit_socket? || Process.pid == @pid
|
375
375
|
raise InheritedError,
|
376
|
-
|
377
|
-
|
378
|
-
|
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."
|
379
379
|
end
|
380
380
|
else
|
381
381
|
connect
|
@@ -386,7 +386,7 @@ class Redis
|
|
386
386
|
disconnect
|
387
387
|
|
388
388
|
if attempts <= @options[:reconnect_attempts] && @reconnect
|
389
|
-
sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
|
389
|
+
sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
|
390
390
|
@options[:reconnect_delay_max]].min
|
391
391
|
|
392
392
|
Kernel.sleep(sleep_t)
|
@@ -408,16 +408,14 @@ class Redis
|
|
408
408
|
|
409
409
|
defaults.keys.each do |key|
|
410
410
|
# Fill in defaults if needed
|
411
|
-
if defaults[key].respond_to?(:call)
|
412
|
-
defaults[key] = defaults[key].call
|
413
|
-
end
|
411
|
+
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
414
412
|
|
415
413
|
# Symbolize only keys that are needed
|
416
|
-
options[key] = options[key.to_s] if options.
|
414
|
+
options[key] = options[key.to_s] if options.key?(key.to_s)
|
417
415
|
end
|
418
416
|
|
419
417
|
url = options[:url]
|
420
|
-
url = defaults[:url] if url
|
418
|
+
url = defaults[:url] if url.nil?
|
421
419
|
|
422
420
|
# Override defaults from URL if given
|
423
421
|
if url
|
@@ -426,7 +424,7 @@ class Redis
|
|
426
424
|
uri = URI(url)
|
427
425
|
|
428
426
|
if uri.scheme == "unix"
|
429
|
-
defaults[:path]
|
427
|
+
defaults[:path] = uri.path
|
430
428
|
elsif uri.scheme == "redis" || uri.scheme == "rediss"
|
431
429
|
defaults[:scheme] = uri.scheme
|
432
430
|
defaults[:host] = uri.host if uri.host
|
@@ -457,7 +455,7 @@ class Redis
|
|
457
455
|
options[:port] = options[:port].to_i
|
458
456
|
end
|
459
457
|
|
460
|
-
if options.
|
458
|
+
if options.key?(:timeout)
|
461
459
|
options[:connect_timeout] ||= options[:timeout]
|
462
460
|
options[:read_timeout] ||= options[:timeout]
|
463
461
|
options[:write_timeout] ||= options[:timeout]
|
@@ -476,7 +474,7 @@ class Redis
|
|
476
474
|
|
477
475
|
case options[:tcp_keepalive]
|
478
476
|
when Hash
|
479
|
-
[
|
477
|
+
%i[time intvl probes].each do |key|
|
480
478
|
unless options[:tcp_keepalive][key].is_a?(Integer)
|
481
479
|
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
482
480
|
end
|
@@ -484,13 +482,13 @@ class Redis
|
|
484
482
|
|
485
483
|
when Integer
|
486
484
|
if options[:tcp_keepalive] >= 60
|
487
|
-
options[:tcp_keepalive] = {:
|
485
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
488
486
|
|
489
487
|
elsif options[:tcp_keepalive] >= 30
|
490
|
-
options[:tcp_keepalive] = {:
|
488
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
491
489
|
|
492
490
|
elsif options[:tcp_keepalive] >= 5
|
493
|
-
options[:tcp_keepalive] = {:
|
491
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
494
492
|
end
|
495
493
|
end
|
496
494
|
|
@@ -502,14 +500,14 @@ class Redis
|
|
502
500
|
def _parse_driver(driver)
|
503
501
|
driver = driver.to_s if driver.is_a?(Symbol)
|
504
502
|
|
505
|
-
if driver.
|
503
|
+
if driver.is_a?(String)
|
506
504
|
begin
|
507
505
|
require_relative "connection/#{driver}"
|
508
|
-
rescue LoadError, NameError
|
506
|
+
rescue LoadError, NameError
|
509
507
|
begin
|
510
508
|
require "connection/#{driver}"
|
511
|
-
rescue LoadError, NameError =>
|
512
|
-
raise
|
509
|
+
rescue LoadError, NameError => error
|
510
|
+
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
513
511
|
end
|
514
512
|
end
|
515
513
|
|
@@ -528,8 +526,7 @@ class Redis
|
|
528
526
|
@options
|
529
527
|
end
|
530
528
|
|
531
|
-
def check(client)
|
532
|
-
end
|
529
|
+
def check(client); end
|
533
530
|
|
534
531
|
class Sentinel < Connector
|
535
532
|
def initialize(options)
|
@@ -538,7 +535,7 @@ class Redis
|
|
538
535
|
@options[:db] = DEFAULTS.fetch(:db)
|
539
536
|
|
540
537
|
@sentinels = @options.delete(:sentinels).dup
|
541
|
-
@role = @options
|
538
|
+
@role = (@options[:role] || "master").to_s
|
542
539
|
@master = @options[:host]
|
543
540
|
end
|
544
541
|
|
@@ -561,13 +558,13 @@ class Redis
|
|
561
558
|
|
562
559
|
def resolve
|
563
560
|
result = case @role
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
561
|
+
when "master"
|
562
|
+
resolve_master
|
563
|
+
when "slave"
|
564
|
+
resolve_slave
|
565
|
+
else
|
566
|
+
raise ArgumentError, "Unknown instance role #{@role}"
|
567
|
+
end
|
571
568
|
|
572
569
|
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
573
570
|
end
|
@@ -575,11 +572,11 @@ class Redis
|
|
575
572
|
def sentinel_detect
|
576
573
|
@sentinels.each do |sentinel|
|
577
574
|
client = Client.new(@options.merge({
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
575
|
+
host: sentinel[:host] || sentinel["host"],
|
576
|
+
port: sentinel[:port] || sentinel["port"],
|
577
|
+
password: sentinel[:password] || sentinel["password"],
|
578
|
+
reconnect_attempts: 0
|
579
|
+
}))
|
583
580
|
|
584
581
|
begin
|
585
582
|
if result = yield(client)
|
@@ -601,7 +598,7 @@ class Redis
|
|
601
598
|
def resolve_master
|
602
599
|
sentinel_detect do |client|
|
603
600
|
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
604
|
-
{:
|
601
|
+
{ host: reply[0], port: reply[1] }
|
605
602
|
end
|
606
603
|
end
|
607
604
|
end
|
@@ -619,7 +616,7 @@ class Redis
|
|
619
616
|
slave = slaves.sample
|
620
617
|
{
|
621
618
|
host: slave.fetch('ip'),
|
622
|
-
port: slave.fetch('port')
|
619
|
+
port: slave.fetch('port')
|
623
620
|
}
|
624
621
|
end
|
625
622
|
end
|
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)
|
data/lib/redis/cluster/option.rb
CHANGED
@@ -15,36 +15,35 @@ 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, :password)
|
20
22
|
@options = options
|
21
23
|
end
|
22
24
|
|
23
25
|
def per_node_key
|
24
|
-
@
|
26
|
+
@node_opts.map { |opt| [NodeKey.build_from_host_port(opt[:host], opt[:port]), @options.merge(opt)] }
|
25
27
|
.to_h
|
26
28
|
end
|
27
29
|
|
28
|
-
def secure?
|
29
|
-
@node_uris.any? { |uri| uri.scheme == SECURE_SCHEME } || @options[:ssl_params] || false
|
30
|
-
end
|
31
|
-
|
32
30
|
def use_replica?
|
33
31
|
@replica
|
34
32
|
end
|
35
33
|
|
36
34
|
def update_node(addrs)
|
37
|
-
@
|
35
|
+
@node_opts = build_node_options(addrs)
|
38
36
|
end
|
39
37
|
|
40
38
|
def add_node(host, port)
|
41
|
-
@
|
39
|
+
@node_opts << { host: host, port: port }
|
42
40
|
end
|
43
41
|
|
44
42
|
private
|
45
43
|
|
46
|
-
def
|
44
|
+
def build_node_options(addrs)
|
47
45
|
raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
|
46
|
+
|
48
47
|
addrs.map { |addr| parse_node_addr(addr) }
|
49
48
|
end
|
50
49
|
|
@@ -53,7 +52,7 @@ class Redis
|
|
53
52
|
when String
|
54
53
|
parse_node_url(addr)
|
55
54
|
when Hash
|
56
|
-
|
55
|
+
parse_node_option(addr)
|
57
56
|
else
|
58
57
|
raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash'
|
59
58
|
end
|
@@ -62,15 +61,29 @@ class Redis
|
|
62
61
|
def parse_node_url(addr)
|
63
62
|
uri = URI(addr)
|
64
63
|
raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
|
65
|
-
|
64
|
+
|
65
|
+
db = uri.path.split('/')[1]&.to_i
|
66
|
+
{ scheme: uri.scheme, password: uri.password, host: uri.host, port: uri.port, db: db }.reject { |_, v| v.nil? }
|
66
67
|
rescue URI::InvalidURIError => err
|
67
68
|
raise InvalidClientOptionError, err.message
|
68
69
|
end
|
69
70
|
|
70
|
-
def
|
71
|
+
def parse_node_option(addr)
|
71
72
|
addr = addr.map { |k, v| [k.to_sym, v] }.to_h
|
72
|
-
|
73
|
-
|
73
|
+
if addr.values_at(:host, :port).any?(&:nil?)
|
74
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
|
75
|
+
end
|
76
|
+
|
77
|
+
addr
|
78
|
+
end
|
79
|
+
|
80
|
+
# Redis cluster node returns only host and port information.
|
81
|
+
# So we should complement additional information such as:
|
82
|
+
# scheme, password and so on.
|
83
|
+
def add_common_node_option_if_needed(options, node_opts, key)
|
84
|
+
return options if options[key].nil? && node_opts.first[key].nil?
|
85
|
+
|
86
|
+
options[key] ||= node_opts.first[key]
|
74
87
|
end
|
75
88
|
end
|
76
89
|
end
|
data/lib/redis/cluster/slot.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'set'
|
4
|
-
|
5
3
|
class Redis
|
6
4
|
class Cluster
|
7
5
|
# Keep slot and node key map for Redis Cluster Client
|
@@ -28,11 +26,20 @@ class Redis
|
|
28
26
|
return nil unless exists?(slot)
|
29
27
|
return find_node_key_of_master(slot) if replica_disabled?
|
30
28
|
|
31
|
-
@map[slot][:slaves].
|
29
|
+
@map[slot][:slaves].sample
|
32
30
|
end
|
33
31
|
|
34
32
|
def put(slot, node_key)
|
35
|
-
|
33
|
+
# Since we're sharing a hash for build_slot_node_key_map, duplicate it
|
34
|
+
# if it already exists instead of preserving as-is.
|
35
|
+
@map[slot] = @map[slot] ? @map[slot].dup : { master: nil, slaves: [] }
|
36
|
+
|
37
|
+
if master?(node_key)
|
38
|
+
@map[slot][:master] = node_key
|
39
|
+
elsif !@map[slot][:slaves].include?(node_key)
|
40
|
+
@map[slot][:slaves] << node_key
|
41
|
+
end
|
42
|
+
|
36
43
|
nil
|
37
44
|
end
|
38
45
|
|
@@ -50,19 +57,29 @@ class Redis
|
|
50
57
|
@node_flags[node_key] == ROLE_SLAVE
|
51
58
|
end
|
52
59
|
|
60
|
+
# available_slots is mapping of node_key to list of slot ranges
|
53
61
|
def build_slot_node_key_map(available_slots)
|
54
|
-
|
55
|
-
|
62
|
+
by_ranges = {}
|
63
|
+
available_slots.each do |node_key, slots_arr|
|
64
|
+
by_ranges[slots_arr] ||= { master: nil, slaves: [] }
|
65
|
+
|
66
|
+
if master?(node_key)
|
67
|
+
by_ranges[slots_arr][:master] = node_key
|
68
|
+
elsif !by_ranges[slots_arr][:slaves].include?(node_key)
|
69
|
+
by_ranges[slots_arr][:slaves] << node_key
|
70
|
+
end
|
56
71
|
end
|
57
|
-
end
|
58
72
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
73
|
+
by_slot = {}
|
74
|
+
by_ranges.each do |slots_arr, nodes|
|
75
|
+
slots_arr.each do |slots|
|
76
|
+
slots.each do |slot|
|
77
|
+
by_slot[slot] = nodes
|
78
|
+
end
|
79
|
+
end
|
65
80
|
end
|
81
|
+
|
82
|
+
by_slot
|
66
83
|
end
|
67
84
|
end
|
68
85
|
end
|
@@ -13,7 +13,7 @@ class Redis
|
|
13
13
|
info = {}
|
14
14
|
|
15
15
|
nodes.each do |node|
|
16
|
-
info =
|
16
|
+
info = fetch_slot_info(node)
|
17
17
|
info.empty? ? next : break
|
18
18
|
end
|
19
19
|
|
@@ -23,9 +23,10 @@ class Redis
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def fetch_slot_info(node)
|
26
|
+
hash_with_default_arr = Hash.new { |h, k| h[k] = [] }
|
26
27
|
node.call(%i[cluster slots])
|
27
|
-
.
|
28
|
-
.
|
28
|
+
.flat_map { |arr| parse_slot_info(arr, default_ip: node.host) }
|
29
|
+
.each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] }
|
29
30
|
rescue CannotConnectError, ConnectionError, CommandError
|
30
31
|
{} # can retry on another node
|
31
32
|
end
|
@@ -34,7 +35,6 @@ class Redis
|
|
34
35
|
first_slot, last_slot = arr[0..1]
|
35
36
|
slot_range = (first_slot..last_slot).freeze
|
36
37
|
arr[2..-1].map { |addr| [stringify_node_key(addr, default_ip), slot_range] }
|
37
|
-
.flatten
|
38
38
|
end
|
39
39
|
|
40
40
|
def stringify_node_key(arr, default_ip)
|