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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e99f4e628112a227719d2000dc5b081893273cfbd51ae25bf00a8f6b6594061
4
- data.tar.gz: 42a8e0cf75aebbc14cdf680b4bc1f7bbdec9e20b53f150c6443c01126960a959
3
+ metadata.gz: 5236e71d674779c964e99ebf2eac29efe80339a93de6907acfcd230a5d312153
4
+ data.tar.gz: 208b4bf708d70a570c354da2d5570972416cbe6007e1f8d99c31f3ef4958a342
5
5
  SHA512:
6
- metadata.gz: 8bc57fe306c601f27d32df4940d5632e9e7d75529f6ac5d42732b21fe04452a4a1ab89567492c6fb7249ef4d69ebb2c0b7c985e18b0ef4f9ee6fb816c31bcbda
7
- data.tar.gz: 42b2f8c584d6f96d0fc783d0b36af0fd9415d1dbd43ad8fe7a980688e52fbdb51645a07960d9cb3faacf6696c460cf38a013e290d0b58d4bbbb07626f18f15c7
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 = false`.
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] || "redis://#{location}/#{db}"
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
- require_relative '../errors'
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.each do |node|
13
+ errors = nodes.map do |node|
14
14
  begin
15
15
  return fetch_command_details(node)
16
- rescue CannotConnectError, ConnectionError, CommandError
17
- next # can retry on another node
16
+ rescue CannotConnectError, ConnectionError, CommandError => error
17
+ error
18
18
  end
19
19
  end
20
20
 
21
- raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
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
- require_relative '../errors'
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
- info = {}
13
-
14
- nodes.each do |node|
15
- info = fetch_node_info(node)
16
- info.empty? ? next : break
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
- return info unless info.empty?
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
@@ -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 { |opt| [NodeKey.build_from_host_port(opt[:host], opt[:port]), @options.merge(opt)] }
28
- .to_h
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: uri.user, password: uri.password, host: uri.host, port: uri.port, db: db }
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
- require_relative '../errors'
4
- require_relative 'node_key'
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
- info = {}
14
-
15
- nodes.each do |node|
16
- info = fetch_slot_info(node)
17
- info.empty? ? next : break
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
- return info unless info.empty?
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 + args[0], &(incr ? Floatify : nil))
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
@@ -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.futures)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Redis
4
- VERSION = '4.6.0'
4
+ VERSION = '4.7.0'
5
5
  end
data/lib/redis.rb CHANGED
@@ -37,7 +37,7 @@ class Redis
37
37
  end
38
38
 
39
39
  def current
40
- deprecate!("`Redis.current=` is deprecated and will be removed in 5.0. (called from: #{caller(1, 1).first})")
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.6.0
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-02-02 00:00:00.000000000 Z
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.6.0
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.6.0
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: