redis 4.2.0 → 4.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|