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/client.rb
CHANGED
|
@@ -1,658 +1,163 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "socket"
|
|
4
|
-
require "cgi"
|
|
5
|
-
require "redis/errors"
|
|
6
|
-
|
|
7
3
|
class Redis
|
|
8
|
-
class Client
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def password
|
|
70
|
-
@options[:password]
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def db
|
|
74
|
-
@options[:db]
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def db=(db)
|
|
78
|
-
@options[:db] = db.to_i
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def driver
|
|
82
|
-
@options[:driver]
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def inherit_socket?
|
|
86
|
-
@options[:inherit_socket]
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
attr_accessor :logger
|
|
90
|
-
|
|
91
|
-
def initialize(options = {})
|
|
92
|
-
@options = _parse_options(options)
|
|
93
|
-
@reconnect = true
|
|
94
|
-
@logger = @options[:logger]
|
|
95
|
-
@connection = nil
|
|
96
|
-
@command_map = {}
|
|
97
|
-
|
|
98
|
-
@pending_reads = 0
|
|
99
|
-
|
|
100
|
-
@connector =
|
|
101
|
-
if !@options[:sentinels].nil?
|
|
102
|
-
Connector::Sentinel.new(@options)
|
|
103
|
-
elsif options.include?(:connector) && options[:connector].respond_to?(:new)
|
|
104
|
-
options.delete(:connector).new(@options)
|
|
4
|
+
class Client < ::RedisClient
|
|
5
|
+
ERROR_MAPPING = {
|
|
6
|
+
RedisClient::ConnectionError => Redis::ConnectionError,
|
|
7
|
+
RedisClient::CommandError => Redis::CommandError,
|
|
8
|
+
RedisClient::ReadTimeoutError => Redis::TimeoutError,
|
|
9
|
+
RedisClient::CannotConnectError => Redis::CannotConnectError,
|
|
10
|
+
RedisClient::AuthenticationError => Redis::CannotConnectError,
|
|
11
|
+
RedisClient::FailoverError => Redis::CannotConnectError,
|
|
12
|
+
RedisClient::PermissionError => Redis::PermissionError,
|
|
13
|
+
RedisClient::WrongTypeError => Redis::WrongTypeError,
|
|
14
|
+
RedisClient::ReadOnlyError => Redis::ReadOnlyError,
|
|
15
|
+
RedisClient::ProtocolError => Redis::ProtocolError,
|
|
16
|
+
RedisClient::OutOfMemoryError => Redis::OutOfMemoryError,
|
|
17
|
+
}
|
|
18
|
+
if defined?(RedisClient::NoScriptError)
|
|
19
|
+
ERROR_MAPPING[RedisClient::NoScriptError] = Redis::NoScriptError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def config(protocol: 3, **kwargs)
|
|
24
|
+
super(protocol: protocol, **kwargs,
|
|
25
|
+
driver_info: Redis::LibIdentity.driver_info(kwargs[:driver_info]))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def sentinel(protocol: 3, **kwargs)
|
|
29
|
+
super(protocol: protocol, **kwargs,
|
|
30
|
+
driver_info: Redis::LibIdentity.driver_info(kwargs[:driver_info]),
|
|
31
|
+
client_implementation: ::RedisClient)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def translate_error!(error, mapping: ERROR_MAPPING)
|
|
35
|
+
redis_error = translate_error_class(error.class, mapping: mapping)
|
|
36
|
+
raise redis_error, error.message, error.backtrace
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Whether +error+ raised during the connection handshake means the server can't speak RESP3
|
|
40
|
+
# (so we should retry the connection as RESP2). Covers Redis < 6 (no HELLO command, surfaced
|
|
41
|
+
# by redis-client as UnsupportedServer) and any server replying NOPROTO to `HELLO 3`.
|
|
42
|
+
def resp3_unsupported?(error)
|
|
43
|
+
return true if error.is_a?(::RedisClient::UnsupportedServer)
|
|
44
|
+
return true if error.is_a?(::RedisClient::CommandError) && error.message.include?("NOPROTO")
|
|
45
|
+
|
|
46
|
+
# Redis::Cluster discovers its topology by connecting to each startup node. When those nodes
|
|
47
|
+
# don't speak RESP3, redis-cluster-client collects the per-node failures and re-raises them
|
|
48
|
+
# wrapped in an InitialSetupError, discarding the original error classes (see
|
|
49
|
+
# RedisClient::Cluster::InitialSetupError.from_errors). Only the concatenated message
|
|
50
|
+
# survives, so match it to still trigger the RESP2 fallback for pre-6.0 clusters. Guarded by
|
|
51
|
+
# defined? because the cluster error class is only loaded with the redis-clustering gem.
|
|
52
|
+
defined?(::RedisClient::Cluster::InitialSetupError) &&
|
|
53
|
+
error.is_a?(::RedisClient::Cluster::InitialSetupError) &&
|
|
54
|
+
(error.message.include?("NOPROTO") || error.message.include?("HELLO command"))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def translate_error_class(error_class, mapping: ERROR_MAPPING)
|
|
60
|
+
mapping.fetch(error_class)
|
|
61
|
+
rescue IndexError
|
|
62
|
+
if (client_error = error_class.ancestors.find { |a| mapping[a] })
|
|
63
|
+
mapping[error_class] = mapping[client_error]
|
|
105
64
|
else
|
|
106
|
-
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def connect
|
|
111
|
-
@pid = Process.pid
|
|
112
|
-
|
|
113
|
-
# Don't try to reconnect when the connection is fresh
|
|
114
|
-
with_reconnect(false) do
|
|
115
|
-
establish_connection
|
|
116
|
-
if password
|
|
117
|
-
if username
|
|
118
|
-
begin
|
|
119
|
-
call [:auth, username, password]
|
|
120
|
-
rescue CommandError => err # Likely on Redis < 6
|
|
121
|
-
case err.message
|
|
122
|
-
when /ERR wrong number of arguments for 'auth' command/
|
|
123
|
-
call [:auth, password]
|
|
124
|
-
when /WRONGPASS invalid username-password pair/
|
|
125
|
-
begin
|
|
126
|
-
call [:auth, password]
|
|
127
|
-
rescue CommandError
|
|
128
|
-
raise err
|
|
129
|
-
end
|
|
130
|
-
::Redis.deprecate!(
|
|
131
|
-
"[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
|
|
132
|
-
" the provided password was for the default user. This will start failing in redis-rb 5.0.0."
|
|
133
|
-
)
|
|
134
|
-
else
|
|
135
|
-
raise
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
else
|
|
139
|
-
call [:auth, password]
|
|
140
|
-
end
|
|
65
|
+
raise
|
|
141
66
|
end
|
|
142
|
-
|
|
143
|
-
call [:readonly] if @options[:readonly]
|
|
144
|
-
call [:select, db] if db != 0
|
|
145
|
-
call [:client, :setname, @options[:id]] if @options[:id]
|
|
146
|
-
@connector.check(self)
|
|
147
67
|
end
|
|
148
|
-
|
|
149
|
-
self
|
|
150
68
|
end
|
|
151
69
|
|
|
152
70
|
def id
|
|
153
|
-
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def location
|
|
157
|
-
path || "#{host}:#{port}"
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def call(command)
|
|
161
|
-
reply = process([command]) { read }
|
|
162
|
-
raise reply if reply.is_a?(CommandError)
|
|
163
|
-
|
|
164
|
-
if block_given? && reply != 'QUEUED'
|
|
165
|
-
yield reply
|
|
166
|
-
else
|
|
167
|
-
reply
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def call_loop(command, timeout = 0)
|
|
172
|
-
error = nil
|
|
173
|
-
|
|
174
|
-
result = with_socket_timeout(timeout) do
|
|
175
|
-
process([command]) do
|
|
176
|
-
loop do
|
|
177
|
-
reply = read
|
|
178
|
-
if reply.is_a?(CommandError)
|
|
179
|
-
error = reply
|
|
180
|
-
break
|
|
181
|
-
else
|
|
182
|
-
yield reply
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
# Raise error when previous block broke out of the loop.
|
|
189
|
-
raise error if error
|
|
190
|
-
|
|
191
|
-
# Result is set to the value that the provided block used to break.
|
|
192
|
-
result
|
|
71
|
+
config.id
|
|
193
72
|
end
|
|
194
73
|
|
|
195
|
-
def
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
with_reconnect pipeline.with_reconnect? do
|
|
199
|
-
begin
|
|
200
|
-
pipeline.finish(call_pipelined(pipeline)).tap do
|
|
201
|
-
self.db = pipeline.db if pipeline.db
|
|
202
|
-
end
|
|
203
|
-
rescue ConnectionError => e
|
|
204
|
-
return nil if pipeline.shutdown?
|
|
205
|
-
|
|
206
|
-
# Assume the pipeline was sent in one piece, but execution of
|
|
207
|
-
# SHUTDOWN caused none of the replies for commands that were executed
|
|
208
|
-
# prior to it from coming back around.
|
|
209
|
-
raise e
|
|
210
|
-
end
|
|
211
|
-
end
|
|
74
|
+
def server_url
|
|
75
|
+
config.server_url
|
|
212
76
|
end
|
|
213
77
|
|
|
214
|
-
def
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
# The method #ensure_connected (called from #process) reconnects once on
|
|
218
|
-
# I/O errors. To make an effort in making sure that commands are not
|
|
219
|
-
# executed more than once, only allow reconnection before the first reply
|
|
220
|
-
# has been read. When an error occurs after the first reply has been
|
|
221
|
-
# read, retrying would re-execute the entire pipeline, thus re-issuing
|
|
222
|
-
# already successfully executed commands. To circumvent this, don't retry
|
|
223
|
-
# after the first reply has been read successfully.
|
|
224
|
-
|
|
225
|
-
commands = pipeline.commands
|
|
226
|
-
|
|
227
|
-
result = Array.new(commands.size)
|
|
228
|
-
reconnect = @reconnect
|
|
229
|
-
|
|
230
|
-
begin
|
|
231
|
-
exception = nil
|
|
232
|
-
|
|
233
|
-
process(commands) do
|
|
234
|
-
pipeline.timeouts.each_with_index do |timeout, i|
|
|
235
|
-
reply = if timeout
|
|
236
|
-
with_socket_timeout(timeout) { read }
|
|
237
|
-
else
|
|
238
|
-
read
|
|
239
|
-
end
|
|
240
|
-
result[i] = reply
|
|
241
|
-
@reconnect = false
|
|
242
|
-
exception = reply if exception.nil? && reply.is_a?(CommandError)
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
raise exception if exception
|
|
247
|
-
ensure
|
|
248
|
-
@reconnect = reconnect
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
result
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def call_with_timeout(command, extra_timeout, &blk)
|
|
255
|
-
timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout
|
|
256
|
-
with_socket_timeout(timeout) do
|
|
257
|
-
call(command, &blk)
|
|
258
|
-
end
|
|
259
|
-
rescue ConnectionError
|
|
260
|
-
retry
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def call_without_timeout(command, &blk)
|
|
264
|
-
call_with_timeout(command, 0, &blk)
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def process(commands)
|
|
268
|
-
logging(commands) do
|
|
269
|
-
ensure_connected do
|
|
270
|
-
commands.each do |command|
|
|
271
|
-
if command_map[command.first]
|
|
272
|
-
command = command.dup
|
|
273
|
-
command[0] = command_map[command.first]
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
write(command)
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
yield if block_given?
|
|
280
|
-
end
|
|
281
|
-
end
|
|
78
|
+
def timeout
|
|
79
|
+
config.read_timeout
|
|
282
80
|
end
|
|
283
81
|
|
|
284
|
-
def
|
|
285
|
-
|
|
82
|
+
def db
|
|
83
|
+
config.db
|
|
286
84
|
end
|
|
287
85
|
|
|
288
|
-
def
|
|
289
|
-
|
|
86
|
+
def protocol
|
|
87
|
+
config.protocol
|
|
290
88
|
end
|
|
291
|
-
alias close disconnect
|
|
292
89
|
|
|
293
|
-
def
|
|
294
|
-
|
|
295
|
-
connect
|
|
90
|
+
def host
|
|
91
|
+
config.host unless config.path
|
|
296
92
|
end
|
|
297
93
|
|
|
298
|
-
def
|
|
299
|
-
|
|
300
|
-
rescue TimeoutError => e1
|
|
301
|
-
# Add a message to the exception without destroying the original stack
|
|
302
|
-
e2 = TimeoutError.new("Connection timed out")
|
|
303
|
-
e2.set_backtrace(e1.backtrace)
|
|
304
|
-
raise e2
|
|
305
|
-
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL, EOFError => e
|
|
306
|
-
raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
|
|
94
|
+
def port
|
|
95
|
+
config.port unless config.path
|
|
307
96
|
end
|
|
308
97
|
|
|
309
|
-
def
|
|
310
|
-
|
|
311
|
-
value = connection.read
|
|
312
|
-
@pending_reads -= 1
|
|
313
|
-
value
|
|
314
|
-
end
|
|
98
|
+
def path
|
|
99
|
+
config.path
|
|
315
100
|
end
|
|
316
101
|
|
|
317
|
-
def
|
|
318
|
-
|
|
319
|
-
@pending_reads += 1
|
|
320
|
-
connection.write(command)
|
|
321
|
-
end
|
|
102
|
+
def username
|
|
103
|
+
config.username
|
|
322
104
|
end
|
|
323
105
|
|
|
324
|
-
def
|
|
325
|
-
|
|
326
|
-
original = @options[:read_timeout]
|
|
327
|
-
|
|
328
|
-
begin
|
|
329
|
-
connection.timeout = timeout
|
|
330
|
-
@options[:read_timeout] = timeout # for reconnection
|
|
331
|
-
yield
|
|
332
|
-
ensure
|
|
333
|
-
connection.timeout = self.timeout if connected?
|
|
334
|
-
@options[:read_timeout] = original
|
|
335
|
-
end
|
|
106
|
+
def password
|
|
107
|
+
config.password
|
|
336
108
|
end
|
|
337
109
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
110
|
+
undef_method :call
|
|
111
|
+
undef_method :call_once
|
|
112
|
+
undef_method :call_once_v
|
|
113
|
+
undef_method :blocking_call
|
|
341
114
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
115
|
+
# We default to RESP3. Servers that can't speak it reject the `HELLO 3` handshake (Redis < 6.0
|
|
116
|
+
# has no HELLO at all; others answer NOPROTO). Re-raise those errors untranslated so they reach
|
|
117
|
+
# Redis#with_protocol_fallback as RedisClient::Error and trigger a transparent rebuild as RESP2 —
|
|
118
|
+
# the same path the sentinel and cluster clients already use. Everything else is translated to
|
|
119
|
+
# the matching Redis::* error.
|
|
120
|
+
def call_v(command, &block)
|
|
121
|
+
super(command, &block)
|
|
122
|
+
rescue ::RedisClient::Error => error
|
|
123
|
+
raise if Client.resp3_unsupported?(error)
|
|
348
124
|
|
|
349
|
-
|
|
350
|
-
with_reconnect(false, &blk)
|
|
125
|
+
Client.translate_error!(error)
|
|
351
126
|
end
|
|
352
127
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
commands.each do |name, *args|
|
|
360
|
-
logged_args = args.map do |a|
|
|
361
|
-
if a.respond_to?(:inspect) then a.inspect
|
|
362
|
-
elsif a.respond_to?(:to_s) then a.to_s
|
|
363
|
-
else
|
|
364
|
-
# handle poorly-behaved descendants of BasicObject
|
|
365
|
-
klass = a.instance_exec { (class << self; self end).superclass }
|
|
366
|
-
"\#<#{klass}:#{a.__id__}>"
|
|
367
|
-
end
|
|
368
|
-
end
|
|
369
|
-
@logger.debug("[Redis] command=#{name.to_s.upcase} args=#{logged_args.join(' ')}")
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
t1 = Time.now
|
|
373
|
-
yield
|
|
374
|
-
ensure
|
|
375
|
-
@logger.debug("[Redis] call_time=%0.2f ms" % ((Time.now - t1) * 1000)) if t1
|
|
128
|
+
def blocking_call_v(timeout, command, &block)
|
|
129
|
+
if timeout && timeout > 0
|
|
130
|
+
# Can't use the command timeout argument as the connection timeout
|
|
131
|
+
# otherwise it would be very racy. So we add the regular read_timeout on top
|
|
132
|
+
# to account for the network delay.
|
|
133
|
+
timeout += config.read_timeout
|
|
376
134
|
end
|
|
377
|
-
end
|
|
378
135
|
|
|
379
|
-
|
|
380
|
-
|
|
136
|
+
super(timeout, command, &block)
|
|
137
|
+
rescue ::RedisClient::Error => error
|
|
138
|
+
raise if Client.resp3_unsupported?(error)
|
|
381
139
|
|
|
382
|
-
|
|
383
|
-
@options[:port] = Integer(server[:port]) if server.include?(:port)
|
|
384
|
-
|
|
385
|
-
@connection = @options[:driver].connect(@options)
|
|
386
|
-
@pending_reads = 0
|
|
387
|
-
rescue TimeoutError,
|
|
388
|
-
SocketError,
|
|
389
|
-
Errno::EADDRNOTAVAIL,
|
|
390
|
-
Errno::ECONNREFUSED,
|
|
391
|
-
Errno::EHOSTDOWN,
|
|
392
|
-
Errno::EHOSTUNREACH,
|
|
393
|
-
Errno::ENETUNREACH,
|
|
394
|
-
Errno::ENOENT,
|
|
395
|
-
Errno::ETIMEDOUT,
|
|
396
|
-
Errno::EINVAL => error
|
|
397
|
-
|
|
398
|
-
raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
|
|
140
|
+
Client.translate_error!(error)
|
|
399
141
|
end
|
|
400
142
|
|
|
401
|
-
def
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
begin
|
|
407
|
-
attempts += 1
|
|
408
|
-
|
|
409
|
-
connect unless connected?
|
|
410
|
-
|
|
411
|
-
yield
|
|
412
|
-
rescue BaseConnectionError
|
|
413
|
-
disconnect
|
|
414
|
-
|
|
415
|
-
if attempts <= @options[:reconnect_attempts] && @reconnect
|
|
416
|
-
sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
|
|
417
|
-
@options[:reconnect_delay_max]].min
|
|
418
|
-
|
|
419
|
-
Kernel.sleep(sleep_t)
|
|
420
|
-
retry
|
|
421
|
-
else
|
|
422
|
-
raise
|
|
423
|
-
end
|
|
424
|
-
rescue Exception
|
|
425
|
-
disconnect
|
|
426
|
-
raise
|
|
427
|
-
end
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
def _parse_options(options)
|
|
431
|
-
return options if options[:_parsed]
|
|
432
|
-
|
|
433
|
-
defaults = DEFAULTS.dup
|
|
434
|
-
options = options.dup
|
|
435
|
-
|
|
436
|
-
defaults.each_key do |key|
|
|
437
|
-
# Fill in defaults if needed
|
|
438
|
-
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
|
439
|
-
|
|
440
|
-
# Symbolize only keys that are needed
|
|
441
|
-
options[key] = options[key.to_s] if options.key?(key.to_s)
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
url = options[:url]
|
|
445
|
-
url = defaults[:url] if url.nil?
|
|
446
|
-
|
|
447
|
-
# Override defaults from URL if given
|
|
448
|
-
if url
|
|
449
|
-
require "uri"
|
|
450
|
-
|
|
451
|
-
uri = URI(url)
|
|
452
|
-
|
|
453
|
-
case uri.scheme
|
|
454
|
-
when "unix"
|
|
455
|
-
defaults[:path] = uri.path
|
|
456
|
-
when "redis", "rediss"
|
|
457
|
-
defaults[:scheme] = uri.scheme
|
|
458
|
-
defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
|
|
459
|
-
defaults[:port] = uri.port if uri.port
|
|
460
|
-
defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
|
|
461
|
-
defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
|
|
462
|
-
defaults[:db] = uri.path[1..-1].to_i if uri.path
|
|
463
|
-
defaults[:role] = :master
|
|
464
|
-
else
|
|
465
|
-
raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
|
|
466
|
-
end
|
|
467
|
-
|
|
468
|
-
defaults[:ssl] = true if uri.scheme == "rediss"
|
|
469
|
-
end
|
|
470
|
-
|
|
471
|
-
# Use default when option is not specified or nil
|
|
472
|
-
defaults.each_key do |key|
|
|
473
|
-
options[key] = defaults[key] if options[key].nil?
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
if options[:path]
|
|
477
|
-
# Unix socket
|
|
478
|
-
options[:scheme] = "unix"
|
|
479
|
-
options.delete(:host)
|
|
480
|
-
options.delete(:port)
|
|
481
|
-
else
|
|
482
|
-
# TCP socket
|
|
483
|
-
options[:host] = options[:host].to_s
|
|
484
|
-
options[:port] = options[:port].to_i
|
|
485
|
-
end
|
|
486
|
-
|
|
487
|
-
if options.key?(:timeout)
|
|
488
|
-
options[:connect_timeout] ||= options[:timeout]
|
|
489
|
-
options[:read_timeout] ||= options[:timeout]
|
|
490
|
-
options[:write_timeout] ||= options[:timeout]
|
|
491
|
-
end
|
|
492
|
-
|
|
493
|
-
options[:connect_timeout] = Float(options[:connect_timeout])
|
|
494
|
-
options[:read_timeout] = Float(options[:read_timeout])
|
|
495
|
-
options[:write_timeout] = Float(options[:write_timeout])
|
|
496
|
-
|
|
497
|
-
options[:reconnect_attempts] = options[:reconnect_attempts].to_i
|
|
498
|
-
options[:reconnect_delay] = options[:reconnect_delay].to_f
|
|
499
|
-
options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
|
|
500
|
-
|
|
501
|
-
options[:db] = options[:db].to_i
|
|
502
|
-
options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
|
|
503
|
-
|
|
504
|
-
case options[:tcp_keepalive]
|
|
505
|
-
when Hash
|
|
506
|
-
%i[time intvl probes].each do |key|
|
|
507
|
-
unless options[:tcp_keepalive][key].is_a?(Integer)
|
|
508
|
-
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
|
509
|
-
end
|
|
510
|
-
end
|
|
511
|
-
|
|
512
|
-
when Integer
|
|
513
|
-
if options[:tcp_keepalive] >= 60
|
|
514
|
-
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
|
515
|
-
|
|
516
|
-
elsif options[:tcp_keepalive] >= 30
|
|
517
|
-
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
|
518
|
-
|
|
519
|
-
elsif options[:tcp_keepalive] >= 5
|
|
520
|
-
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
|
521
|
-
end
|
|
522
|
-
end
|
|
523
|
-
|
|
524
|
-
options[:_parsed] = true
|
|
143
|
+
def pipelined(exception: true)
|
|
144
|
+
super
|
|
145
|
+
rescue ::RedisClient::Error => error
|
|
146
|
+
raise if Client.resp3_unsupported?(error)
|
|
525
147
|
|
|
526
|
-
|
|
148
|
+
Client.translate_error!(error)
|
|
527
149
|
end
|
|
528
150
|
|
|
529
|
-
def
|
|
530
|
-
|
|
151
|
+
def multi(watch: nil)
|
|
152
|
+
super
|
|
153
|
+
rescue ::RedisClient::Error => error
|
|
154
|
+
raise if Client.resp3_unsupported?(error)
|
|
531
155
|
|
|
532
|
-
|
|
533
|
-
begin
|
|
534
|
-
require_relative "connection/#{driver}"
|
|
535
|
-
rescue LoadError, NameError
|
|
536
|
-
begin
|
|
537
|
-
require "redis/connection/#{driver}"
|
|
538
|
-
rescue LoadError, NameError => error
|
|
539
|
-
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
|
540
|
-
end
|
|
541
|
-
end
|
|
542
|
-
|
|
543
|
-
driver = Connection.const_get(driver.capitalize)
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
driver
|
|
156
|
+
Client.translate_error!(error)
|
|
547
157
|
end
|
|
548
158
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
@options = options.dup
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
def resolve
|
|
555
|
-
@options
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
def check(client); end
|
|
559
|
-
|
|
560
|
-
class Sentinel < Connector
|
|
561
|
-
def initialize(options)
|
|
562
|
-
super(options)
|
|
563
|
-
|
|
564
|
-
@options[:db] = DEFAULTS.fetch(:db)
|
|
565
|
-
|
|
566
|
-
@sentinels = @options.delete(:sentinels).dup
|
|
567
|
-
@role = (@options[:role] || "master").to_s
|
|
568
|
-
@master = @options[:host]
|
|
569
|
-
end
|
|
570
|
-
|
|
571
|
-
def check(client)
|
|
572
|
-
# Check the instance is really of the role we are looking for.
|
|
573
|
-
# We can't assume the command is supported since it was introduced
|
|
574
|
-
# recently and this client should work with old stuff.
|
|
575
|
-
begin
|
|
576
|
-
role = client.call([:role])[0]
|
|
577
|
-
rescue Redis::CommandError
|
|
578
|
-
# Assume the test is passed if we can't get a reply from ROLE...
|
|
579
|
-
role = @role
|
|
580
|
-
end
|
|
581
|
-
|
|
582
|
-
if role != @role
|
|
583
|
-
client.disconnect
|
|
584
|
-
raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
|
|
585
|
-
end
|
|
586
|
-
end
|
|
587
|
-
|
|
588
|
-
def resolve
|
|
589
|
-
result = case @role
|
|
590
|
-
when "master"
|
|
591
|
-
resolve_master
|
|
592
|
-
when "slave"
|
|
593
|
-
resolve_slave
|
|
594
|
-
else
|
|
595
|
-
raise ArgumentError, "Unknown instance role #{@role}"
|
|
596
|
-
end
|
|
597
|
-
|
|
598
|
-
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
|
599
|
-
end
|
|
600
|
-
|
|
601
|
-
def sentinel_detect
|
|
602
|
-
@sentinels.each do |sentinel|
|
|
603
|
-
client = Client.new(@options.merge({
|
|
604
|
-
host: sentinel[:host] || sentinel["host"],
|
|
605
|
-
port: sentinel[:port] || sentinel["port"],
|
|
606
|
-
username: sentinel[:username] || sentinel["username"],
|
|
607
|
-
password: sentinel[:password] || sentinel["password"],
|
|
608
|
-
reconnect_attempts: 0
|
|
609
|
-
}))
|
|
610
|
-
|
|
611
|
-
begin
|
|
612
|
-
if result = yield(client)
|
|
613
|
-
# This sentinel responded. Make sure we ask it first next time.
|
|
614
|
-
@sentinels.delete(sentinel)
|
|
615
|
-
@sentinels.unshift(sentinel)
|
|
616
|
-
|
|
617
|
-
return result
|
|
618
|
-
end
|
|
619
|
-
rescue BaseConnectionError
|
|
620
|
-
ensure
|
|
621
|
-
client.disconnect
|
|
622
|
-
end
|
|
623
|
-
end
|
|
624
|
-
|
|
625
|
-
raise CannotConnectError, "No sentinels available."
|
|
626
|
-
end
|
|
627
|
-
|
|
628
|
-
def resolve_master
|
|
629
|
-
sentinel_detect do |client|
|
|
630
|
-
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
|
631
|
-
{ host: reply[0], port: reply[1] }
|
|
632
|
-
end
|
|
633
|
-
end
|
|
634
|
-
end
|
|
635
|
-
|
|
636
|
-
def resolve_slave
|
|
637
|
-
sentinel_detect do |client|
|
|
638
|
-
if reply = client.call(["sentinel", "slaves", @master])
|
|
639
|
-
slaves = reply.map { |s| s.each_slice(2).to_h }
|
|
640
|
-
slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
|
|
641
|
-
slaves.reject! { |s| s.fetch('flags').include?('s_down') }
|
|
642
|
-
|
|
643
|
-
if slaves.empty?
|
|
644
|
-
raise CannotConnectError, 'No slaves available.'
|
|
645
|
-
else
|
|
646
|
-
slave = slaves.sample
|
|
647
|
-
{
|
|
648
|
-
host: slave.fetch('ip'),
|
|
649
|
-
port: slave.fetch('port')
|
|
650
|
-
}
|
|
651
|
-
end
|
|
652
|
-
end
|
|
653
|
-
end
|
|
654
|
-
end
|
|
655
|
-
end
|
|
159
|
+
def inherit_socket!
|
|
160
|
+
@inherit_socket = true
|
|
656
161
|
end
|
|
657
162
|
end
|
|
658
163
|
end
|