redis 4.2.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 +4 -4
- data/CHANGELOG.md +114 -0
- data/README.md +27 -21
- data/lib/redis/client.rb +52 -13
- data/lib/redis/cluster/command.rb +4 -6
- data/lib/redis/cluster/command_loader.rb +6 -7
- data/lib/redis/cluster/node.rb +14 -1
- data/lib/redis/cluster/option.rb +5 -2
- data/lib/redis/cluster.rb +33 -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 +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 +2 -0
- data/lib/redis/connection/hiredis.rb +3 -2
- data/lib/redis/connection/ruby.rb +65 -56
- data/lib/redis/connection/synchrony.rb +10 -8
- data/lib/redis/connection.rb +1 -1
- data/lib/redis/distributed.rb +136 -23
- data/lib/redis/pipeline.rb +95 -2
- data/lib/redis/version.rb +1 -1
- data/lib/redis.rb +145 -3369
- metadata +30 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7e99f4e628112a227719d2000dc5b081893273cfbd51ae25bf00a8f6b6594061
|
4
|
+
data.tar.gz: 42a8e0cf75aebbc14cdf680b4bc1f7bbdec9e20b53f150c6443c01126960a959
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8bc57fe306c601f27d32df4940d5632e9e7d75529f6ac5d42732b21fe04452a4a1ab89567492c6fb7249ef4d69ebb2c0b7c985e18b0ef4f9ee6fb816c31bcbda
|
7
|
+
data.tar.gz: 42b2f8c584d6f96d0fc783d0b36af0fd9415d1dbd43ad8fe7a980688e52fbdb51645a07960d9cb3faacf6696c460cf38a013e290d0b58d4bbbb07626f18f15c7
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,118 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
+
# 4.6.0
|
4
|
+
|
5
|
+
* Deprecate `Redis.current`.
|
6
|
+
* Deprecate calling commands on `Redis` inside `Redis#pipelined`. See #1059.
|
7
|
+
```ruby
|
8
|
+
redis.pipelined do
|
9
|
+
redis.get("key")
|
10
|
+
end
|
11
|
+
```
|
12
|
+
|
13
|
+
should be replaced by:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
redis.pipelined do |pipeline|
|
17
|
+
pipeline.get("key")
|
18
|
+
end
|
19
|
+
```
|
20
|
+
* Deprecate calling commands on `Redis` inside `Redis#multi`. See #1059.
|
21
|
+
```ruby
|
22
|
+
redis.multi do
|
23
|
+
redis.get("key")
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
should be replaced by:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
redis.multi do |transaction|
|
31
|
+
transaction.get("key")
|
32
|
+
end
|
33
|
+
```
|
34
|
+
* Deprecate `Redis#queue` and `Redis#commit`. See #1059.
|
35
|
+
|
36
|
+
* Fix `zpopmax` and `zpopmin` when called inside a pipeline. See #1055.
|
37
|
+
* `Redis#synchronize` is now private like it should always have been.
|
38
|
+
|
39
|
+
* Add `Redis.silence_deprecations=` to turn off deprecation warnings.
|
40
|
+
If you don't wish to see warnings yet, you can set `Redis.silence_deprecations = false`.
|
41
|
+
It is however heavily recommended to fix them instead when possible.
|
42
|
+
* Add `Redis.raise_deprecations=` to turn deprecation warnings into errors.
|
43
|
+
This makes it easier to identitify the source of deprecated APIs usage.
|
44
|
+
It is recommended to set `Redis.raise_deprecations = true` in development and test environments.
|
45
|
+
* Add new options to ZRANGE. See #1053.
|
46
|
+
* Add ZRANGESTORE command. See #1053.
|
47
|
+
* Add SCAN support for `Redis::Cluster`. See #1049.
|
48
|
+
* Add COPY command. See #1053. See #1048.
|
49
|
+
* Add ZDIFFSTORE command. See #1046.
|
50
|
+
* Add ZDIFF command. See #1044.
|
51
|
+
* Add ZUNION command. See #1042.
|
52
|
+
* Add HRANDFIELD command. See #1040.
|
53
|
+
|
54
|
+
# 4.5.1
|
55
|
+
|
56
|
+
* 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,
|
57
|
+
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.
|
58
|
+
This behavior is deprecated and will be removed in Redis 4.6.0. Fix #1038.
|
59
|
+
|
60
|
+
# 4.5.0
|
61
|
+
|
62
|
+
* Handle parts of the command using incompatible encodings. See #1037.
|
63
|
+
* Add GET option to SET command. See #1036.
|
64
|
+
* Add ZRANDMEMBER command. See #1035.
|
65
|
+
* Add LMOVE/BLMOVE commands. See #1034.
|
66
|
+
* Add ZMSCORE command. See #1032.
|
67
|
+
* Add LT/GT options to ZADD. See #1033.
|
68
|
+
* Add SMISMEMBER command. See #1031.
|
69
|
+
* Add EXAT/PXAT options to SET. See #1028.
|
70
|
+
* Add GETDEL/GETEX commands. See #1024.
|
71
|
+
* `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`.
|
72
|
+
* Fix Redis < 6 detection during connect. See #1025.
|
73
|
+
* Fix fetching command details in Redis cluster when the first node is unhealthy. See #1026.
|
74
|
+
|
75
|
+
# 4.4.0
|
76
|
+
|
77
|
+
* Redis cluster: fix cross-slot validation in pipelines. Fix ##1019.
|
78
|
+
* Add support for `XAUTOCLAIM`. See #1018.
|
79
|
+
* Properly issue `READONLY` when reconnecting to replicas. Fix #1017.
|
80
|
+
* Make `del` a noop if passed an empty list of keys. See #998.
|
81
|
+
* Add support for `ZINTER`. See #995.
|
82
|
+
|
83
|
+
# 4.3.1
|
84
|
+
|
85
|
+
* Fix password authentication against redis server 5 and older.
|
86
|
+
|
87
|
+
# 4.3.0
|
88
|
+
|
89
|
+
* Add the TYPE argument to scan and scan_each. See #985.
|
90
|
+
* Support AUTH command for ACL. See #967.
|
91
|
+
|
92
|
+
# 4.2.5
|
93
|
+
|
94
|
+
* Optimize the ruby connector write buffering. See #964.
|
95
|
+
|
96
|
+
# 4.2.4
|
97
|
+
|
98
|
+
* Fix bytesize calculations in the ruby connector, and work on a copy of the buffer. Fix #961, #962.
|
99
|
+
|
100
|
+
# 4.2.3
|
101
|
+
|
102
|
+
* Use io/wait instead of IO.select in the ruby connector. See #960.
|
103
|
+
* Use exception free non blocking IOs in the ruby connector. See #926.
|
104
|
+
* Prevent corruption of the client when an interrupt happen during inside a pipeline block. See #945.
|
105
|
+
|
106
|
+
# 4.2.2
|
107
|
+
|
108
|
+
* Fix `WATCH` support for `Redis::Distributed`. See #941.
|
109
|
+
* Fix handling of empty stream responses. See #905, #929.
|
110
|
+
|
111
|
+
# 4.2.1
|
112
|
+
|
113
|
+
* Fix `exists?` returning an actual boolean when called with multiple keys. See #918.
|
114
|
+
* Setting `Redis.exists_returns_integer = false` disables warning message about new behaviour. See #920.
|
115
|
+
|
3
116
|
# 4.2.0
|
4
117
|
|
5
118
|
* Convert commands to accept keyword arguments rather than option hashes. This both help catching typos, and reduce needless allocations.
|
@@ -12,6 +125,7 @@
|
|
12
125
|
* Optimized initialization of Redis::Cluster. See #912.
|
13
126
|
* Accept sentinel options even with string key. See #599.
|
14
127
|
* Verify TLS connections by default. See #900.
|
128
|
+
* Make `Redis#hset` variadic. It now returns an integer, not a boolean. See #910.
|
15
129
|
|
16
130
|
# 4.1.4
|
17
131
|
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# redis-rb [![Build Status][
|
1
|
+
# redis-rb [![Build Status][gh-actions-image]][gh-actions-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.
|
@@ -54,6 +54,12 @@ To connect to a password protected Redis instance, use:
|
|
54
54
|
redis = Redis.new(password: "mysecret")
|
55
55
|
```
|
56
56
|
|
57
|
+
To connect a Redis instance using [ACL](https://redis.io/topics/acl), use:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
redis = Redis.new(username: 'myname', password: 'mysecret')
|
61
|
+
```
|
62
|
+
|
57
63
|
The Redis class exports methods that are named identical to the commands
|
58
64
|
they execute. The arguments these methods accept are often identical to
|
59
65
|
the arguments specified on the [Redis website][redis-commands]. For
|
@@ -178,9 +184,9 @@ commands to Redis and gathers their replies. These replies are returned
|
|
178
184
|
by the `#pipelined` method.
|
179
185
|
|
180
186
|
```ruby
|
181
|
-
redis.pipelined do
|
182
|
-
|
183
|
-
|
187
|
+
redis.pipelined do |pipeline|
|
188
|
+
pipeline.set "foo", "bar"
|
189
|
+
pipeline.incr "baz"
|
184
190
|
end
|
185
191
|
# => ["OK", 1]
|
186
192
|
```
|
@@ -194,9 +200,9 @@ the regular pipeline, the replies to the commands are returned by the
|
|
194
200
|
`#multi` method.
|
195
201
|
|
196
202
|
```ruby
|
197
|
-
redis.multi do
|
198
|
-
|
199
|
-
|
203
|
+
redis.multi do |transaction|
|
204
|
+
transaction.set "foo", "bar"
|
205
|
+
transaction.incr "baz"
|
200
206
|
end
|
201
207
|
# => ["OK", 1]
|
202
208
|
```
|
@@ -204,15 +210,15 @@ end
|
|
204
210
|
### Futures
|
205
211
|
|
206
212
|
Replies to commands in a pipeline can be accessed via the *futures* they
|
207
|
-
emit (since redis-rb 3.0). All calls
|
213
|
+
emit (since redis-rb 3.0). All calls on the pipeline object return a
|
208
214
|
`Future` object, which responds to the `#value` method. When the
|
209
215
|
pipeline has successfully executed, all futures are assigned their
|
210
216
|
respective replies and can be used.
|
211
217
|
|
212
218
|
```ruby
|
213
|
-
redis.pipelined do
|
214
|
-
@set =
|
215
|
-
@incr =
|
219
|
+
redis.pipelined do |pipeline|
|
220
|
+
@set = pipeline.set "foo", "bar"
|
221
|
+
@incr = pipeline.incr "baz"
|
216
222
|
end
|
217
223
|
|
218
224
|
@set.value
|
@@ -265,6 +271,7 @@ All timeout values are specified in seconds.
|
|
265
271
|
When using pub/sub, you can subscribe to a channel using a timeout as well:
|
266
272
|
|
267
273
|
```ruby
|
274
|
+
redis = Redis.new(reconnect_attempts: 0)
|
268
275
|
redis.subscribe_with_timeout(5, "news") do |on|
|
269
276
|
on.message do |channel, message|
|
270
277
|
# ...
|
@@ -439,7 +446,7 @@ redis = Redis.new(:driver => :synchrony)
|
|
439
446
|
## Testing
|
440
447
|
|
441
448
|
This library is tested against recent Ruby and Redis versions.
|
442
|
-
Check [
|
449
|
+
Check [Github Actions][gh-actions-link] for the exact versions supported.
|
443
450
|
|
444
451
|
## See Also
|
445
452
|
|
@@ -458,12 +465,11 @@ client and evangelized Redis in Rubyland. Thank you, Ezra.
|
|
458
465
|
requests.
|
459
466
|
|
460
467
|
|
461
|
-
[inchpages-image]:
|
462
|
-
[inchpages-link]:
|
463
|
-
[redis-commands]:
|
464
|
-
[redis-home]:
|
465
|
-
[redis-url]:
|
466
|
-
[
|
467
|
-
[
|
468
|
-
[
|
469
|
-
[rubydoc]: http://www.rubydoc.info/gems/redis
|
468
|
+
[inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg
|
469
|
+
[inchpages-link]: https://inch-ci.org/github/redis/redis-rb
|
470
|
+
[redis-commands]: https://redis.io/commands
|
471
|
+
[redis-home]: https://redis.io
|
472
|
+
[redis-url]: http://www.iana.org/assignments/uri-schemes/prov/redis
|
473
|
+
[gh-actions-image]: https://github.com/redis/redis-rb/workflows/Test/badge.svg
|
474
|
+
[gh-actions-link]: https://github.com/redis/redis-rb/actions
|
475
|
+
[rubydoc]: http://www.rubydoc.info/gems/redis
|
data/lib/redis/client.rb
CHANGED
@@ -1,18 +1,23 @@
|
|
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
|
+
# Defaults are also used for converting string keys to symbols.
|
9
10
|
DEFAULTS = {
|
10
11
|
url: -> { ENV["REDIS_URL"] },
|
11
12
|
scheme: "redis",
|
12
13
|
host: "127.0.0.1",
|
13
14
|
port: 6379,
|
14
15
|
path: nil,
|
16
|
+
read_timeout: nil,
|
17
|
+
write_timeout: nil,
|
18
|
+
connect_timeout: nil,
|
15
19
|
timeout: 5.0,
|
20
|
+
username: nil,
|
16
21
|
password: nil,
|
17
22
|
db: 0,
|
18
23
|
driver: nil,
|
@@ -22,11 +27,12 @@ class Redis
|
|
22
27
|
reconnect_delay: 0,
|
23
28
|
reconnect_delay_max: 0.5,
|
24
29
|
inherit_socket: false,
|
30
|
+
logger: nil,
|
25
31
|
sentinels: nil,
|
26
32
|
role: nil
|
27
33
|
}.freeze
|
28
34
|
|
29
|
-
attr_reader :options
|
35
|
+
attr_reader :options, :connection, :command_map
|
30
36
|
|
31
37
|
def scheme
|
32
38
|
@options[:scheme]
|
@@ -56,6 +62,10 @@ class Redis
|
|
56
62
|
@options[:read_timeout]
|
57
63
|
end
|
58
64
|
|
65
|
+
def username
|
66
|
+
@options[:username]
|
67
|
+
end
|
68
|
+
|
59
69
|
def password
|
60
70
|
@options[:password]
|
61
71
|
end
|
@@ -77,8 +87,6 @@ class Redis
|
|
77
87
|
end
|
78
88
|
|
79
89
|
attr_accessor :logger
|
80
|
-
attr_reader :connection
|
81
|
-
attr_reader :command_map
|
82
90
|
|
83
91
|
def initialize(options = {})
|
84
92
|
@options = _parse_options(options)
|
@@ -105,7 +113,34 @@ class Redis
|
|
105
113
|
# Don't try to reconnect when the connection is fresh
|
106
114
|
with_reconnect(false) do
|
107
115
|
establish_connection
|
108
|
-
|
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]
|
109
144
|
call [:select, db] if db != 0
|
110
145
|
call [:client, :setname, @options[:id]] if @options[:id]
|
111
146
|
@connector.check(self)
|
@@ -126,7 +161,7 @@ class Redis
|
|
126
161
|
reply = process([command]) { read }
|
127
162
|
raise reply if reply.is_a?(CommandError)
|
128
163
|
|
129
|
-
if block_given?
|
164
|
+
if block_given? && reply != 'QUEUED'
|
130
165
|
yield reply
|
131
166
|
else
|
132
167
|
reply
|
@@ -216,7 +251,8 @@ class Redis
|
|
216
251
|
result
|
217
252
|
end
|
218
253
|
|
219
|
-
def call_with_timeout(command,
|
254
|
+
def call_with_timeout(command, extra_timeout, &blk)
|
255
|
+
timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout
|
220
256
|
with_socket_timeout(timeout) do
|
221
257
|
call(command, &blk)
|
222
258
|
end
|
@@ -406,7 +442,7 @@ class Redis
|
|
406
442
|
defaults = DEFAULTS.dup
|
407
443
|
options = options.dup
|
408
444
|
|
409
|
-
defaults.
|
445
|
+
defaults.each_key do |key|
|
410
446
|
# Fill in defaults if needed
|
411
447
|
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
412
448
|
|
@@ -423,13 +459,15 @@ class Redis
|
|
423
459
|
|
424
460
|
uri = URI(url)
|
425
461
|
|
426
|
-
|
462
|
+
case uri.scheme
|
463
|
+
when "unix"
|
427
464
|
defaults[:path] = uri.path
|
428
|
-
|
465
|
+
when "redis", "rediss"
|
429
466
|
defaults[:scheme] = uri.scheme
|
430
467
|
defaults[:host] = uri.host if uri.host
|
431
468
|
defaults[:port] = uri.port if uri.port
|
432
|
-
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?
|
433
471
|
defaults[:db] = uri.path[1..-1].to_i if uri.path
|
434
472
|
defaults[:role] = :master
|
435
473
|
else
|
@@ -440,7 +478,7 @@ class Redis
|
|
440
478
|
end
|
441
479
|
|
442
480
|
# Use default when option is not specified or nil
|
443
|
-
defaults.
|
481
|
+
defaults.each_key do |key|
|
444
482
|
options[key] = defaults[key] if options[key].nil?
|
445
483
|
end
|
446
484
|
|
@@ -505,7 +543,7 @@ class Redis
|
|
505
543
|
require_relative "connection/#{driver}"
|
506
544
|
rescue LoadError, NameError
|
507
545
|
begin
|
508
|
-
require "connection/#{driver}"
|
546
|
+
require "redis/connection/#{driver}"
|
509
547
|
rescue LoadError, NameError => error
|
510
548
|
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
511
549
|
end
|
@@ -574,6 +612,7 @@ class Redis
|
|
574
612
|
client = Client.new(@options.merge({
|
575
613
|
host: sentinel[:host] || sentinel["host"],
|
576
614
|
port: sentinel[:port] || sentinel["port"],
|
615
|
+
username: sentinel[:username] || sentinel["username"],
|
577
616
|
password: sentinel[:password] || sentinel["password"],
|
578
617
|
reconnect_attempts: 0
|
579
618
|
}))
|
@@ -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
@@ -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?
|
@@ -76,8 +88,9 @@ class Redis
|
|
76
88
|
clients = options.map do |node_key, option|
|
77
89
|
next if replica_disabled? && slave?(node_key)
|
78
90
|
|
91
|
+
option = option.merge(readonly: true) if slave?(node_key)
|
92
|
+
|
79
93
|
client = Client.new(option)
|
80
|
-
client.call(%i[readonly]) if slave?(node_key)
|
81
94
|
[node_key, client]
|
82
95
|
end
|
83
96
|
|
data/lib/redis/cluster/option.rb
CHANGED
@@ -18,6 +18,7 @@ class Redis
|
|
18
18
|
@node_opts = build_node_options(node_addrs)
|
19
19
|
@replica = options.delete(:replica) == true
|
20
20
|
add_common_node_option_if_needed(options, @node_opts, :scheme)
|
21
|
+
add_common_node_option_if_needed(options, @node_opts, :username)
|
21
22
|
add_common_node_option_if_needed(options, @node_opts, :password)
|
22
23
|
@options = options
|
23
24
|
end
|
@@ -63,7 +64,9 @@ class Redis
|
|
63
64
|
raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
|
64
65
|
|
65
66
|
db = uri.path.split('/')[1]&.to_i
|
66
|
-
|
67
|
+
|
68
|
+
{ scheme: uri.scheme, username: uri.user, password: uri.password, host: uri.host, port: uri.port, db: db }
|
69
|
+
.reject { |_, v| v.nil? || v == '' }
|
67
70
|
rescue URI::InvalidURIError => err
|
68
71
|
raise InvalidClientOptionError, err.message
|
69
72
|
end
|
@@ -79,7 +82,7 @@ class Redis
|
|
79
82
|
|
80
83
|
# Redis cluster node returns only host and port information.
|
81
84
|
# So we should complement additional information such as:
|
82
|
-
# scheme, password and so on.
|
85
|
+
# scheme, username, password and so on.
|
83
86
|
def add_common_node_option_if_needed(options, node_opts, key)
|
84
87
|
return options if options[key].nil? && node_opts.first[key].nil?
|
85
88
|
|
data/lib/redis/cluster.rb
CHANGED
@@ -78,11 +78,13 @@ class Redis
|
|
78
78
|
end
|
79
79
|
|
80
80
|
def call_pipeline(pipeline)
|
81
|
-
node_keys
|
82
|
-
|
81
|
+
node_keys = pipeline.commands.map { |cmd| find_node_key(cmd, primary_only: true) }.compact.uniq
|
82
|
+
if node_keys.size > 1
|
83
|
+
raise(CrossSlotPipeliningError,
|
84
|
+
pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?).uniq)
|
85
|
+
end
|
83
86
|
|
84
|
-
|
85
|
-
try_send(node, :call_pipeline, pipeline)
|
87
|
+
try_send(find_node(node_keys.first), :call_pipeline, pipeline)
|
86
88
|
end
|
87
89
|
|
88
90
|
def call_with_timeout(command, timeout, &block)
|
@@ -128,13 +130,14 @@ class Redis
|
|
128
130
|
def send_command(command, &block)
|
129
131
|
cmd = command.first.to_s.downcase
|
130
132
|
case cmd
|
131
|
-
when 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
|
133
|
+
when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
|
132
134
|
@node.call_all(command, &block).first
|
133
135
|
when 'flushall', 'flushdb'
|
134
136
|
@node.call_master(command, &block).first
|
135
137
|
when 'wait' then @node.call_master(command, &block).reduce(:+)
|
136
138
|
when 'keys' then @node.call_slave(command, &block).flatten.sort
|
137
139
|
when 'dbsize' then @node.call_slave(command, &block).reduce(:+)
|
140
|
+
when 'scan' then _scan(command, &block)
|
138
141
|
when 'lastsave' then @node.call_all(command, &block).sort
|
139
142
|
when 'role' then @node.call_all(command, &block)
|
140
143
|
when 'config' then send_config_command(command, &block)
|
@@ -236,6 +239,29 @@ class Redis
|
|
236
239
|
raise
|
237
240
|
end
|
238
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
|
+
|
239
265
|
def assign_redirection_node(err_msg)
|
240
266
|
_, slot, node_key = err_msg.split(' ')
|
241
267
|
slot = slot.to_i
|
@@ -253,14 +279,14 @@ class Redis
|
|
253
279
|
find_node(node_key)
|
254
280
|
end
|
255
281
|
|
256
|
-
def find_node_key(command)
|
282
|
+
def find_node_key(command, primary_only: false)
|
257
283
|
key = @command.extract_first_key(command)
|
258
284
|
return if key.empty?
|
259
285
|
|
260
286
|
slot = KeySlotConverter.convert(key)
|
261
287
|
return unless @slot.exists?(slot)
|
262
288
|
|
263
|
-
if @command.should_send_to_master?(command)
|
289
|
+
if @command.should_send_to_master?(command) || primary_only
|
264
290
|
@slot.find_node_key_of_master(slot)
|
265
291
|
else
|
266
292
|
@slot.find_node_key_of_slave(slot)
|
@@ -285,11 +311,5 @@ class Redis
|
|
285
311
|
@node.map(&:disconnect)
|
286
312
|
@node, @slot = fetch_cluster_info!(@option)
|
287
313
|
end
|
288
|
-
|
289
|
-
def extract_keys_in_pipeline(pipeline)
|
290
|
-
node_keys = pipeline.commands.map { |cmd| find_node_key(cmd) }.compact.uniq
|
291
|
-
command_keys = pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?)
|
292
|
-
[node_keys, command_keys]
|
293
|
-
end
|
294
314
|
end
|
295
315
|
end
|
@@ -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
|