redis 4.8.0 → 5.0.2
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 +31 -0
- data/README.md +75 -161
- data/lib/redis/client.rb +81 -613
- data/lib/redis/commands/bitmaps.rb +4 -1
- 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 +8 -5
- data/lib/redis/commands/hyper_log_log.rb +1 -1
- data/lib/redis/commands/keys.rb +1 -19
- data/lib/redis/commands/lists.rb +20 -25
- data/lib/redis/commands/pubsub.rb +7 -25
- data/lib/redis/commands/server.rb +15 -15
- data/lib/redis/commands/sets.rb +31 -40
- data/lib/redis/commands/sorted_sets.rb +18 -12
- data/lib/redis/commands/streams.rb +12 -10
- data/lib/redis/commands/strings.rb +14 -13
- data/lib/redis/commands/transactions.rb +7 -31
- data/lib/redis/commands.rb +1 -6
- data/lib/redis/distributed.rb +86 -64
- data/lib/redis/errors.rb +11 -50
- data/lib/redis/hash_ring.rb +26 -26
- data/lib/redis/pipeline.rb +43 -222
- data/lib/redis/subscribe.rb +23 -15
- data/lib/redis/version.rb +1 -1
- data/lib/redis.rb +89 -184
- metadata +11 -55
- 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,667 +1,135 @@
|
|
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
|
-
username: nil,
|
21
|
-
password: nil,
|
22
|
-
db: 0,
|
23
|
-
driver: nil,
|
24
|
-
id: nil,
|
25
|
-
tcp_keepalive: 0,
|
26
|
-
reconnect_attempts: 1,
|
27
|
-
reconnect_delay: 0,
|
28
|
-
reconnect_delay_max: 0.5,
|
29
|
-
inherit_socket: false,
|
30
|
-
logger: nil,
|
31
|
-
sentinels: nil,
|
32
|
-
role: nil
|
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,
|
33
18
|
}.freeze
|
34
19
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
@options[:scheme]
|
39
|
-
end
|
40
|
-
|
41
|
-
def host
|
42
|
-
@options[:host]
|
43
|
-
end
|
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)
|
105
|
-
else
|
106
|
-
Connector.new(@options)
|
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
|
141
|
-
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)
|
20
|
+
class << self
|
21
|
+
def config(**kwargs)
|
22
|
+
super(protocol: 2, **kwargs)
|
147
23
|
end
|
148
24
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
def id
|
153
|
-
@options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
|
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
|
25
|
+
def sentinel(**kwargs)
|
26
|
+
super(protocol: 2, **kwargs)
|
168
27
|
end
|
169
28
|
end
|
170
29
|
|
171
|
-
def
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
30
|
+
def initialize(*)
|
31
|
+
super
|
32
|
+
@inherit_socket = false
|
33
|
+
@pid = nil
|
193
34
|
end
|
35
|
+
ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true)
|
194
36
|
|
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
|
37
|
+
def id
|
38
|
+
config.id
|
212
39
|
end
|
213
40
|
|
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
|
41
|
+
def server_url
|
42
|
+
config.server_url
|
252
43
|
end
|
253
44
|
|
254
|
-
def
|
255
|
-
|
256
|
-
with_socket_timeout(timeout) do
|
257
|
-
call(command, &blk)
|
258
|
-
end
|
259
|
-
rescue ConnectionError
|
260
|
-
retry
|
45
|
+
def timeout
|
46
|
+
config.read_timeout
|
261
47
|
end
|
262
48
|
|
263
|
-
def
|
264
|
-
|
49
|
+
def db
|
50
|
+
config.db
|
265
51
|
end
|
266
52
|
|
267
|
-
def
|
268
|
-
|
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
|
53
|
+
def host
|
54
|
+
config.host unless config.path
|
282
55
|
end
|
283
56
|
|
284
|
-
def
|
285
|
-
|
57
|
+
def port
|
58
|
+
config.port unless config.path
|
286
59
|
end
|
287
60
|
|
288
|
-
def
|
289
|
-
|
61
|
+
def path
|
62
|
+
config.path
|
290
63
|
end
|
291
|
-
alias close disconnect
|
292
64
|
|
293
|
-
def
|
294
|
-
|
295
|
-
connect
|
65
|
+
def username
|
66
|
+
config.username
|
296
67
|
end
|
297
68
|
|
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]
|
69
|
+
def password
|
70
|
+
config.password
|
307
71
|
end
|
308
72
|
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
value
|
314
|
-
end
|
315
|
-
end
|
73
|
+
undef_method :call
|
74
|
+
undef_method :call_once
|
75
|
+
undef_method :call_once_v
|
76
|
+
undef_method :blocking_call
|
316
77
|
|
317
|
-
def
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
end
|
78
|
+
def call_v(command, &block)
|
79
|
+
super(command, &block)
|
80
|
+
rescue ::RedisClient::Error => error
|
81
|
+
raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace
|
322
82
|
end
|
323
83
|
|
324
|
-
def
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
@options[:read_timeout] = timeout # for reconnection
|
331
|
-
yield
|
332
|
-
ensure
|
333
|
-
connection.timeout = self.timeout if connected?
|
334
|
-
@options[:read_timeout] = original
|
84
|
+
def blocking_call_v(timeout, command, &block)
|
85
|
+
if timeout && timeout > 0
|
86
|
+
# Can't use the command timeout argument as the connection timeout
|
87
|
+
# otherwise it would be very racy. So we add an extra 100ms to account for
|
88
|
+
# the network delay.
|
89
|
+
timeout += 0.1
|
335
90
|
end
|
336
|
-
end
|
337
|
-
|
338
|
-
def without_socket_timeout(&blk)
|
339
|
-
with_socket_timeout(0, &blk)
|
340
|
-
end
|
341
91
|
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
ensure
|
346
|
-
@reconnect = original
|
92
|
+
super(timeout, command, &block)
|
93
|
+
rescue ::RedisClient::Error => error
|
94
|
+
raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace
|
347
95
|
end
|
348
96
|
|
349
|
-
def
|
350
|
-
|
97
|
+
def pipelined
|
98
|
+
super
|
99
|
+
rescue ::RedisClient::Error => error
|
100
|
+
raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace
|
351
101
|
end
|
352
102
|
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
begin
|
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
|
376
|
-
end
|
377
|
-
end
|
378
|
-
|
379
|
-
def establish_connection
|
380
|
-
server = @connector.resolve.dup
|
381
|
-
|
382
|
-
@options[:host] = server[:host]
|
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})"
|
103
|
+
def multi
|
104
|
+
super
|
105
|
+
rescue ::RedisClient::Error => error
|
106
|
+
raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace
|
399
107
|
end
|
400
108
|
|
401
|
-
def
|
402
|
-
|
403
|
-
|
404
|
-
attempts = 0
|
405
|
-
|
406
|
-
begin
|
407
|
-
attempts += 1
|
408
|
-
|
409
|
-
if connected?
|
410
|
-
unless inherit_socket? || Process.pid == @pid
|
411
|
-
raise InheritedError,
|
412
|
-
"Tried to use a connection from a child process without reconnecting. " \
|
413
|
-
"You need to reconnect to Redis after forking " \
|
414
|
-
"or set :inherit_socket to true."
|
415
|
-
end
|
416
|
-
else
|
417
|
-
connect
|
418
|
-
end
|
419
|
-
|
420
|
-
yield
|
421
|
-
rescue BaseConnectionError
|
422
|
-
disconnect
|
423
|
-
|
424
|
-
if attempts <= @options[:reconnect_attempts] && @reconnect
|
425
|
-
sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
|
426
|
-
@options[:reconnect_delay_max]].min
|
427
|
-
|
428
|
-
Kernel.sleep(sleep_t)
|
429
|
-
retry
|
430
|
-
else
|
431
|
-
raise
|
432
|
-
end
|
433
|
-
rescue Exception
|
434
|
-
disconnect
|
435
|
-
raise
|
436
|
-
end
|
109
|
+
def disable_reconnection(&block)
|
110
|
+
ensure_connected(retryable: false, &block)
|
437
111
|
end
|
438
112
|
|
439
|
-
def
|
440
|
-
|
441
|
-
|
442
|
-
defaults = DEFAULTS.dup
|
443
|
-
options = options.dup
|
444
|
-
|
445
|
-
defaults.each_key do |key|
|
446
|
-
# Fill in defaults if needed
|
447
|
-
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
448
|
-
|
449
|
-
# Symbolize only keys that are needed
|
450
|
-
options[key] = options[key.to_s] if options.key?(key.to_s)
|
451
|
-
end
|
452
|
-
|
453
|
-
url = options[:url]
|
454
|
-
url = defaults[:url] if url.nil?
|
455
|
-
|
456
|
-
# Override defaults from URL if given
|
457
|
-
if url
|
458
|
-
require "uri"
|
459
|
-
|
460
|
-
uri = URI(url)
|
461
|
-
|
462
|
-
case uri.scheme
|
463
|
-
when "unix"
|
464
|
-
defaults[:path] = uri.path
|
465
|
-
when "redis", "rediss"
|
466
|
-
defaults[:scheme] = uri.scheme
|
467
|
-
defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
|
468
|
-
defaults[:port] = uri.port if uri.port
|
469
|
-
defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
|
470
|
-
defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
|
471
|
-
defaults[:db] = uri.path[1..-1].to_i if uri.path
|
472
|
-
defaults[:role] = :master
|
473
|
-
else
|
474
|
-
raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
|
475
|
-
end
|
476
|
-
|
477
|
-
defaults[:ssl] = true if uri.scheme == "rediss"
|
478
|
-
end
|
479
|
-
|
480
|
-
# Use default when option is not specified or nil
|
481
|
-
defaults.each_key do |key|
|
482
|
-
options[key] = defaults[key] if options[key].nil?
|
483
|
-
end
|
484
|
-
|
485
|
-
if options[:path]
|
486
|
-
# Unix socket
|
487
|
-
options[:scheme] = "unix"
|
488
|
-
options.delete(:host)
|
489
|
-
options.delete(:port)
|
490
|
-
else
|
491
|
-
# TCP socket
|
492
|
-
options[:host] = options[:host].to_s
|
493
|
-
options[:port] = options[:port].to_i
|
494
|
-
end
|
495
|
-
|
496
|
-
if options.key?(:timeout)
|
497
|
-
options[:connect_timeout] ||= options[:timeout]
|
498
|
-
options[:read_timeout] ||= options[:timeout]
|
499
|
-
options[:write_timeout] ||= options[:timeout]
|
500
|
-
end
|
501
|
-
|
502
|
-
options[:connect_timeout] = Float(options[:connect_timeout])
|
503
|
-
options[:read_timeout] = Float(options[:read_timeout])
|
504
|
-
options[:write_timeout] = Float(options[:write_timeout])
|
505
|
-
|
506
|
-
options[:reconnect_attempts] = options[:reconnect_attempts].to_i
|
507
|
-
options[:reconnect_delay] = options[:reconnect_delay].to_f
|
508
|
-
options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
|
509
|
-
|
510
|
-
options[:db] = options[:db].to_i
|
511
|
-
options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
|
512
|
-
|
513
|
-
case options[:tcp_keepalive]
|
514
|
-
when Hash
|
515
|
-
%i[time intvl probes].each do |key|
|
516
|
-
unless options[:tcp_keepalive][key].is_a?(Integer)
|
517
|
-
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
518
|
-
end
|
519
|
-
end
|
520
|
-
|
521
|
-
when Integer
|
522
|
-
if options[:tcp_keepalive] >= 60
|
523
|
-
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
524
|
-
|
525
|
-
elsif options[:tcp_keepalive] >= 30
|
526
|
-
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
527
|
-
|
528
|
-
elsif options[:tcp_keepalive] >= 5
|
529
|
-
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
530
|
-
end
|
531
|
-
end
|
532
|
-
|
533
|
-
options[:_parsed] = true
|
534
|
-
|
535
|
-
options
|
113
|
+
def inherit_socket!
|
114
|
+
@inherit_socket = true
|
536
115
|
end
|
537
116
|
|
538
|
-
def
|
539
|
-
|
540
|
-
|
541
|
-
if driver.is_a?(String)
|
542
|
-
begin
|
543
|
-
require_relative "connection/#{driver}"
|
544
|
-
rescue LoadError, NameError
|
545
|
-
begin
|
546
|
-
require "redis/connection/#{driver}"
|
547
|
-
rescue LoadError, NameError => error
|
548
|
-
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
549
|
-
end
|
550
|
-
end
|
551
|
-
|
552
|
-
driver = Connection.const_get(driver.capitalize)
|
553
|
-
end
|
554
|
-
|
555
|
-
driver
|
117
|
+
def close
|
118
|
+
super
|
119
|
+
@pid = nil
|
556
120
|
end
|
557
121
|
|
558
|
-
|
559
|
-
def initialize(options)
|
560
|
-
@options = options.dup
|
561
|
-
end
|
122
|
+
private
|
562
123
|
|
563
|
-
|
564
|
-
|
124
|
+
def ensure_connected(retryable: true)
|
125
|
+
unless @inherit_socket || (@pid ||= Process.pid) == Process.pid
|
126
|
+
raise InheritedError,
|
127
|
+
"Tried to use a connection from a child process without reconnecting. " \
|
128
|
+
"You need to reconnect to Redis after forking " \
|
129
|
+
"or set :inherit_socket to true."
|
565
130
|
end
|
566
131
|
|
567
|
-
|
568
|
-
|
569
|
-
class Sentinel < Connector
|
570
|
-
def initialize(options)
|
571
|
-
super(options)
|
572
|
-
|
573
|
-
@options[:db] = DEFAULTS.fetch(:db)
|
574
|
-
|
575
|
-
@sentinels = @options.delete(:sentinels).dup
|
576
|
-
@role = (@options[:role] || "master").to_s
|
577
|
-
@master = @options[:host]
|
578
|
-
end
|
579
|
-
|
580
|
-
def check(client)
|
581
|
-
# Check the instance is really of the role we are looking for.
|
582
|
-
# We can't assume the command is supported since it was introduced
|
583
|
-
# recently and this client should work with old stuff.
|
584
|
-
begin
|
585
|
-
role = client.call([:role])[0]
|
586
|
-
rescue Redis::CommandError
|
587
|
-
# Assume the test is passed if we can't get a reply from ROLE...
|
588
|
-
role = @role
|
589
|
-
end
|
590
|
-
|
591
|
-
if role != @role
|
592
|
-
client.disconnect
|
593
|
-
raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
|
594
|
-
end
|
595
|
-
end
|
596
|
-
|
597
|
-
def resolve
|
598
|
-
result = case @role
|
599
|
-
when "master"
|
600
|
-
resolve_master
|
601
|
-
when "slave"
|
602
|
-
resolve_slave
|
603
|
-
else
|
604
|
-
raise ArgumentError, "Unknown instance role #{@role}"
|
605
|
-
end
|
606
|
-
|
607
|
-
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
608
|
-
end
|
609
|
-
|
610
|
-
def sentinel_detect
|
611
|
-
@sentinels.each do |sentinel|
|
612
|
-
client = Client.new(@options.merge({
|
613
|
-
host: sentinel[:host] || sentinel["host"],
|
614
|
-
port: sentinel[:port] || sentinel["port"],
|
615
|
-
username: sentinel[:username] || sentinel["username"],
|
616
|
-
password: sentinel[:password] || sentinel["password"],
|
617
|
-
reconnect_attempts: 0
|
618
|
-
}))
|
619
|
-
|
620
|
-
begin
|
621
|
-
if result = yield(client)
|
622
|
-
# This sentinel responded. Make sure we ask it first next time.
|
623
|
-
@sentinels.delete(sentinel)
|
624
|
-
@sentinels.unshift(sentinel)
|
625
|
-
|
626
|
-
return result
|
627
|
-
end
|
628
|
-
rescue BaseConnectionError
|
629
|
-
ensure
|
630
|
-
client.disconnect
|
631
|
-
end
|
632
|
-
end
|
633
|
-
|
634
|
-
raise CannotConnectError, "No sentinels available."
|
635
|
-
end
|
636
|
-
|
637
|
-
def resolve_master
|
638
|
-
sentinel_detect do |client|
|
639
|
-
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
640
|
-
{ host: reply[0], port: reply[1] }
|
641
|
-
end
|
642
|
-
end
|
643
|
-
end
|
644
|
-
|
645
|
-
def resolve_slave
|
646
|
-
sentinel_detect do |client|
|
647
|
-
if reply = client.call(["sentinel", "slaves", @master])
|
648
|
-
slaves = reply.map { |s| s.each_slice(2).to_h }
|
649
|
-
slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
|
650
|
-
slaves.reject! { |s| s.fetch('flags').include?('s_down') }
|
651
|
-
|
652
|
-
if slaves.empty?
|
653
|
-
raise CannotConnectError, 'No slaves available.'
|
654
|
-
else
|
655
|
-
slave = slaves.sample
|
656
|
-
{
|
657
|
-
host: slave.fetch('ip'),
|
658
|
-
port: slave.fetch('port')
|
659
|
-
}
|
660
|
-
end
|
661
|
-
end
|
662
|
-
end
|
663
|
-
end
|
664
|
-
end
|
132
|
+
super
|
665
133
|
end
|
666
134
|
end
|
667
135
|
end
|