redis 5.4.1 → 6.0.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.
@@ -80,7 +80,7 @@ class Redis
80
80
  end
81
81
 
82
82
  class Subscription
83
- attr :callbacks
83
+ attr_reader :callbacks
84
84
 
85
85
  def initialize
86
86
  @callbacks = {}
data/lib/redis/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Redis
4
- VERSION = '5.4.1'
4
+ VERSION = '6.0.0'
5
5
  end
data/lib/redis.rb CHANGED
@@ -53,8 +53,14 @@ class Redis
53
53
  # @option options [String] :password Password to authenticate against server
54
54
  # @option options [Integer] :db (0) Database to select after connect and on reconnects
55
55
  # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis`
56
+ # @option options [Integer] :protocol (3) RESP protocol version to negotiate (`HELLO`). Defaults
57
+ # to RESP3; set to `2` for RESP2. Servers without RESP3 support automatically fall back to RESP2.
56
58
  # @option options [String] :id ID for the client connection, assigns name to current connection by sending
57
59
  # `CLIENT SETNAME`
60
+ # @option options [String, Array<String>, false] :driver_info Identity a library built on top of `redis-rb`
61
+ # reports to the server via `CLIENT SETINFO`, shown as `lib-name=redis-rb(<driver_info>)` in `CLIENT LIST`.
62
+ # The recommended format is `<name>_v<version>`; an Array is joined with `;`. Pass `false` to disable
63
+ # client identification entirely.
58
64
  # @option options [Integer, Array<Integer, Float>] :reconnect_attempts Number of attempts trying to connect,
59
65
  # or a list of sleep duration between attempts.
60
66
  # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not
@@ -69,16 +75,29 @@ class Redis
69
75
  if ENV["REDIS_URL"] && SERVER_URL_OPTIONS.none? { |o| @options.key?(o) }
70
76
  @options[:url] = ENV["REDIS_URL"]
71
77
  end
72
- inherit_socket = @options.delete(:inherit_socket)
78
+ # Kept as state, not just a local: the RESP3->RESP2 fallback rebuilds @client and must re-apply
79
+ # socket inheritance, otherwise fork safety would be silently lost after a downgrade.
80
+ @inherit_socket = @options.delete(:inherit_socket)
81
+ # HIMPORT fieldsets are server session state that dies with the physical connection; the
82
+ # registry remembers each prepared schema so a lost session can be repaired (see the
83
+ # himport_* overrides below). Deleted from @options so it never reaches RedisClient::Config.
84
+ @himport_auto_prepare = @options.delete(:himport_auto_prepare) != false
85
+ @himport_fieldsets = {}
73
86
  @subscription_client = nil
74
87
 
75
- @client = initialize_client(@options)
76
- @client.inherit_socket! if inherit_socket
88
+ @client = build_client
77
89
  end
78
90
 
79
91
  # Run code without the client reconnecting
80
92
  def without_reconnect(&block)
81
- @client.disable_reconnection(&block)
93
+ # Route through #synchronize like every other @client access: it holds @monitor and applies the
94
+ # RESP3->RESP2 fallback. disable_reconnection establishes the connection eagerly, so if that
95
+ # handshake fails the fallback rebuilds @client and the whole block re-runs against the new
96
+ # (RESP2) client — keeping disable_reconnection bound to the live @client rather than a discarded
97
+ # one. Referencing the block argument (not @client) is what makes the retry pick up the rebuild.
98
+ synchronize do |client|
99
+ client.disable_reconnection(&block)
100
+ end
82
101
  end
83
102
 
84
103
  # Test whether or not the client is connected
@@ -118,7 +137,10 @@ class Redis
118
137
  end
119
138
 
120
139
  def dup
121
- self.class.new(@options)
140
+ # inherit_socket and himport_auto_prepare are stripped from @options before the client config
141
+ # is built (RedisClient::Config doesn't know them); merge the live values back so the
142
+ # duplicate keeps the caller's settings instead of silently reverting to defaults.
143
+ self.class.new(@options.merge(inherit_socket: @inherit_socket, himport_auto_prepare: @himport_auto_prepare))
122
144
  end
123
145
 
124
146
  def connection
@@ -131,8 +153,70 @@ class Redis
131
153
  }
132
154
  end
133
155
 
156
+ # HIMPORT overrides adding fieldset-loss recovery on top of the transport-pure methods in
157
+ # Commands::Hashes. The connection can be transparently replaced under the caller (reconnect
158
+ # after a network error, failover, RESET by a proxy), destroying every prepared fieldset. The
159
+ # server then answers HIMPORT SET with "no such fieldset" — the authoritative signal that the
160
+ # session was lost. These overrides remember the last schema prepared per fieldset name and,
161
+ # on that error, re-prepare it and retry the SET exactly once. An explicitly discarded
162
+ # fieldset is removed from the registry and is never resurrected. Disable the recovery with
163
+ # `Redis.new(himport_auto_prepare: false)`; the registry is still recorded for manual use.
164
+
165
+ # Each method holds @monitor across the server command AND its registry mutation: the two must
166
+ # be atomic with respect to other threads, otherwise a himport_set failing between another
167
+ # thread's DISCARD reply and its registry delete would still see the schema and re-prepare,
168
+ # resurrecting the fieldset the discard just removed. @monitor is reentrant, so the nested
169
+ # send_command/himport_prepare calls re-enter it safely.
170
+
171
+ def himport_prepare(fieldset_name, *fields)
172
+ fields.flatten!(1)
173
+ @monitor.synchronize do
174
+ reply = super(fieldset_name, fields)
175
+ @himport_fieldsets[fieldset_name.to_s] = fields.dup.freeze
176
+ reply
177
+ end
178
+ end
179
+
180
+ def himport_set(key, fieldset_name, *values)
181
+ @monitor.synchronize do
182
+ super
183
+ rescue CommandError => error
184
+ fields = @himport_fieldsets[fieldset_name.to_s]
185
+ raise unless @himport_auto_prepare && fields && error.message.include?("no such fieldset")
186
+
187
+ himport_prepare(fieldset_name, fields)
188
+ # `super` (not himport_set) so a second failure propagates instead of recovering again.
189
+ super
190
+ end
191
+ end
192
+
193
+ def himport_discard(fieldset_name)
194
+ @monitor.synchronize do
195
+ reply = super
196
+ @himport_fieldsets.delete(fieldset_name.to_s)
197
+ reply
198
+ end
199
+ end
200
+
201
+ def himport_discard_all
202
+ @monitor.synchronize do
203
+ reply = super
204
+ @himport_fieldsets.clear
205
+ reply
206
+ end
207
+ end
208
+
134
209
  private
135
210
 
211
+ # Builds @client from @options and applies any instance-level settings (socket inheritance) that
212
+ # live outside @options. Used both at construction and when the RESP3->RESP2 fallback rebuilds the
213
+ # client, so those settings survive a protocol downgrade.
214
+ def build_client
215
+ client = initialize_client(@options)
216
+ client.inherit_socket! if @inherit_socket
217
+ client
218
+ end
219
+
136
220
  def initialize_client(options)
137
221
  if options.key?(:cluster)
138
222
  raise "Redis Cluster support was moved to the `redis-clustering` gem."
@@ -145,22 +229,64 @@ class Redis
145
229
  end
146
230
  end
147
231
 
232
+ # All access to @client funnels through here: it serializes on @monitor and applies the RESP3
233
+ # protocol fallback. Routing pipelined/multi/watch (which all call synchronize) through the same
234
+ # path means they fall back to RESP2 against pre-HELLO servers just like single commands do.
148
235
  def synchronize
149
- @monitor.synchronize { yield(@client) }
236
+ @monitor.synchronize do
237
+ with_protocol_fallback do
238
+ yield(@client)
239
+ end
240
+ end
150
241
  end
151
242
 
152
243
  def send_command(command, &block)
153
- @monitor.synchronize do
154
- @client.call_v(command, &block)
244
+ synchronize do |client|
245
+ client.call_v(command, &block)
155
246
  end
156
247
  rescue ::RedisClient::Error => error
157
248
  Client.translate_error!(error)
158
249
  end
159
250
 
160
251
  def send_blocking_command(command, timeout, &block)
161
- @monitor.synchronize do
162
- @client.blocking_call_v(timeout, command, &block)
252
+ synchronize do |client|
253
+ client.blocking_call_v(timeout, command, &block)
163
254
  end
255
+ rescue ::RedisClient::Error => error
256
+ Client.translate_error!(error)
257
+ end
258
+
259
+ # We default to RESP3. Servers that don't support it reject the `HELLO 3` handshake (most
260
+ # notably Redis < 6.0, which has no HELLO command). Rebuild the client as RESP2 once so those
261
+ # servers keep working without the user setting `protocol: 2`.
262
+ #
263
+ # This is the single fallback point for every client type. Each one surfaces the resp3-unsupported
264
+ # error here untranslated (still a RedisClient::Error): standalone/distributed via
265
+ # Redis::Client#call_v et al., sentinel via the plain RedisClient (which never translates), and
266
+ # cluster via Redis::Cluster::Client#handle_errors. Because every @client access — single commands,
267
+ # pipelined, multi, watch, and the pub/sub socket — flows through #synchronize, wrapping it here
268
+ # covers them all.
269
+ #
270
+ # Must be called while holding @monitor: it closes and replaces @client, so it has to be
271
+ # serialized with the command execution that uses @client. The retried block re-reads @client, so
272
+ # callers must reference the rebuilt instance (via the synchronize block argument), not a cached
273
+ # one.
274
+ def with_protocol_fallback
275
+ yield
276
+ rescue ::RedisClient::Error => error
277
+ if @options.fetch(:protocol, 3).to_i == 3 && Client.resp3_unsupported?(error)
278
+ @options = @options.merge(protocol: 2)
279
+ @client.close
280
+ @client = build_client
281
+ # Warn only once the RESP2 client is actually in place — if the rebuild itself raises we
282
+ # haven't really fallen back. Fires once per client: @options[:protocol] is now 2, so this
283
+ # branch never re-enters. Passing `protocol: 2` explicitly skips it (and silences this).
284
+ warn("Redis: the server does not support RESP3 (the HELLO 3 handshake failed); falling back " \
285
+ "to RESP2. Pass `protocol: 2` to select RESP2 explicitly and silence this warning.")
286
+ retry
287
+ end
288
+
289
+ raise
164
290
  end
165
291
 
166
292
  def _subscription(method, timeout, channels, block)
@@ -170,7 +296,10 @@ class Redis
170
296
  end
171
297
 
172
298
  begin
173
- @subscription_client = SubscribedClient.new(@client.pubsub)
299
+ # The pub/sub second socket is opened via @client.pubsub, which connects through
300
+ # ensure_connected rather than a command path. Route it through #synchronize so the same
301
+ # RESP3->RESP2 fallback applies when subscribe is the first operation against an old server.
302
+ @subscription_client = SubscribedClient.new(synchronize(&:pubsub))
174
303
  if timeout > 0
175
304
  @subscription_client.send(method, timeout, *channels, &block)
176
305
  else
@@ -191,6 +320,7 @@ class Redis
191
320
  end
192
321
 
193
322
  require "redis/version"
323
+ require "redis/lib_identity"
194
324
  require "redis/client"
195
325
  require "redis/pipeline"
196
326
  require "redis/subscribe"
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: 5.4.1
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ezra Zygmuntowicz
@@ -15,22 +15,22 @@ authors:
15
15
  - Pieter Noordhuis
16
16
  bindir: bin
17
17
  cert_chain: []
18
- date: 2025-07-17 00:00:00.000000000 Z
18
+ date: 2026-07-31 00:00:00.000000000 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: redis-client
22
22
  requirement: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.22.0
26
+ version: 0.30.1
27
27
  type: :runtime
28
28
  prerelease: false
29
29
  version_requirements: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - '='
32
32
  - !ruby/object:Gem::Version
33
- version: 0.22.0
33
+ version: 0.30.1
34
34
  description: |2
35
35
  A Ruby client that tries to match Redis' API one-to-one, while still
36
36
  providing an idiomatic interface.
@@ -54,6 +54,18 @@ files:
54
54
  - lib/redis/commands/hyper_log_log.rb
55
55
  - lib/redis/commands/keys.rb
56
56
  - lib/redis/commands/lists.rb
57
+ - lib/redis/commands/modules/json.rb
58
+ - lib/redis/commands/modules/search.rb
59
+ - lib/redis/commands/modules/search/aggregation.rb
60
+ - lib/redis/commands/modules/search/dialect.rb
61
+ - lib/redis/commands/modules/search/field.rb
62
+ - lib/redis/commands/modules/search/hybrid.rb
63
+ - lib/redis/commands/modules/search/index.rb
64
+ - lib/redis/commands/modules/search/index_definition.rb
65
+ - lib/redis/commands/modules/search/miscellaneous.rb
66
+ - lib/redis/commands/modules/search/query.rb
67
+ - lib/redis/commands/modules/search/result.rb
68
+ - lib/redis/commands/modules/search/schema.rb
57
69
  - lib/redis/commands/pubsub.rb
58
70
  - lib/redis/commands/scripting.rb
59
71
  - lib/redis/commands/server.rb
@@ -65,6 +77,7 @@ files:
65
77
  - lib/redis/distributed.rb
66
78
  - lib/redis/errors.rb
67
79
  - lib/redis/hash_ring.rb
80
+ - lib/redis/lib_identity.rb
68
81
  - lib/redis/pipeline.rb
69
82
  - lib/redis/subscribe.rb
70
83
  - lib/redis/version.rb
@@ -74,9 +87,9 @@ licenses:
74
87
  metadata:
75
88
  bug_tracker_uri: https://github.com/redis/redis-rb/issues
76
89
  changelog_uri: https://github.com/redis/redis-rb/blob/master/CHANGELOG.md
77
- documentation_uri: https://www.rubydoc.info/gems/redis/5.4.1
90
+ documentation_uri: https://www.rubydoc.info/gems/redis/6.0.0
78
91
  homepage_uri: https://github.com/redis/redis-rb
79
- source_code_uri: https://github.com/redis/redis-rb/tree/v5.4.1
92
+ source_code_uri: https://github.com/redis/redis-rb/tree/v6.0.0
80
93
  rdoc_options: []
81
94
  require_paths:
82
95
  - lib
@@ -84,7 +97,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
84
97
  requirements:
85
98
  - - ">="
86
99
  - !ruby/object:Gem::Version
87
- version: 2.6.0
100
+ version: 3.2.0
88
101
  required_rubygems_version: !ruby/object:Gem::Requirement
89
102
  requirements:
90
103
  - - ">="