redis 4.6.0 → 4.7.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e99f4e628112a227719d2000dc5b081893273cfbd51ae25bf00a8f6b6594061
4
- data.tar.gz: 42a8e0cf75aebbc14cdf680b4bc1f7bbdec9e20b53f150c6443c01126960a959
3
+ metadata.gz: 185ccbe36adc1e1561474af65d0e0b8d246e9c161757c9218f5ac7b6c1d24bb9
4
+ data.tar.gz: 03a4203747503b971551500ea40f996cb9620e5d9a693231d7a63944a9f73bf8
5
5
  SHA512:
6
- metadata.gz: 8bc57fe306c601f27d32df4940d5632e9e7d75529f6ac5d42732b21fe04452a4a1ab89567492c6fb7249ef4d69ebb2c0b7c985e18b0ef4f9ee6fb816c31bcbda
7
- data.tar.gz: 42b2f8c584d6f96d0fc783d0b36af0fd9415d1dbd43ad8fe7a980688e52fbdb51645a07960d9cb3faacf6696c460cf38a013e290d0b58d4bbbb07626f18f15c7
6
+ metadata.gz: 3354c910dcf3feb76b7f4ed7adb5bcce4f1cfc3ef7c58347897ba4705337a89052d2db67a8a71765f34e16f01a7ab2c8894bba60a3a89e1f36b6487f3fe0089b
7
+ data.tar.gz: 0b23396f9ecf62dd952536b94107a7748bb5ee225e7c7ac1da36e813e7fffc588c5939abe1800fc440e173acc54a9ac3649037b62b3ff59aa946ffc9a3e11180
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Unreleased
2
2
 
3
+ # 4.7.1
4
+
5
+ * Gracefully handle OpenSSL 3.0 EOF Errors (`OpenSSL::SSL::SSLError: SSL_read: unexpected eof while reading`). See #1106
6
+ This happens frequently on heroku-22.
7
+
8
+ # 4.7.0
9
+
10
+ * Support single endpoint architecture with SSL/TLS in cluster mode. See #1086.
11
+ * `zrem` and `zadd` act as noop when provided an empty list of keys. See #1097.
12
+ * Support IPv6 URLs.
13
+ * Add `Redis#with` for better compatibility with `connection_pool` usage.
14
+ * Fix the block form of `multi` called inside `pipelined`. Previously the `MUTLI/EXEC` wouldn't be sent. See #1073.
15
+
3
16
  # 4.6.0
4
17
 
5
18
  * Deprecate `Redis.current`.
@@ -37,7 +50,7 @@
37
50
  * `Redis#synchronize` is now private like it should always have been.
38
51
 
39
52
  * 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`.
53
+ If you don't wish to see warnings yet, you can set `Redis.silence_deprecations = true`.
41
54
  It is however heavily recommended to fix them instead when possible.
42
55
  * Add `Redis.raise_deprecations=` to turn deprecation warnings into errors.
43
56
  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
@@ -302,7 +302,7 @@ class Redis
302
302
  e2 = TimeoutError.new("Connection timed out")
303
303
  e2.set_backtrace(e1.backtrace)
304
304
  raise e2
305
- 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
306
306
  raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
307
307
  end
308
308
 
@@ -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
@@ -384,6 +384,12 @@ class Redis
384
384
  format_reply(reply_type, line)
385
385
  rescue Errno::EAGAIN
386
386
  raise TimeoutError
387
+ rescue OpenSSL::SSL::SSLError => ssl_error
388
+ if ssl_error.message.match?(/SSL_read: unexpected eof while reading/i)
389
+ raise EOFError, ssl_error.message
390
+ else
391
+ raise
392
+ end
387
393
  end
388
394
 
389
395
  def format_reply(reply_type, line)
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.1'
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.1
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-07-01 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.1
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.1
125
125
  post_install_message:
126
126
  rdoc_options: []
127
127
  require_paths: