redis 4.8.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +138 -1
- data/README.md +285 -169
- data/lib/redis/client.rb +116 -611
- data/lib/redis/commands/bitmaps.rb +14 -4
- data/lib/redis/commands/cluster.rb +1 -18
- data/lib/redis/commands/connection.rb +5 -10
- data/lib/redis/commands/geo.rb +109 -7
- data/lib/redis/commands/hashes.rb +179 -8
- data/lib/redis/commands/hyper_log_log.rb +1 -1
- data/lib/redis/commands/keys.rb +32 -24
- data/lib/redis/commands/lists.rb +167 -25
- data/lib/redis/commands/modules/json.rb +530 -0
- data/lib/redis/commands/modules/search/aggregation.rb +418 -0
- data/lib/redis/commands/modules/search/dialect.rb +14 -0
- data/lib/redis/commands/modules/search/field.rb +306 -0
- data/lib/redis/commands/modules/search/hybrid.rb +359 -0
- data/lib/redis/commands/modules/search/index.rb +351 -0
- data/lib/redis/commands/modules/search/index_definition.rb +114 -0
- data/lib/redis/commands/modules/search/miscellaneous.rb +607 -0
- data/lib/redis/commands/modules/search/query.rb +738 -0
- data/lib/redis/commands/modules/search/result.rb +488 -0
- data/lib/redis/commands/modules/search/schema.rb +211 -0
- data/lib/redis/commands/modules/search.rb +19 -0
- data/lib/redis/commands/pubsub.rb +34 -25
- data/lib/redis/commands/server.rb +15 -15
- data/lib/redis/commands/sets.rb +76 -40
- data/lib/redis/commands/sorted_sets.rb +128 -19
- data/lib/redis/commands/streams.rb +75 -28
- data/lib/redis/commands/strings.rb +18 -17
- data/lib/redis/commands/transactions.rb +7 -31
- data/lib/redis/commands.rb +39 -20
- data/lib/redis/distributed.rb +407 -73
- data/lib/redis/errors.rb +20 -50
- data/lib/redis/hash_ring.rb +26 -26
- data/lib/redis/lib_identity.rb +105 -0
- data/lib/redis/pipeline.rb +47 -222
- data/lib/redis/subscribe.rb +51 -15
- data/lib/redis/version.rb +1 -1
- data/lib/redis.rb +213 -188
- metadata +25 -59
- data/lib/redis/cluster/command.rb +0 -79
- data/lib/redis/cluster/command_loader.rb +0 -33
- data/lib/redis/cluster/key_slot_converter.rb +0 -72
- data/lib/redis/cluster/node.rb +0 -120
- data/lib/redis/cluster/node_key.rb +0 -31
- data/lib/redis/cluster/node_loader.rb +0 -34
- data/lib/redis/cluster/option.rb +0 -100
- data/lib/redis/cluster/slot.rb +0 -86
- data/lib/redis/cluster/slot_loader.rb +0 -46
- data/lib/redis/cluster.rb +0 -315
- data/lib/redis/connection/command_helper.rb +0 -41
- data/lib/redis/connection/hiredis.rb +0 -68
- data/lib/redis/connection/registry.rb +0 -13
- data/lib/redis/connection/ruby.rb +0 -437
- data/lib/redis/connection/synchrony.rb +0 -148
- data/lib/redis/connection.rb +0 -11
data/lib/redis/subscribe.rb
CHANGED
|
@@ -4,10 +4,13 @@ class Redis
|
|
|
4
4
|
class SubscribedClient
|
|
5
5
|
def initialize(client)
|
|
6
6
|
@client = client
|
|
7
|
+
@write_monitor = Monitor.new
|
|
7
8
|
end
|
|
8
9
|
|
|
9
|
-
def
|
|
10
|
-
@
|
|
10
|
+
def call_v(command)
|
|
11
|
+
@write_monitor.synchronize do
|
|
12
|
+
@client.call_v(command)
|
|
13
|
+
end
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def subscribe(*channels, &block)
|
|
@@ -26,12 +29,28 @@ class Redis
|
|
|
26
29
|
subscription("psubscribe", "punsubscribe", channels, block, timeout)
|
|
27
30
|
end
|
|
28
31
|
|
|
32
|
+
def ssubscribe(*channels, &block)
|
|
33
|
+
subscription("ssubscribe", "sunsubscribe", channels, block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ssubscribe_with_timeout(timeout, *channels, &block)
|
|
37
|
+
subscription("ssubscribe", "sunsubscribe", channels, block, timeout)
|
|
38
|
+
end
|
|
39
|
+
|
|
29
40
|
def unsubscribe(*channels)
|
|
30
|
-
|
|
41
|
+
call_v([:unsubscribe, *channels])
|
|
31
42
|
end
|
|
32
43
|
|
|
33
44
|
def punsubscribe(*channels)
|
|
34
|
-
|
|
45
|
+
call_v([:punsubscribe, *channels])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def sunsubscribe(*channels)
|
|
49
|
+
call_v([:sunsubscribe, *channels])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def close
|
|
53
|
+
@client.close
|
|
35
54
|
end
|
|
36
55
|
|
|
37
56
|
protected
|
|
@@ -39,13 +58,21 @@ class Redis
|
|
|
39
58
|
def subscription(start, stop, channels, block, timeout = 0)
|
|
40
59
|
sub = Subscription.new(&block)
|
|
41
60
|
|
|
42
|
-
|
|
61
|
+
case start
|
|
62
|
+
when "ssubscribe" then channels.each { |c| call_v([start, c]) } # avoid cross-slot keys
|
|
63
|
+
else call_v([start, *channels])
|
|
64
|
+
end
|
|
43
65
|
|
|
44
|
-
@client.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
66
|
+
while event = @client.next_event(timeout)
|
|
67
|
+
if event.is_a?(::RedisClient::CommandError)
|
|
68
|
+
raise Client::ERROR_MAPPING.fetch(event.class), event.message
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
type, *rest = event
|
|
72
|
+
if callback = sub.callbacks[type]
|
|
73
|
+
callback.call(*rest)
|
|
74
|
+
end
|
|
75
|
+
break if type == stop && rest.last == 0
|
|
49
76
|
end
|
|
50
77
|
# No need to unsubscribe here. The real client closes the connection
|
|
51
78
|
# whenever an exception is raised (see #ensure_connected).
|
|
@@ -53,13 +80,10 @@ class Redis
|
|
|
53
80
|
end
|
|
54
81
|
|
|
55
82
|
class Subscription
|
|
56
|
-
|
|
83
|
+
attr_reader :callbacks
|
|
57
84
|
|
|
58
85
|
def initialize
|
|
59
|
-
@callbacks =
|
|
60
|
-
hash[key] = ->(*_) {}
|
|
61
|
-
end
|
|
62
|
-
|
|
86
|
+
@callbacks = {}
|
|
63
87
|
yield(self)
|
|
64
88
|
end
|
|
65
89
|
|
|
@@ -86,5 +110,17 @@ class Redis
|
|
|
86
110
|
def pmessage(&block)
|
|
87
111
|
@callbacks["pmessage"] = block
|
|
88
112
|
end
|
|
113
|
+
|
|
114
|
+
def ssubscribe(&block)
|
|
115
|
+
@callbacks["ssubscribe"] = block
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def sunsubscribe(&block)
|
|
119
|
+
@callbacks["sunsubscribe"] = block
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def smessage(&block)
|
|
123
|
+
@callbacks["smessage"] = block
|
|
124
|
+
end
|
|
89
125
|
end
|
|
90
126
|
end
|
data/lib/redis/version.rb
CHANGED
data/lib/redis.rb
CHANGED
|
@@ -1,31 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "redis-client"
|
|
4
|
+
|
|
3
5
|
require "monitor"
|
|
4
6
|
require "redis/errors"
|
|
5
7
|
require "redis/commands"
|
|
6
8
|
|
|
7
9
|
class Redis
|
|
8
10
|
BASE_PATH = __dir__
|
|
9
|
-
@exists_returns_integer = true
|
|
10
|
-
@sadd_returns_boolean = true
|
|
11
|
-
|
|
12
11
|
Deprecated = Class.new(StandardError)
|
|
13
12
|
|
|
14
13
|
class << self
|
|
15
|
-
|
|
16
|
-
attr_accessor :silence_deprecations, :raise_deprecations, :sadd_returns_boolean
|
|
17
|
-
|
|
18
|
-
def exists_returns_integer=(value)
|
|
19
|
-
unless value
|
|
20
|
-
deprecate!(
|
|
21
|
-
"`Redis#exists(key)` will return an Integer by default in redis-rb 4.3. The option to explicitly " \
|
|
22
|
-
"disable this behaviour via `Redis.exists_returns_integer` will be removed in 5.0. You should use " \
|
|
23
|
-
"`exists?` instead."
|
|
24
|
-
)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
@exists_returns_integer = value
|
|
28
|
-
end
|
|
14
|
+
attr_accessor :silence_deprecations, :raise_deprecations
|
|
29
15
|
|
|
30
16
|
def deprecate!(message)
|
|
31
17
|
unless silence_deprecations
|
|
@@ -36,20 +22,22 @@ class Redis
|
|
|
36
22
|
end
|
|
37
23
|
end
|
|
38
24
|
end
|
|
25
|
+
end
|
|
39
26
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@current = redis
|
|
27
|
+
# soft-deprecated
|
|
28
|
+
# We added this back for older sidekiq releases
|
|
29
|
+
module Connection
|
|
30
|
+
class << self
|
|
31
|
+
def drivers
|
|
32
|
+
[RedisClient.default_driver]
|
|
33
|
+
end
|
|
48
34
|
end
|
|
49
35
|
end
|
|
50
36
|
|
|
51
37
|
include Commands
|
|
52
38
|
|
|
39
|
+
SERVER_URL_OPTIONS = %i(url host port path).freeze
|
|
40
|
+
|
|
53
41
|
# Create a new client instance
|
|
54
42
|
#
|
|
55
43
|
# @param [Hash] options
|
|
@@ -59,56 +47,68 @@ class Redis
|
|
|
59
47
|
# @option options [String] :host ("127.0.0.1") server hostname
|
|
60
48
|
# @option options [Integer] :port (6379) server port
|
|
61
49
|
# @option options [String] :path path to server socket (overrides host and port)
|
|
62
|
-
# @option options [Float] :timeout (
|
|
50
|
+
# @option options [Float] :timeout (1.0) timeout in seconds
|
|
63
51
|
# @option options [Float] :connect_timeout (same as timeout) timeout for initial connect in seconds
|
|
64
52
|
# @option options [String] :username Username to authenticate against server
|
|
65
53
|
# @option options [String] :password Password to authenticate against server
|
|
66
|
-
# @option options [Integer] :db (0) Database to select after
|
|
67
|
-
# @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis
|
|
54
|
+
# @option options [Integer] :db (0) Database to select after connect and on reconnects
|
|
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.
|
|
68
58
|
# @option options [String] :id ID for the client connection, assigns name to current connection by sending
|
|
69
59
|
# `CLIENT SETNAME`
|
|
70
|
-
# @option options [
|
|
71
|
-
#
|
|
72
|
-
#
|
|
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.
|
|
64
|
+
# @option options [Integer, Array<Integer, Float>] :reconnect_attempts Number of attempts trying to connect,
|
|
65
|
+
# or a list of sleep duration between attempts.
|
|
73
66
|
# @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not
|
|
67
|
+
# @option options [String] :name The name of the server group to connect to.
|
|
74
68
|
# @option options [Array] :sentinels List of sentinels to contact
|
|
75
|
-
# @option options [Symbol] :role (:master) Role to fetch via Sentinel, either `:master` or `:slave`
|
|
76
|
-
# @option options [Array<String, Hash{Symbol => String, Integer}>] :cluster List of cluster nodes to contact
|
|
77
|
-
# @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not
|
|
78
|
-
# @option options [String] :fixed_hostname Specify a FQDN if cluster mode enabled and
|
|
79
|
-
# client has to connect nodes via single endpoint with SSL/TLS
|
|
80
|
-
# @option options [Class] :connector Class of custom connector
|
|
81
69
|
#
|
|
82
70
|
# @return [Redis] a new client instance
|
|
83
71
|
def initialize(options = {})
|
|
84
|
-
@options = options.dup
|
|
85
|
-
@cluster_mode = options.key?(:cluster)
|
|
86
|
-
client = @cluster_mode ? Cluster : Client
|
|
87
|
-
@original_client = @client = client.new(options)
|
|
88
|
-
@queue = Hash.new { |h, k| h[k] = [] }
|
|
89
72
|
@monitor = Monitor.new
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
synchronize do |client|
|
|
95
|
-
client.with_reconnect(val, &blk)
|
|
73
|
+
@options = options.dup
|
|
74
|
+
@options[:reconnect_attempts] = 1 unless @options.key?(:reconnect_attempts)
|
|
75
|
+
if ENV["REDIS_URL"] && SERVER_URL_OPTIONS.none? { |o| @options.key?(o) }
|
|
76
|
+
@options[:url] = ENV["REDIS_URL"]
|
|
96
77
|
end
|
|
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 = {}
|
|
86
|
+
@subscription_client = nil
|
|
87
|
+
|
|
88
|
+
@client = build_client
|
|
97
89
|
end
|
|
98
90
|
|
|
99
91
|
# Run code without the client reconnecting
|
|
100
|
-
def without_reconnect(&
|
|
101
|
-
|
|
92
|
+
def without_reconnect(&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
|
|
102
101
|
end
|
|
103
102
|
|
|
104
103
|
# Test whether or not the client is connected
|
|
105
104
|
def connected?
|
|
106
|
-
@
|
|
105
|
+
@client.connected? || @subscription_client&.connected?
|
|
107
106
|
end
|
|
108
107
|
|
|
109
108
|
# Disconnect the client as quickly and silently as possible.
|
|
110
109
|
def close
|
|
111
|
-
@
|
|
110
|
+
@client.close
|
|
111
|
+
@subscription_client&.close
|
|
112
112
|
end
|
|
113
113
|
alias disconnect! close
|
|
114
114
|
|
|
@@ -116,186 +116,211 @@ class Redis
|
|
|
116
116
|
yield self
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# Commands in the queue are executed with the Redis#commit method.
|
|
122
|
-
#
|
|
123
|
-
# See http://redis.io/topics/pipelining for more details.
|
|
124
|
-
#
|
|
125
|
-
def queue(*command)
|
|
126
|
-
::Redis.deprecate!(
|
|
127
|
-
"Redis#queue is deprecated and will be removed in Redis 5.0.0. Use Redis#pipelined instead." \
|
|
128
|
-
"(called from: #{caller(1, 1).first})"
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
synchronize do
|
|
132
|
-
@queue[Thread.current.object_id] << command
|
|
133
|
-
end
|
|
119
|
+
def _client
|
|
120
|
+
@client
|
|
134
121
|
end
|
|
135
122
|
|
|
136
|
-
|
|
137
|
-
#
|
|
138
|
-
# See http://redis.io/topics/pipelining for more details.
|
|
139
|
-
#
|
|
140
|
-
def commit
|
|
141
|
-
::Redis.deprecate!(
|
|
142
|
-
"Redis#commit is deprecated and will be removed in Redis 5.0.0. Use Redis#pipelined instead. " \
|
|
143
|
-
"(called from: #{Kernel.caller(1, 1).first})"
|
|
144
|
-
)
|
|
145
|
-
|
|
123
|
+
def pipelined(exception: true)
|
|
146
124
|
synchronize do |client|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
@queue[Thread.current.object_id].each do |command|
|
|
150
|
-
pipeline.call(command)
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
client.call_pipelined(pipeline)
|
|
154
|
-
ensure
|
|
155
|
-
@queue.delete(Thread.current.object_id)
|
|
125
|
+
client.pipelined(exception: exception) do |raw_pipeline|
|
|
126
|
+
yield PipelinedConnection.new(raw_pipeline, exception: exception)
|
|
156
127
|
end
|
|
157
128
|
end
|
|
158
129
|
end
|
|
159
130
|
|
|
160
|
-
def
|
|
161
|
-
@client
|
|
131
|
+
def id
|
|
132
|
+
@client.id || @client.server_url
|
|
162
133
|
end
|
|
163
134
|
|
|
164
|
-
def
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
Pipeline.deprecation_warning("pipelined", Kernel.caller_locations(1, 5))
|
|
168
|
-
deprecation_displayed = true
|
|
169
|
-
end
|
|
135
|
+
def inspect
|
|
136
|
+
"#<Redis client v#{Redis::VERSION} for #{id}>"
|
|
137
|
+
end
|
|
170
138
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
yield pipelined_connection
|
|
177
|
-
prior_client.call_pipeline(pipeline)
|
|
178
|
-
ensure
|
|
179
|
-
@client = prior_client
|
|
180
|
-
end
|
|
181
|
-
end
|
|
139
|
+
def dup
|
|
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))
|
|
182
144
|
end
|
|
183
145
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
#
|
|
194
|
-
# @example Without a block
|
|
195
|
-
# redis.multi
|
|
196
|
-
# # => "OK"
|
|
197
|
-
# redis.set("key", "value")
|
|
198
|
-
# # => "QUEUED"
|
|
199
|
-
# redis.incr("counter")
|
|
200
|
-
# # => "QUEUED"
|
|
201
|
-
# redis.exec
|
|
202
|
-
# # => ["OK", 6]
|
|
203
|
-
#
|
|
204
|
-
# @yield [multi] the commands that are called inside this block are cached
|
|
205
|
-
# and written to the server upon returning from it
|
|
206
|
-
# @yieldparam [Redis] multi `self`
|
|
207
|
-
#
|
|
208
|
-
# @return [String, Array<...>]
|
|
209
|
-
# - when a block is not given, `OK`
|
|
210
|
-
# - when a block is given, an array with replies
|
|
211
|
-
#
|
|
212
|
-
# @see #watch
|
|
213
|
-
# @see #unwatch
|
|
214
|
-
def multi(&block)
|
|
215
|
-
if block_given?
|
|
216
|
-
deprecation_displayed = false
|
|
217
|
-
if block&.arity == 0
|
|
218
|
-
Pipeline.deprecation_warning("multi", Kernel.caller_locations(1, 5))
|
|
219
|
-
deprecation_displayed = true
|
|
220
|
-
end
|
|
146
|
+
def connection
|
|
147
|
+
{
|
|
148
|
+
host: @client.host,
|
|
149
|
+
port: @client.port,
|
|
150
|
+
db: @client.db,
|
|
151
|
+
id: id,
|
|
152
|
+
location: "#{@client.host}:#{@client.port}"
|
|
153
|
+
}
|
|
154
|
+
end
|
|
221
155
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
235
177
|
end
|
|
236
178
|
end
|
|
237
179
|
|
|
238
|
-
def
|
|
239
|
-
@
|
|
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
|
|
240
191
|
end
|
|
241
192
|
|
|
242
|
-
def
|
|
243
|
-
|
|
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
|
|
244
199
|
end
|
|
245
200
|
|
|
246
|
-
def
|
|
247
|
-
|
|
201
|
+
def himport_discard_all
|
|
202
|
+
@monitor.synchronize do
|
|
203
|
+
reply = super
|
|
204
|
+
@himport_fieldsets.clear
|
|
205
|
+
reply
|
|
206
|
+
end
|
|
248
207
|
end
|
|
249
208
|
|
|
250
|
-
|
|
251
|
-
return @original_client.connection_info if @cluster_mode
|
|
209
|
+
private
|
|
252
210
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
260
218
|
end
|
|
261
219
|
|
|
262
|
-
|
|
220
|
+
def initialize_client(options)
|
|
221
|
+
if options.key?(:cluster)
|
|
222
|
+
raise "Redis Cluster support was moved to the `redis-clustering` gem."
|
|
223
|
+
end
|
|
263
224
|
|
|
225
|
+
if options.key?(:sentinels)
|
|
226
|
+
Client.sentinel(**options).new_client
|
|
227
|
+
else
|
|
228
|
+
Client.config(**options).new_client
|
|
229
|
+
end
|
|
230
|
+
end
|
|
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.
|
|
264
235
|
def synchronize
|
|
265
|
-
@monitor.synchronize
|
|
236
|
+
@monitor.synchronize do
|
|
237
|
+
with_protocol_fallback do
|
|
238
|
+
yield(@client)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
266
241
|
end
|
|
267
242
|
|
|
268
243
|
def send_command(command, &block)
|
|
269
|
-
|
|
270
|
-
|
|
244
|
+
synchronize do |client|
|
|
245
|
+
client.call_v(command, &block)
|
|
271
246
|
end
|
|
247
|
+
rescue ::RedisClient::Error => error
|
|
248
|
+
Client.translate_error!(error)
|
|
272
249
|
end
|
|
273
250
|
|
|
274
251
|
def send_blocking_command(command, timeout, &block)
|
|
275
|
-
|
|
276
|
-
|
|
252
|
+
synchronize do |client|
|
|
253
|
+
client.blocking_call_v(timeout, command, &block)
|
|
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
|
|
277
287
|
end
|
|
288
|
+
|
|
289
|
+
raise
|
|
278
290
|
end
|
|
279
291
|
|
|
280
292
|
def _subscription(method, timeout, channels, block)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
293
|
+
if block
|
|
294
|
+
if @subscription_client
|
|
295
|
+
raise SubscriptionError, "This client is already subscribed"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
begin
|
|
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))
|
|
303
|
+
if timeout > 0
|
|
304
|
+
@subscription_client.send(method, timeout, *channels, &block)
|
|
305
|
+
else
|
|
306
|
+
@subscription_client.send(method, *channels, &block)
|
|
307
|
+
end
|
|
308
|
+
ensure
|
|
309
|
+
@subscription_client&.close
|
|
310
|
+
@subscription_client = nil
|
|
289
311
|
end
|
|
290
|
-
|
|
291
|
-
@
|
|
312
|
+
else
|
|
313
|
+
unless @subscription_client
|
|
314
|
+
raise SubscriptionError, "This client is not subscribed"
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
@subscription_client.call_v([method].concat(channels))
|
|
292
318
|
end
|
|
293
319
|
end
|
|
294
320
|
end
|
|
295
321
|
|
|
296
322
|
require "redis/version"
|
|
297
|
-
require "redis/
|
|
323
|
+
require "redis/lib_identity"
|
|
298
324
|
require "redis/client"
|
|
299
|
-
require "redis/cluster"
|
|
300
325
|
require "redis/pipeline"
|
|
301
326
|
require "redis/subscribe"
|