redis 4.4.0 → 4.8.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +96 -0
- data/README.md +25 -10
- data/lib/redis/client.rb +31 -25
- data/lib/redis/cluster/command.rb +4 -6
- data/lib/redis/cluster/command_loader.rb +8 -9
- data/lib/redis/cluster/node.rb +12 -0
- data/lib/redis/cluster/node_loader.rb +8 -11
- data/lib/redis/cluster/option.rb +10 -3
- data/lib/redis/cluster/slot_loader.rb +9 -12
- data/lib/redis/cluster.rb +24 -0
- 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 +455 -0
- data/lib/redis/commands/lists.rb +290 -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 +223 -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 +240 -0
- data/lib/redis/connection/command_helper.rb +2 -0
- data/lib/redis/connection/hiredis.rb +3 -2
- data/lib/redis/connection/ruby.rb +19 -9
- data/lib/redis/connection/synchrony.rb +10 -8
- data/lib/redis/connection.rb +1 -1
- data/lib/redis/distributed.rb +111 -23
- data/lib/redis/errors.rb +9 -0
- data/lib/redis/pipeline.rb +128 -3
- data/lib/redis/version.rb +1 -1
- data/lib/redis.rb +138 -3482
- metadata +22 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b18788ff80698e8f79fb103e7419d9ba74fb0b6a5eb55c672422cd76abff985c
|
4
|
+
data.tar.gz: 1eed18a57039c677c894564ceaa1cf6bc9c0535be51501d078b09d75617a9d89
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5f2f7ce595d431f548c126a63c5ce697cc9e596e9b0eaa00f1e0186ae3853e73922c6e130b989ba9e026aec32f766a774433fdd1cff216420e837837db90659b
|
7
|
+
data.tar.gz: 02fb8debb0d11f7b9d04f7616093535426f0ee5a0994992fcf3187ab00aa890dcd2fb248f14da49837508a3ccbb5756be208fc5d3b8dcd18ec6aaf0fb1bb1193
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,101 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
+
# 4.8.1
|
4
|
+
|
5
|
+
* Automatically reconnect after fork regardless of `reconnect_attempts`
|
6
|
+
|
7
|
+
# 4.8.0
|
8
|
+
|
9
|
+
* Introduce `sadd?` and `srem?` as boolean returning versions of `sadd` and `srem`.
|
10
|
+
* Deprecate `sadd` and `srem` returning a boolean when called with a single argument.
|
11
|
+
To enable the redis 5.0 behavior you can set `Redis.sadd_returns_boolean = false`.
|
12
|
+
* Deprecate passing `timeout` as a positional argument in blocking commands (`brpop`, `blop`, etc).
|
13
|
+
|
14
|
+
# 4.7.1
|
15
|
+
|
16
|
+
* Gracefully handle OpenSSL 3.0 EOF Errors (`OpenSSL::SSL::SSLError: SSL_read: unexpected eof while reading`). See #1106
|
17
|
+
This happens frequently on heroku-22.
|
18
|
+
|
19
|
+
# 4.7.0
|
20
|
+
|
21
|
+
* Support single endpoint architecture with SSL/TLS in cluster mode. See #1086.
|
22
|
+
* `zrem` and `zadd` act as noop when provided an empty list of keys. See #1097.
|
23
|
+
* Support IPv6 URLs.
|
24
|
+
* Add `Redis#with` for better compatibility with `connection_pool` usage.
|
25
|
+
* Fix the block form of `multi` called inside `pipelined`. Previously the `MUTLI/EXEC` wouldn't be sent. See #1073.
|
26
|
+
|
27
|
+
# 4.6.0
|
28
|
+
|
29
|
+
* Deprecate `Redis.current`.
|
30
|
+
* Deprecate calling commands on `Redis` inside `Redis#pipelined`. See #1059.
|
31
|
+
```ruby
|
32
|
+
redis.pipelined do
|
33
|
+
redis.get("key")
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
should be replaced by:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
redis.pipelined do |pipeline|
|
41
|
+
pipeline.get("key")
|
42
|
+
end
|
43
|
+
```
|
44
|
+
* Deprecate calling commands on `Redis` inside `Redis#multi`. See #1059.
|
45
|
+
```ruby
|
46
|
+
redis.multi do
|
47
|
+
redis.get("key")
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
should be replaced by:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
redis.multi do |transaction|
|
55
|
+
transaction.get("key")
|
56
|
+
end
|
57
|
+
```
|
58
|
+
* Deprecate `Redis#queue` and `Redis#commit`. See #1059.
|
59
|
+
|
60
|
+
* Fix `zpopmax` and `zpopmin` when called inside a pipeline. See #1055.
|
61
|
+
* `Redis#synchronize` is now private like it should always have been.
|
62
|
+
|
63
|
+
* Add `Redis.silence_deprecations=` to turn off deprecation warnings.
|
64
|
+
If you don't wish to see warnings yet, you can set `Redis.silence_deprecations = true`.
|
65
|
+
It is however heavily recommended to fix them instead when possible.
|
66
|
+
* Add `Redis.raise_deprecations=` to turn deprecation warnings into errors.
|
67
|
+
This makes it easier to identitify the source of deprecated APIs usage.
|
68
|
+
It is recommended to set `Redis.raise_deprecations = true` in development and test environments.
|
69
|
+
* Add new options to ZRANGE. See #1053.
|
70
|
+
* Add ZRANGESTORE command. See #1053.
|
71
|
+
* Add SCAN support for `Redis::Cluster`. See #1049.
|
72
|
+
* Add COPY command. See #1053. See #1048.
|
73
|
+
* Add ZDIFFSTORE command. See #1046.
|
74
|
+
* Add ZDIFF command. See #1044.
|
75
|
+
* Add ZUNION command. See #1042.
|
76
|
+
* Add HRANDFIELD command. See #1040.
|
77
|
+
|
78
|
+
# 4.5.1
|
79
|
+
|
80
|
+
* Restore the accidential auth behavior of redis-rb 4.3.0 with a warning. If provided with the `default` user's password, but a wrong username,
|
81
|
+
redis-rb will first try to connect as the provided user, but then will fallback to connect as the `default` user with the provided password.
|
82
|
+
This behavior is deprecated and will be removed in Redis 4.6.0. Fix #1038.
|
83
|
+
|
84
|
+
# 4.5.0
|
85
|
+
|
86
|
+
* Handle parts of the command using incompatible encodings. See #1037.
|
87
|
+
* Add GET option to SET command. See #1036.
|
88
|
+
* Add ZRANDMEMBER command. See #1035.
|
89
|
+
* Add LMOVE/BLMOVE commands. See #1034.
|
90
|
+
* Add ZMSCORE command. See #1032.
|
91
|
+
* Add LT/GT options to ZADD. See #1033.
|
92
|
+
* Add SMISMEMBER command. See #1031.
|
93
|
+
* Add EXAT/PXAT options to SET. See #1028.
|
94
|
+
* Add GETDEL/GETEX commands. See #1024.
|
95
|
+
* `Redis#exists` now returns an Integer by default, as warned since 4.2.0. The old behavior can be restored with `Redis.exists_returns_integer = false`.
|
96
|
+
* Fix Redis < 6 detection during connect. See #1025.
|
97
|
+
* Fix fetching command details in Redis cluster when the first node is unhealthy. See #1026.
|
98
|
+
|
3
99
|
# 4.4.0
|
4
100
|
|
5
101
|
* Redis cluster: fix cross-slot validation in pipelines. Fix ##1019.
|
data/README.md
CHANGED
@@ -155,6 +155,21 @@ redis.mget('{key}1', '{key}2')
|
|
155
155
|
* The client support permanent node failures, and will reroute requests to promoted slaves.
|
156
156
|
* The client supports `MOVED` and `ASK` redirections transparently.
|
157
157
|
|
158
|
+
## Cluster mode with SSL/TLS
|
159
|
+
Since Redis can return FQDN of nodes in reply to client since `7.*` with CLUSTER commands, we can use cluster feature with SSL/TLS connection like this:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
Redis.new(cluster: %w[rediss://foo.example.com:6379])
|
163
|
+
```
|
164
|
+
|
165
|
+
On the other hand, in Redis versions prior to `6.*`, you can specify options like the following if cluster mode is enabled and client has to connect to nodes via single endpoint with SSL/TLS.
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
Redis.new(cluster: %w[rediss://foo-endpoint.example.com:6379], fixed_hostname: 'foo-endpoint.example.com')
|
169
|
+
```
|
170
|
+
|
171
|
+
In case of the above architecture, if you don't pass the `fixed_hostname` option to the client and servers return IP addresses of nodes, the client may fail to verify certificates.
|
172
|
+
|
158
173
|
## Storing objects
|
159
174
|
|
160
175
|
Redis "string" types can be used to store serialized Ruby objects, for
|
@@ -184,9 +199,9 @@ commands to Redis and gathers their replies. These replies are returned
|
|
184
199
|
by the `#pipelined` method.
|
185
200
|
|
186
201
|
```ruby
|
187
|
-
redis.pipelined do
|
188
|
-
|
189
|
-
|
202
|
+
redis.pipelined do |pipeline|
|
203
|
+
pipeline.set "foo", "bar"
|
204
|
+
pipeline.incr "baz"
|
190
205
|
end
|
191
206
|
# => ["OK", 1]
|
192
207
|
```
|
@@ -200,9 +215,9 @@ the regular pipeline, the replies to the commands are returned by the
|
|
200
215
|
`#multi` method.
|
201
216
|
|
202
217
|
```ruby
|
203
|
-
redis.multi do
|
204
|
-
|
205
|
-
|
218
|
+
redis.multi do |transaction|
|
219
|
+
transaction.set "foo", "bar"
|
220
|
+
transaction.incr "baz"
|
206
221
|
end
|
207
222
|
# => ["OK", 1]
|
208
223
|
```
|
@@ -210,15 +225,15 @@ end
|
|
210
225
|
### Futures
|
211
226
|
|
212
227
|
Replies to commands in a pipeline can be accessed via the *futures* they
|
213
|
-
emit (since redis-rb 3.0). All calls
|
228
|
+
emit (since redis-rb 3.0). All calls on the pipeline object return a
|
214
229
|
`Future` object, which responds to the `#value` method. When the
|
215
230
|
pipeline has successfully executed, all futures are assigned their
|
216
231
|
respective replies and can be used.
|
217
232
|
|
218
233
|
```ruby
|
219
|
-
redis.pipelined do
|
220
|
-
@set =
|
221
|
-
@incr =
|
234
|
+
redis.pipelined do |pipeline|
|
235
|
+
@set = pipeline.set "foo", "bar"
|
236
|
+
@incr = pipeline.incr "baz"
|
222
237
|
end
|
223
238
|
|
224
239
|
@set.value
|
data/lib/redis/client.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
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
|
@@ -32,7 +32,7 @@ class Redis
|
|
32
32
|
role: nil
|
33
33
|
}.freeze
|
34
34
|
|
35
|
-
attr_reader :options
|
35
|
+
attr_reader :options, :connection, :command_map
|
36
36
|
|
37
37
|
def scheme
|
38
38
|
@options[:scheme]
|
@@ -87,8 +87,6 @@ class Redis
|
|
87
87
|
end
|
88
88
|
|
89
89
|
attr_accessor :logger
|
90
|
-
attr_reader :connection
|
91
|
-
attr_reader :command_map
|
92
90
|
|
93
91
|
def initialize(options = {})
|
94
92
|
@options = _parse_options(options)
|
@@ -119,8 +117,23 @@ class Redis
|
|
119
117
|
if username
|
120
118
|
begin
|
121
119
|
call [:auth, username, password]
|
122
|
-
rescue CommandError # Likely on Redis < 6
|
123
|
-
|
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
|
124
137
|
end
|
125
138
|
else
|
126
139
|
call [:auth, password]
|
@@ -137,7 +150,7 @@ class Redis
|
|
137
150
|
end
|
138
151
|
|
139
152
|
def id
|
140
|
-
@options[:id] || "
|
153
|
+
@options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
|
141
154
|
end
|
142
155
|
|
143
156
|
def location
|
@@ -238,7 +251,8 @@ class Redis
|
|
238
251
|
result
|
239
252
|
end
|
240
253
|
|
241
|
-
def call_with_timeout(command,
|
254
|
+
def call_with_timeout(command, extra_timeout, &blk)
|
255
|
+
timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout
|
242
256
|
with_socket_timeout(timeout) do
|
243
257
|
call(command, &blk)
|
244
258
|
end
|
@@ -288,7 +302,7 @@ class Redis
|
|
288
302
|
e2 = TimeoutError.new("Connection timed out")
|
289
303
|
e2.set_backtrace(e1.backtrace)
|
290
304
|
raise e2
|
291
|
-
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
|
292
306
|
raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
|
293
307
|
end
|
294
308
|
|
@@ -385,23 +399,14 @@ class Redis
|
|
385
399
|
end
|
386
400
|
|
387
401
|
def ensure_connected
|
388
|
-
disconnect if @pending_reads > 0
|
402
|
+
disconnect if @pending_reads > 0 || (@pid != Process.pid && !inherit_socket?)
|
389
403
|
|
390
404
|
attempts = 0
|
391
405
|
|
392
406
|
begin
|
393
407
|
attempts += 1
|
394
408
|
|
395
|
-
|
396
|
-
unless inherit_socket? || Process.pid == @pid
|
397
|
-
raise InheritedError,
|
398
|
-
"Tried to use a connection from a child process without reconnecting. " \
|
399
|
-
"You need to reconnect to Redis after forking " \
|
400
|
-
"or set :inherit_socket to true."
|
401
|
-
end
|
402
|
-
else
|
403
|
-
connect
|
404
|
-
end
|
409
|
+
connect unless connected?
|
405
410
|
|
406
411
|
yield
|
407
412
|
rescue BaseConnectionError
|
@@ -428,7 +433,7 @@ class Redis
|
|
428
433
|
defaults = DEFAULTS.dup
|
429
434
|
options = options.dup
|
430
435
|
|
431
|
-
defaults.
|
436
|
+
defaults.each_key do |key|
|
432
437
|
# Fill in defaults if needed
|
433
438
|
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
434
439
|
|
@@ -445,11 +450,12 @@ class Redis
|
|
445
450
|
|
446
451
|
uri = URI(url)
|
447
452
|
|
448
|
-
|
453
|
+
case uri.scheme
|
454
|
+
when "unix"
|
449
455
|
defaults[:path] = uri.path
|
450
|
-
|
456
|
+
when "redis", "rediss"
|
451
457
|
defaults[:scheme] = uri.scheme
|
452
|
-
defaults[:host] = uri.host if uri.host
|
458
|
+
defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
|
453
459
|
defaults[:port] = uri.port if uri.port
|
454
460
|
defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
|
455
461
|
defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
|
@@ -463,7 +469,7 @@ class Redis
|
|
463
469
|
end
|
464
470
|
|
465
471
|
# Use default when option is not specified or nil
|
466
|
-
defaults.
|
472
|
+
defaults.each_key do |key|
|
467
473
|
options[key] = defaults[key] if options[key].nil?
|
468
474
|
end
|
469
475
|
|
@@ -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
@@ -58,6 +58,18 @@ class Redis
|
|
58
58
|
try_map { |_, client| client.process(commands, &block) }.values
|
59
59
|
end
|
60
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
|
+
|
61
73
|
private
|
62
74
|
|
63
75
|
def replica_disabled?
|
@@ -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,6 +17,7 @@ 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)
|
21
22
|
add_common_node_option_if_needed(options, @node_opts, :username)
|
22
23
|
add_common_node_option_if_needed(options, @node_opts, :password)
|
@@ -24,8 +25,12 @@ class Redis
|
|
24
25
|
end
|
25
26
|
|
26
27
|
def per_node_key
|
27
|
-
@node_opts.map
|
28
|
-
|
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
|
29
34
|
end
|
30
35
|
|
31
36
|
def use_replica?
|
@@ -64,8 +69,10 @@ class Redis
|
|
64
69
|
raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
|
65
70
|
|
66
71
|
db = uri.path.split('/')[1]&.to_i
|
72
|
+
username = uri.user ? URI.decode_www_form_component(uri.user) : nil
|
73
|
+
password = uri.password ? URI.decode_www_form_component(uri.password) : nil
|
67
74
|
|
68
|
-
{ scheme: uri.scheme, username:
|
75
|
+
{ scheme: uri.scheme, username: username, password: password, host: uri.host, port: uri.port, db: db }
|
69
76
|
.reject { |_, v| v.nil? || v == '' }
|
70
77
|
rescue URI::InvalidURIError => err
|
71
78
|
raise InvalidClientOptionError, err.message
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require 'redis/errors'
|
4
|
+
require 'redis/cluster/node_key'
|
5
5
|
|
6
6
|
class Redis
|
7
7
|
class Cluster
|
@@ -10,16 +10,15 @@ 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_slot_info(node)
|
16
|
+
rescue CannotConnectError, ConnectionError, CommandError => error
|
17
|
+
error
|
18
|
+
end
|
18
19
|
end
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
|
21
|
+
raise InitialSetupError, errors
|
23
22
|
end
|
24
23
|
|
25
24
|
def fetch_slot_info(node)
|
@@ -27,8 +26,6 @@ class Redis
|
|
27
26
|
node.call(%i[cluster slots])
|
28
27
|
.flat_map { |arr| parse_slot_info(arr, default_ip: node.host) }
|
29
28
|
.each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] }
|
30
|
-
rescue CannotConnectError, ConnectionError, CommandError
|
31
|
-
{} # can retry on another node
|
32
29
|
end
|
33
30
|
|
34
31
|
def parse_slot_info(arr, default_ip:)
|
data/lib/redis/cluster.rb
CHANGED
@@ -137,6 +137,7 @@ class Redis
|
|
137
137
|
when 'wait' then @node.call_master(command, &block).reduce(:+)
|
138
138
|
when 'keys' then @node.call_slave(command, &block).flatten.sort
|
139
139
|
when 'dbsize' then @node.call_slave(command, &block).reduce(:+)
|
140
|
+
when 'scan' then _scan(command, &block)
|
140
141
|
when 'lastsave' then @node.call_all(command, &block).sort
|
141
142
|
when 'role' then @node.call_all(command, &block)
|
142
143
|
when 'config' then send_config_command(command, &block)
|
@@ -238,6 +239,29 @@ class Redis
|
|
238
239
|
raise
|
239
240
|
end
|
240
241
|
|
242
|
+
def _scan(command, &block)
|
243
|
+
input_cursor = Integer(command[1])
|
244
|
+
|
245
|
+
client_index = input_cursor % 256
|
246
|
+
raw_cursor = input_cursor >> 8
|
247
|
+
|
248
|
+
clients = @node.scale_reading_clients
|
249
|
+
|
250
|
+
client = clients[client_index]
|
251
|
+
return ['0', []] unless client
|
252
|
+
|
253
|
+
command[1] = raw_cursor.to_s
|
254
|
+
|
255
|
+
result_cursor, result_keys = client.call(command, &block)
|
256
|
+
result_cursor = Integer(result_cursor)
|
257
|
+
|
258
|
+
if result_cursor == 0
|
259
|
+
client_index += 1
|
260
|
+
end
|
261
|
+
|
262
|
+
[((result_cursor << 8) + client_index).to_s, result_keys]
|
263
|
+
end
|
264
|
+
|
241
265
|
def assign_redirection_node(err_msg)
|
242
266
|
_, slot, node_key = err_msg.split(' ')
|
243
267
|
slot = slot.to_i
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
module Commands
|
5
|
+
module Bitmaps
|
6
|
+
# Sets or clears the bit at offset in the string value stored at key.
|
7
|
+
#
|
8
|
+
# @param [String] key
|
9
|
+
# @param [Integer] offset bit offset
|
10
|
+
# @param [Integer] value bit value `0` or `1`
|
11
|
+
# @return [Integer] the original bit value stored at `offset`
|
12
|
+
def setbit(key, offset, value)
|
13
|
+
send_command([:setbit, key, offset, value])
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the bit value at offset in the string value stored at key.
|
17
|
+
#
|
18
|
+
# @param [String] key
|
19
|
+
# @param [Integer] offset bit offset
|
20
|
+
# @return [Integer] `0` or `1`
|
21
|
+
def getbit(key, offset)
|
22
|
+
send_command([:getbit, key, offset])
|
23
|
+
end
|
24
|
+
|
25
|
+
# Count the number of set bits in a range of the string value stored at key.
|
26
|
+
#
|
27
|
+
# @param [String] key
|
28
|
+
# @param [Integer] start start index
|
29
|
+
# @param [Integer] stop stop index
|
30
|
+
# @return [Integer] the number of bits set to 1
|
31
|
+
def bitcount(key, start = 0, stop = -1)
|
32
|
+
send_command([:bitcount, key, start, stop])
|
33
|
+
end
|
34
|
+
|
35
|
+
# Perform a bitwise operation between strings and store the resulting string in a key.
|
36
|
+
#
|
37
|
+
# @param [String] operation e.g. `and`, `or`, `xor`, `not`
|
38
|
+
# @param [String] destkey destination key
|
39
|
+
# @param [String, Array<String>] keys one or more source keys to perform `operation`
|
40
|
+
# @return [Integer] the length of the string stored in `destkey`
|
41
|
+
def bitop(operation, destkey, *keys)
|
42
|
+
send_command([:bitop, operation, destkey, *keys])
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return the position of the first bit set to 1 or 0 in a string.
|
46
|
+
#
|
47
|
+
# @param [String] key
|
48
|
+
# @param [Integer] bit whether to look for the first 1 or 0 bit
|
49
|
+
# @param [Integer] start start index
|
50
|
+
# @param [Integer] stop stop index
|
51
|
+
# @return [Integer] the position of the first 1/0 bit.
|
52
|
+
# -1 if looking for 1 and it is not found or start and stop are given.
|
53
|
+
def bitpos(key, bit, start = nil, stop = nil)
|
54
|
+
raise(ArgumentError, 'stop parameter specified without start parameter') if stop && !start
|
55
|
+
|
56
|
+
command = [:bitpos, key, bit]
|
57
|
+
command << start if start
|
58
|
+
command << stop if stop
|
59
|
+
send_command(command)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
module Commands
|
5
|
+
module Cluster
|
6
|
+
# Sends `CLUSTER *` command to random node and returns its reply.
|
7
|
+
#
|
8
|
+
# @see https://redis.io/commands#cluster Reference of cluster command
|
9
|
+
#
|
10
|
+
# @param subcommand [String, Symbol] the subcommand of cluster command
|
11
|
+
# e.g. `:slots`, `:nodes`, `:slaves`, `:info`
|
12
|
+
#
|
13
|
+
# @return [Object] depends on the subcommand
|
14
|
+
def cluster(subcommand, *args)
|
15
|
+
subcommand = subcommand.to_s.downcase
|
16
|
+
block = case subcommand
|
17
|
+
when 'slots'
|
18
|
+
HashifyClusterSlots
|
19
|
+
when 'nodes'
|
20
|
+
HashifyClusterNodes
|
21
|
+
when 'slaves'
|
22
|
+
HashifyClusterSlaves
|
23
|
+
when 'info'
|
24
|
+
HashifyInfo
|
25
|
+
else
|
26
|
+
Noop
|
27
|
+
end
|
28
|
+
|
29
|
+
# @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected
|
30
|
+
block = Noop unless @cluster_mode
|
31
|
+
|
32
|
+
send_command([:cluster, subcommand] + args, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Sends `ASKING` command to random node and returns its reply.
|
36
|
+
#
|
37
|
+
# @see https://redis.io/topics/cluster-spec#ask-redirection ASK redirection
|
38
|
+
#
|
39
|
+
# @return [String] `'OK'`
|
40
|
+
def asking
|
41
|
+
send_command(%i[asking])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|