redis 4.8.1 → 5.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +65 -0
- data/README.md +101 -161
- data/lib/redis/client.rb +82 -616
- 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 +3 -3
- data/lib/redis/commands/hashes.rb +9 -6
- data/lib/redis/commands/hyper_log_log.rb +1 -1
- data/lib/redis/commands/keys.rb +21 -23
- data/lib/redis/commands/lists.rb +74 -25
- data/lib/redis/commands/pubsub.rb +28 -25
- data/lib/redis/commands/server.rb +15 -15
- data/lib/redis/commands/sets.rb +31 -40
- data/lib/redis/commands/sorted_sets.rb +84 -12
- data/lib/redis/commands/streams.rb +39 -19
- data/lib/redis/commands/strings.rb +18 -17
- data/lib/redis/commands/transactions.rb +7 -31
- data/lib/redis/commands.rb +4 -7
- data/lib/redis/distributed.rb +128 -68
- data/lib/redis/errors.rb +15 -50
- data/lib/redis/hash_ring.rb +26 -26
- data/lib/redis/pipeline.rb +43 -222
- data/lib/redis/subscribe.rb +50 -14
- data/lib/redis/version.rb +1 -1
- data/lib/redis.rb +76 -184
- metadata +10 -54
- 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,124 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require "cgi"
|
5
|
-
require "redis/errors"
|
3
|
+
require 'redis-client'
|
6
4
|
|
7
5
|
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
|
-
def port
|
46
|
-
@options[:port]
|
47
|
-
end
|
48
|
-
|
49
|
-
def path
|
50
|
-
@options[:path]
|
51
|
-
end
|
52
|
-
|
53
|
-
def read_timeout
|
54
|
-
@options[:read_timeout]
|
55
|
-
end
|
56
|
-
|
57
|
-
def connect_timeout
|
58
|
-
@options[:connect_timeout]
|
59
|
-
end
|
60
|
-
|
61
|
-
def timeout
|
62
|
-
@options[:read_timeout]
|
63
|
-
end
|
64
|
-
|
65
|
-
def username
|
66
|
-
@options[:username]
|
67
|
-
end
|
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)
|
6
|
+
class Client < ::RedisClient
|
7
|
+
ERROR_MAPPING = {
|
8
|
+
RedisClient::ConnectionError => Redis::ConnectionError,
|
9
|
+
RedisClient::CommandError => Redis::CommandError,
|
10
|
+
RedisClient::ReadTimeoutError => Redis::TimeoutError,
|
11
|
+
RedisClient::CannotConnectError => Redis::CannotConnectError,
|
12
|
+
RedisClient::AuthenticationError => Redis::CannotConnectError,
|
13
|
+
RedisClient::FailoverError => Redis::CannotConnectError,
|
14
|
+
RedisClient::PermissionError => Redis::PermissionError,
|
15
|
+
RedisClient::WrongTypeError => Redis::WrongTypeError,
|
16
|
+
RedisClient::ReadOnlyError => Redis::ReadOnlyError,
|
17
|
+
RedisClient::ProtocolError => Redis::ProtocolError,
|
18
|
+
RedisClient::OutOfMemoryError => Redis::OutOfMemoryError,
|
19
|
+
}
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def config(**kwargs)
|
23
|
+
super(protocol: 2, **kwargs)
|
24
|
+
end
|
25
|
+
|
26
|
+
def sentinel(**kwargs)
|
27
|
+
super(protocol: 2, **kwargs, client_implementation: ::RedisClient)
|
28
|
+
end
|
29
|
+
|
30
|
+
def translate_error!(error, mapping: ERROR_MAPPING)
|
31
|
+
redis_error = translate_error_class(error.class, mapping: mapping)
|
32
|
+
raise redis_error, error.message, error.backtrace
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def translate_error_class(error_class, mapping: ERROR_MAPPING)
|
38
|
+
mapping.fetch(error_class)
|
39
|
+
rescue IndexError
|
40
|
+
if (client_error = error_class.ancestors.find { |a| mapping[a] })
|
41
|
+
mapping[error_class] = mapping[client_error]
|
105
42
|
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
|
43
|
+
raise
|
141
44
|
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
45
|
end
|
148
|
-
|
149
|
-
self
|
150
46
|
end
|
151
47
|
|
152
48
|
def id
|
153
|
-
|
49
|
+
config.id
|
154
50
|
end
|
155
51
|
|
156
|
-
def
|
157
|
-
|
52
|
+
def server_url
|
53
|
+
config.server_url
|
158
54
|
end
|
159
55
|
|
160
|
-
def
|
161
|
-
|
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
|
193
|
-
end
|
194
|
-
|
195
|
-
def call_pipeline(pipeline)
|
196
|
-
return [] if pipeline.futures.empty?
|
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
|
212
|
-
end
|
213
|
-
|
214
|
-
def call_pipelined(pipeline)
|
215
|
-
return [] if pipeline.futures.empty?
|
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
|
282
|
-
end
|
283
|
-
|
284
|
-
def connected?
|
285
|
-
!!(connection && connection.connected?)
|
286
|
-
end
|
287
|
-
|
288
|
-
def disconnect
|
289
|
-
connection.disconnect if connected?
|
56
|
+
def timeout
|
57
|
+
config.read_timeout
|
290
58
|
end
|
291
|
-
alias close disconnect
|
292
59
|
|
293
|
-
def
|
294
|
-
|
295
|
-
connect
|
60
|
+
def db
|
61
|
+
config.db
|
296
62
|
end
|
297
63
|
|
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]
|
64
|
+
def host
|
65
|
+
config.host unless config.path
|
307
66
|
end
|
308
67
|
|
309
|
-
def
|
310
|
-
|
311
|
-
value = connection.read
|
312
|
-
@pending_reads -= 1
|
313
|
-
value
|
314
|
-
end
|
68
|
+
def port
|
69
|
+
config.port unless config.path
|
315
70
|
end
|
316
71
|
|
317
|
-
def
|
318
|
-
|
319
|
-
@pending_reads += 1
|
320
|
-
connection.write(command)
|
321
|
-
end
|
72
|
+
def path
|
73
|
+
config.path
|
322
74
|
end
|
323
75
|
|
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
|
76
|
+
def username
|
77
|
+
config.username
|
336
78
|
end
|
337
79
|
|
338
|
-
def
|
339
|
-
|
80
|
+
def password
|
81
|
+
config.password
|
340
82
|
end
|
341
83
|
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
@reconnect = original
|
347
|
-
end
|
84
|
+
undef_method :call
|
85
|
+
undef_method :call_once
|
86
|
+
undef_method :call_once_v
|
87
|
+
undef_method :blocking_call
|
348
88
|
|
349
|
-
def
|
350
|
-
|
89
|
+
def call_v(command, &block)
|
90
|
+
super(command, &block)
|
91
|
+
rescue ::RedisClient::Error => error
|
92
|
+
Client.translate_error!(error)
|
351
93
|
end
|
352
94
|
|
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
|
95
|
+
def blocking_call_v(timeout, command, &block)
|
96
|
+
if timeout && timeout > 0
|
97
|
+
# Can't use the command timeout argument as the connection timeout
|
98
|
+
# otherwise it would be very racy. So we add the regular read_timeout on top
|
99
|
+
# to account for the network delay.
|
100
|
+
timeout += config.read_timeout
|
376
101
|
end
|
377
|
-
end
|
378
|
-
|
379
|
-
def establish_connection
|
380
|
-
server = @connector.resolve.dup
|
381
102
|
|
382
|
-
|
383
|
-
|
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})"
|
103
|
+
super(timeout, command, &block)
|
104
|
+
rescue ::RedisClient::Error => error
|
105
|
+
Client.translate_error!(error)
|
399
106
|
end
|
400
107
|
|
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
|
108
|
+
def pipelined
|
109
|
+
super
|
110
|
+
rescue ::RedisClient::Error => error
|
111
|
+
Client.translate_error!(error)
|
428
112
|
end
|
429
113
|
|
430
|
-
def
|
431
|
-
|
432
|
-
|
433
|
-
|
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
|
525
|
-
|
526
|
-
options
|
114
|
+
def multi(watch: nil)
|
115
|
+
super
|
116
|
+
rescue ::RedisClient::Error => error
|
117
|
+
Client.translate_error!(error)
|
527
118
|
end
|
528
119
|
|
529
|
-
def
|
530
|
-
|
531
|
-
|
532
|
-
if driver.is_a?(String)
|
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
|
547
|
-
end
|
548
|
-
|
549
|
-
class Connector
|
550
|
-
def initialize(options)
|
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
|
120
|
+
def inherit_socket!
|
121
|
+
@inherit_socket = true
|
656
122
|
end
|
657
123
|
end
|
658
124
|
end
|