redis 4.6.0 → 4.7.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 +9 -1
- data/README.md +15 -0
- data/lib/redis/client.rb +2 -2
- data/lib/redis/cluster/command_loader.rb +5 -5
- 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/commands/sorted_sets.rb +9 -1
- data/lib/redis/commands/transactions.rb +47 -0
- data/lib/redis/connection/hiredis.rb +0 -2
- data/lib/redis/errors.rb +9 -0
- data/lib/redis/pipeline.rb +33 -1
- data/lib/redis/version.rb +1 -1
- data/lib/redis.rb +7 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5236e71d674779c964e99ebf2eac29efe80339a93de6907acfcd230a5d312153
|
4
|
+
data.tar.gz: 208b4bf708d70a570c354da2d5570972416cbe6007e1f8d99c31f3ef4958a342
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c9c6446c4b666435afcc66256a4387b0360da61cfbf941252d91561e893219d650a8cd1bf98e8a28825c258e543078e6d3b6e22cb790e2ac9ff42af915dd8ef0
|
7
|
+
data.tar.gz: e6649016acce9f6fd2e1767840ac3c07f5fc825576e5c304b4f1cb0b7a2e1c8ce8043450de75a1c910098a47f5bf53230b4efd74b7590b17f5022b04f0e23583
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
+
# 4.7.0
|
4
|
+
|
5
|
+
* Support single endpoint architecture with SSL/TLS in cluster mode. See #1086.
|
6
|
+
* `zrem` and `zadd` act as noop when provided an empty list of keys. See #1097.
|
7
|
+
* Support IPv6 URLs.
|
8
|
+
* Add `Redis#with` for better compatibility with `connection_pool` usage.
|
9
|
+
* Fix the block form of `multi` called inside `pipelined`. Previously the `MUTLI/EXEC` wouldn't be sent. See #1073.
|
10
|
+
|
3
11
|
# 4.6.0
|
4
12
|
|
5
13
|
* Deprecate `Redis.current`.
|
@@ -37,7 +45,7 @@
|
|
37
45
|
* `Redis#synchronize` is now private like it should always have been.
|
38
46
|
|
39
47
|
* 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 =
|
48
|
+
If you don't wish to see warnings yet, you can set `Redis.silence_deprecations = true`.
|
41
49
|
It is however heavily recommended to fix them instead when possible.
|
42
50
|
* Add `Redis.raise_deprecations=` to turn deprecation warnings into errors.
|
43
51
|
This makes it easier to identitify the source of deprecated APIs usage.
|
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
|
data/lib/redis/client.rb
CHANGED
@@ -150,7 +150,7 @@ class Redis
|
|
150
150
|
end
|
151
151
|
|
152
152
|
def id
|
153
|
-
@options[:id] || "
|
153
|
+
@options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
|
154
154
|
end
|
155
155
|
|
156
156
|
def location
|
@@ -464,7 +464,7 @@ class Redis
|
|
464
464
|
defaults[:path] = uri.path
|
465
465
|
when "redis", "rediss"
|
466
466
|
defaults[:scheme] = uri.scheme
|
467
|
-
defaults[:host] = uri.host if uri.host
|
467
|
+
defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
|
468
468
|
defaults[:port] = uri.port if uri.port
|
469
469
|
defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
|
470
470
|
defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
|
@@ -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,15 +10,15 @@ class Redis
|
|
10
10
|
module_function
|
11
11
|
|
12
12
|
def load(nodes)
|
13
|
-
nodes.
|
13
|
+
errors = nodes.map do |node|
|
14
14
|
begin
|
15
15
|
return fetch_command_details(node)
|
16
|
-
rescue CannotConnectError, ConnectionError, CommandError
|
17
|
-
|
16
|
+
rescue CannotConnectError, ConnectionError, CommandError => error
|
17
|
+
error
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
raise
|
21
|
+
raise InitialSetupError, errors
|
22
22
|
end
|
23
23
|
|
24
24
|
def fetch_command_details(node)
|
@@ -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:)
|
@@ -60,8 +60,11 @@ class Redis
|
|
60
60
|
command << "INCR" if incr
|
61
61
|
|
62
62
|
if args.size == 1 && args[0].is_a?(Array)
|
63
|
+
members_to_add = args[0]
|
64
|
+
return 0 if members_to_add.empty?
|
65
|
+
|
63
66
|
# Variadic: return float if INCR, integer if !INCR
|
64
|
-
send_command(command +
|
67
|
+
send_command(command + members_to_add, &(incr ? Floatify : nil))
|
65
68
|
elsif args.size == 2
|
66
69
|
# Single pair: return float if INCR, boolean if !INCR
|
67
70
|
send_command(command + args, &(incr ? Floatify : Boolify))
|
@@ -102,6 +105,11 @@ class Redis
|
|
102
105
|
# - `Integer` when an array of pairs is specified, holding the number of
|
103
106
|
# members that were removed to the sorted set
|
104
107
|
def zrem(key, member)
|
108
|
+
if member.is_a?(Array)
|
109
|
+
members_to_remove = member
|
110
|
+
return 0 if members_to_remove.empty?
|
111
|
+
end
|
112
|
+
|
105
113
|
send_command([:zrem, key, member]) do |reply|
|
106
114
|
if member.is_a? Array
|
107
115
|
# Variadic: return integer
|
@@ -3,6 +3,53 @@
|
|
3
3
|
class Redis
|
4
4
|
module Commands
|
5
5
|
module Transactions
|
6
|
+
# Mark the start of a transaction block.
|
7
|
+
#
|
8
|
+
# Passing a block is optional.
|
9
|
+
#
|
10
|
+
# @example With a block
|
11
|
+
# redis.multi do |multi|
|
12
|
+
# multi.set("key", "value")
|
13
|
+
# multi.incr("counter")
|
14
|
+
# end # => ["OK", 6]
|
15
|
+
#
|
16
|
+
# @example Without a block
|
17
|
+
# redis.multi
|
18
|
+
# # => "OK"
|
19
|
+
# redis.set("key", "value")
|
20
|
+
# # => "QUEUED"
|
21
|
+
# redis.incr("counter")
|
22
|
+
# # => "QUEUED"
|
23
|
+
# redis.exec
|
24
|
+
# # => ["OK", 6]
|
25
|
+
#
|
26
|
+
# @yield [multi] the commands that are called inside this block are cached
|
27
|
+
# and written to the server upon returning from it
|
28
|
+
# @yieldparam [Redis] multi `self`
|
29
|
+
#
|
30
|
+
# @return [String, Array<...>]
|
31
|
+
# - when a block is not given, `OK`
|
32
|
+
# - when a block is given, an array with replies
|
33
|
+
#
|
34
|
+
# @see #watch
|
35
|
+
# @see #unwatch
|
36
|
+
def multi(&block) # :nodoc:
|
37
|
+
if block_given?
|
38
|
+
if block&.arity == 0
|
39
|
+
Pipeline.deprecation_warning("multi", Kernel.caller_locations(1, 5))
|
40
|
+
end
|
41
|
+
|
42
|
+
synchronize do |prior_client|
|
43
|
+
pipeline = Pipeline::Multi.new(prior_client)
|
44
|
+
pipelined_connection = PipelinedConnection.new(pipeline)
|
45
|
+
yield pipelined_connection
|
46
|
+
prior_client.call_pipeline(pipeline)
|
47
|
+
end
|
48
|
+
else
|
49
|
+
send_command([:multi])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
6
53
|
# Watch the given keys to determine execution of the MULTI/EXEC block.
|
7
54
|
#
|
8
55
|
# Using a block is optional, but is necessary for thread-safety.
|
@@ -15,8 +15,6 @@ class Redis
|
|
15
15
|
|
16
16
|
if config[:scheme] == "unix"
|
17
17
|
connection.connect_unix(config[:path], connect_timeout)
|
18
|
-
elsif config[:scheme] == "rediss" || config[:ssl]
|
19
|
-
raise NotImplementedError, "SSL not supported by hiredis driver"
|
20
18
|
else
|
21
19
|
connection.connect(config[:host], config[:port], connect_timeout)
|
22
20
|
end
|
data/lib/redis/errors.rb
CHANGED
@@ -45,6 +45,15 @@ class Redis
|
|
45
45
|
end
|
46
46
|
|
47
47
|
class Cluster
|
48
|
+
# Raised when client connected to redis as cluster mode
|
49
|
+
# and failed to fetch cluster state information by commands.
|
50
|
+
class InitialSetupError < BaseError
|
51
|
+
# @param errors [Array<Redis::BaseError>]
|
52
|
+
def initialize(errors)
|
53
|
+
super("Redis client could not fetch cluster information: #{errors.map(&:message).uniq.join(',')}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
48
57
|
# Raised when client connected to redis as cluster mode
|
49
58
|
# and some cluster subcommands were called.
|
50
59
|
class OrchestrationCommandNotSupported < BaseError
|
data/lib/redis/pipeline.rb
CHANGED
@@ -22,6 +22,11 @@ class Redis
|
|
22
22
|
yield self
|
23
23
|
end
|
24
24
|
|
25
|
+
def call_pipeline(pipeline)
|
26
|
+
@pipeline.call_pipeline(pipeline)
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
25
30
|
private
|
26
31
|
|
27
32
|
def synchronize
|
@@ -69,6 +74,7 @@ class Redis
|
|
69
74
|
attr_reader :client
|
70
75
|
|
71
76
|
attr :futures
|
77
|
+
alias materialized_futures futures
|
72
78
|
|
73
79
|
def initialize(client)
|
74
80
|
@client = client.is_a?(Pipeline) ? client.client : client
|
@@ -112,7 +118,7 @@ class Redis
|
|
112
118
|
|
113
119
|
def call_pipeline(pipeline)
|
114
120
|
@shutdown = true if pipeline.shutdown?
|
115
|
-
@futures.concat(pipeline.
|
121
|
+
@futures.concat(pipeline.materialized_futures)
|
116
122
|
@db = pipeline.db
|
117
123
|
nil
|
118
124
|
end
|
@@ -169,6 +175,18 @@ class Redis
|
|
169
175
|
end
|
170
176
|
end
|
171
177
|
|
178
|
+
def materialized_futures
|
179
|
+
if empty?
|
180
|
+
[]
|
181
|
+
else
|
182
|
+
[
|
183
|
+
Future.new([:multi], nil, 0),
|
184
|
+
*futures,
|
185
|
+
MultiFuture.new(futures)
|
186
|
+
]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
172
190
|
def timeouts
|
173
191
|
if empty?
|
174
192
|
[]
|
@@ -271,4 +289,18 @@ class Redis
|
|
271
289
|
Future
|
272
290
|
end
|
273
291
|
end
|
292
|
+
|
293
|
+
class MultiFuture < Future
|
294
|
+
def initialize(futures)
|
295
|
+
@futures = futures
|
296
|
+
@command = [:exec]
|
297
|
+
end
|
298
|
+
|
299
|
+
def _set(replies)
|
300
|
+
@futures.each_with_index do |future, index|
|
301
|
+
future._set(replies[index])
|
302
|
+
end
|
303
|
+
replies
|
304
|
+
end
|
305
|
+
end
|
274
306
|
end
|
data/lib/redis/version.rb
CHANGED
data/lib/redis.rb
CHANGED
@@ -37,7 +37,7 @@ class Redis
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def current
|
40
|
-
deprecate!("`Redis.current
|
40
|
+
deprecate!("`Redis.current` is deprecated and will be removed in 5.0. (called from: #{caller(1, 1).first})")
|
41
41
|
@current ||= Redis.new
|
42
42
|
end
|
43
43
|
|
@@ -74,6 +74,8 @@ class Redis
|
|
74
74
|
# @option options [Symbol] :role (:master) Role to fetch via Sentinel, either `:master` or `:slave`
|
75
75
|
# @option options [Array<String, Hash{Symbol => String, Integer}>] :cluster List of cluster nodes to contact
|
76
76
|
# @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not
|
77
|
+
# @option options [String] :fixed_hostname Specify a FQDN if cluster mode enabled and
|
78
|
+
# client has to connect nodes via single endpoint with SSL/TLS
|
77
79
|
# @option options [Class] :connector Class of custom connector
|
78
80
|
#
|
79
81
|
# @return [Redis] a new client instance
|
@@ -109,6 +111,10 @@ class Redis
|
|
109
111
|
end
|
110
112
|
alias disconnect! close
|
111
113
|
|
114
|
+
def with
|
115
|
+
yield self
|
116
|
+
end
|
117
|
+
|
112
118
|
# @deprecated Queues a command for pipelining.
|
113
119
|
#
|
114
120
|
# Commands in the queue are executed with the Redis#commit method.
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ezra Zygmuntowicz
|
@@ -16,7 +16,7 @@ authors:
|
|
16
16
|
autorequire:
|
17
17
|
bindir: bin
|
18
18
|
cert_chain: []
|
19
|
-
date: 2022-
|
19
|
+
date: 2022-06-24 00:00:00.000000000 Z
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
22
|
name: em-synchrony
|
@@ -119,9 +119,9 @@ licenses:
|
|
119
119
|
metadata:
|
120
120
|
bug_tracker_uri: https://github.com/redis/redis-rb/issues
|
121
121
|
changelog_uri: https://github.com/redis/redis-rb/blob/master/CHANGELOG.md
|
122
|
-
documentation_uri: https://www.rubydoc.info/gems/redis/4.
|
122
|
+
documentation_uri: https://www.rubydoc.info/gems/redis/4.7.0
|
123
123
|
homepage_uri: https://github.com/redis/redis-rb
|
124
|
-
source_code_uri: https://github.com/redis/redis-rb/tree/v4.
|
124
|
+
source_code_uri: https://github.com/redis/redis-rb/tree/v4.7.0
|
125
125
|
post_install_message:
|
126
126
|
rdoc_options: []
|
127
127
|
require_paths:
|