redis 3.2.0 → 4.6.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 +5 -5
- data/CHANGELOG.md +278 -15
- data/README.md +260 -76
- data/lib/redis/client.rb +239 -115
- data/lib/redis/cluster/command.rb +79 -0
- data/lib/redis/cluster/command_loader.rb +33 -0
- data/lib/redis/cluster/key_slot_converter.rb +72 -0
- data/lib/redis/cluster/node.rb +120 -0
- data/lib/redis/cluster/node_key.rb +31 -0
- data/lib/redis/cluster/node_loader.rb +37 -0
- data/lib/redis/cluster/option.rb +93 -0
- data/lib/redis/cluster/slot.rb +86 -0
- data/lib/redis/cluster/slot_loader.rb +49 -0
- data/lib/redis/cluster.rb +315 -0
- data/lib/redis/commands/bitmaps.rb +63 -0
- data/lib/redis/commands/cluster.rb +45 -0
- data/lib/redis/commands/connection.rb +58 -0
- data/lib/redis/commands/geo.rb +84 -0
- data/lib/redis/commands/hashes.rb +251 -0
- data/lib/redis/commands/hyper_log_log.rb +37 -0
- data/lib/redis/commands/keys.rb +411 -0
- data/lib/redis/commands/lists.rb +289 -0
- data/lib/redis/commands/pubsub.rb +72 -0
- data/lib/redis/commands/scripting.rb +114 -0
- data/lib/redis/commands/server.rb +188 -0
- data/lib/redis/commands/sets.rb +207 -0
- data/lib/redis/commands/sorted_sets.rb +804 -0
- data/lib/redis/commands/streams.rb +382 -0
- data/lib/redis/commands/strings.rb +313 -0
- data/lib/redis/commands/transactions.rb +92 -0
- data/lib/redis/commands.rb +242 -0
- data/lib/redis/connection/command_helper.rb +7 -10
- data/lib/redis/connection/hiredis.rb +11 -6
- data/lib/redis/connection/registry.rb +2 -1
- data/lib/redis/connection/ruby.rb +173 -64
- data/lib/redis/connection/synchrony.rb +32 -8
- data/lib/redis/connection.rb +3 -1
- data/lib/redis/distributed.rb +233 -74
- data/lib/redis/errors.rb +48 -0
- data/lib/redis/hash_ring.rb +30 -72
- data/lib/redis/pipeline.rb +145 -12
- data/lib/redis/subscribe.rb +20 -13
- data/lib/redis/version.rb +3 -1
- data/lib/redis.rb +171 -2476
- metadata +71 -165
- data/.gitignore +0 -15
- data/.travis/Gemfile +0 -11
- data/.travis.yml +0 -54
- data/.yardopts +0 -3
- data/Gemfile +0 -4
- data/Rakefile +0 -68
- data/benchmarking/logging.rb +0 -71
- data/benchmarking/pipeline.rb +0 -51
- data/benchmarking/speed.rb +0 -21
- data/benchmarking/suite.rb +0 -24
- data/benchmarking/worker.rb +0 -71
- data/examples/basic.rb +0 -15
- data/examples/consistency.rb +0 -114
- data/examples/dist_redis.rb +0 -43
- data/examples/incr-decr.rb +0 -17
- data/examples/list.rb +0 -26
- data/examples/pubsub.rb +0 -37
- data/examples/sentinel/sentinel.conf +0 -9
- data/examples/sentinel/start +0 -49
- data/examples/sentinel.rb +0 -41
- data/examples/sets.rb +0 -36
- data/examples/unicorn/config.ru +0 -3
- data/examples/unicorn/unicorn.rb +0 -20
- data/redis.gemspec +0 -43
- data/test/bitpos_test.rb +0 -69
- data/test/blocking_commands_test.rb +0 -42
- data/test/command_map_test.rb +0 -30
- data/test/commands_on_hashes_test.rb +0 -21
- data/test/commands_on_hyper_log_log_test.rb +0 -21
- data/test/commands_on_lists_test.rb +0 -20
- data/test/commands_on_sets_test.rb +0 -77
- data/test/commands_on_sorted_sets_test.rb +0 -123
- data/test/commands_on_strings_test.rb +0 -101
- data/test/commands_on_value_types_test.rb +0 -131
- data/test/connection_handling_test.rb +0 -189
- data/test/db/.gitkeep +0 -0
- data/test/distributed_blocking_commands_test.rb +0 -46
- data/test/distributed_commands_on_hashes_test.rb +0 -10
- data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
- data/test/distributed_commands_on_lists_test.rb +0 -22
- data/test/distributed_commands_on_sets_test.rb +0 -83
- data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
- data/test/distributed_commands_on_strings_test.rb +0 -59
- data/test/distributed_commands_on_value_types_test.rb +0 -95
- data/test/distributed_commands_requiring_clustering_test.rb +0 -164
- data/test/distributed_connection_handling_test.rb +0 -23
- data/test/distributed_internals_test.rb +0 -70
- data/test/distributed_key_tags_test.rb +0 -52
- data/test/distributed_persistence_control_commands_test.rb +0 -26
- data/test/distributed_publish_subscribe_test.rb +0 -92
- data/test/distributed_remote_server_control_commands_test.rb +0 -66
- data/test/distributed_scripting_test.rb +0 -102
- data/test/distributed_sorting_test.rb +0 -20
- data/test/distributed_test.rb +0 -58
- data/test/distributed_transactions_test.rb +0 -32
- data/test/encoding_test.rb +0 -18
- data/test/error_replies_test.rb +0 -59
- data/test/fork_safety_test.rb +0 -65
- data/test/helper.rb +0 -232
- data/test/helper_test.rb +0 -24
- data/test/internals_test.rb +0 -434
- data/test/lint/blocking_commands.rb +0 -150
- data/test/lint/hashes.rb +0 -162
- data/test/lint/hyper_log_log.rb +0 -60
- data/test/lint/lists.rb +0 -143
- data/test/lint/sets.rb +0 -125
- data/test/lint/sorted_sets.rb +0 -238
- data/test/lint/strings.rb +0 -260
- data/test/lint/value_types.rb +0 -122
- data/test/persistence_control_commands_test.rb +0 -26
- data/test/pipelining_commands_test.rb +0 -242
- data/test/publish_subscribe_test.rb +0 -210
- data/test/remote_server_control_commands_test.rb +0 -117
- data/test/scanning_test.rb +0 -413
- data/test/scripting_test.rb +0 -78
- data/test/sorting_test.rb +0 -59
- data/test/support/connection/hiredis.rb +0 -1
- data/test/support/connection/ruby.rb +0 -1
- data/test/support/connection/synchrony.rb +0 -17
- data/test/support/redis_mock.rb +0 -115
- data/test/support/wire/synchrony.rb +0 -24
- data/test/support/wire/thread.rb +0 -5
- data/test/synchrony_driver.rb +0 -88
- data/test/test.conf +0 -9
- data/test/thread_safety_test.rb +0 -32
- data/test/transactions_test.rb +0 -264
- data/test/unknown_commands_test.rb +0 -14
- data/test/url_param_test.rb +0 -132
data/lib/redis/client.rb
CHANGED
@@ -1,29 +1,38 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "socket"
|
3
4
|
require "cgi"
|
5
|
+
require "redis/errors"
|
4
6
|
|
5
7
|
class Redis
|
6
8
|
class Client
|
7
|
-
|
9
|
+
# Defaults are also used for converting string keys to symbols.
|
8
10
|
DEFAULTS = {
|
9
|
-
:
|
10
|
-
:
|
11
|
-
:
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:
|
15
|
-
:
|
16
|
-
:
|
17
|
-
:
|
18
|
-
:
|
19
|
-
:
|
20
|
-
:
|
21
|
-
:
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
11
|
+
url: -> { ENV["REDIS_URL"] },
|
12
|
+
scheme: "redis",
|
13
|
+
host: "127.0.0.1",
|
14
|
+
port: 6379,
|
15
|
+
path: nil,
|
16
|
+
read_timeout: nil,
|
17
|
+
write_timeout: nil,
|
18
|
+
connect_timeout: nil,
|
19
|
+
timeout: 5.0,
|
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
|
33
|
+
}.freeze
|
34
|
+
|
35
|
+
attr_reader :options, :connection, :command_map
|
27
36
|
|
28
37
|
def scheme
|
29
38
|
@options[:scheme]
|
@@ -41,8 +50,20 @@ class Redis
|
|
41
50
|
@options[:path]
|
42
51
|
end
|
43
52
|
|
53
|
+
def read_timeout
|
54
|
+
@options[:read_timeout]
|
55
|
+
end
|
56
|
+
|
57
|
+
def connect_timeout
|
58
|
+
@options[:connect_timeout]
|
59
|
+
end
|
60
|
+
|
44
61
|
def timeout
|
45
|
-
@options[:
|
62
|
+
@options[:read_timeout]
|
63
|
+
end
|
64
|
+
|
65
|
+
def username
|
66
|
+
@options[:username]
|
46
67
|
end
|
47
68
|
|
48
69
|
def password
|
@@ -66,8 +87,6 @@ class Redis
|
|
66
87
|
end
|
67
88
|
|
68
89
|
attr_accessor :logger
|
69
|
-
attr_reader :connection
|
70
|
-
attr_reader :command_map
|
71
90
|
|
72
91
|
def initialize(options = {})
|
73
92
|
@options = _parse_options(options)
|
@@ -76,11 +95,16 @@ class Redis
|
|
76
95
|
@connection = nil
|
77
96
|
@command_map = {}
|
78
97
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
84
108
|
end
|
85
109
|
|
86
110
|
def connect
|
@@ -89,8 +113,36 @@ class Redis
|
|
89
113
|
# Don't try to reconnect when the connection is fresh
|
90
114
|
with_reconnect(false) do
|
91
115
|
establish_connection
|
92
|
-
|
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]
|
93
144
|
call [:select, db] if db != 0
|
145
|
+
call [:client, :setname, @options[:id]] if @options[:id]
|
94
146
|
@connector.check(self)
|
95
147
|
end
|
96
148
|
|
@@ -105,21 +157,21 @@ class Redis
|
|
105
157
|
path || "#{host}:#{port}"
|
106
158
|
end
|
107
159
|
|
108
|
-
def call(command
|
160
|
+
def call(command)
|
109
161
|
reply = process([command]) { read }
|
110
162
|
raise reply if reply.is_a?(CommandError)
|
111
163
|
|
112
|
-
if
|
113
|
-
|
164
|
+
if block_given? && reply != 'QUEUED'
|
165
|
+
yield reply
|
114
166
|
else
|
115
167
|
reply
|
116
168
|
end
|
117
169
|
end
|
118
170
|
|
119
|
-
def call_loop(command)
|
171
|
+
def call_loop(command, timeout = 0)
|
120
172
|
error = nil
|
121
173
|
|
122
|
-
result =
|
174
|
+
result = with_socket_timeout(timeout) do
|
123
175
|
process([command]) do
|
124
176
|
loop do
|
125
177
|
reply = read
|
@@ -141,13 +193,16 @@ class Redis
|
|
141
193
|
end
|
142
194
|
|
143
195
|
def call_pipeline(pipeline)
|
196
|
+
return [] if pipeline.futures.empty?
|
197
|
+
|
144
198
|
with_reconnect pipeline.with_reconnect? do
|
145
199
|
begin
|
146
|
-
pipeline.finish(call_pipelined(pipeline
|
200
|
+
pipeline.finish(call_pipelined(pipeline)).tap do
|
147
201
|
self.db = pipeline.db if pipeline.db
|
148
202
|
end
|
149
203
|
rescue ConnectionError => e
|
150
204
|
return nil if pipeline.shutdown?
|
205
|
+
|
151
206
|
# Assume the pipeline was sent in one piece, but execution of
|
152
207
|
# SHUTDOWN caused none of the replies for commands that were executed
|
153
208
|
# prior to it from coming back around.
|
@@ -156,8 +211,8 @@ class Redis
|
|
156
211
|
end
|
157
212
|
end
|
158
213
|
|
159
|
-
def call_pipelined(
|
160
|
-
return [] if
|
214
|
+
def call_pipelined(pipeline)
|
215
|
+
return [] if pipeline.futures.empty?
|
161
216
|
|
162
217
|
# The method #ensure_connected (called from #process) reconnects once on
|
163
218
|
# I/O errors. To make an effort in making sure that commands are not
|
@@ -167,19 +222,28 @@ class Redis
|
|
167
222
|
# already successfully executed commands. To circumvent this, don't retry
|
168
223
|
# after the first reply has been read successfully.
|
169
224
|
|
225
|
+
commands = pipeline.commands
|
226
|
+
|
170
227
|
result = Array.new(commands.size)
|
171
228
|
reconnect = @reconnect
|
172
229
|
|
173
230
|
begin
|
174
|
-
|
175
|
-
result[0] = read
|
176
|
-
|
177
|
-
@reconnect = false
|
231
|
+
exception = nil
|
178
232
|
|
179
|
-
|
180
|
-
|
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)
|
181
243
|
end
|
182
244
|
end
|
245
|
+
|
246
|
+
raise exception if exception
|
183
247
|
ensure
|
184
248
|
@reconnect = reconnect
|
185
249
|
end
|
@@ -187,7 +251,8 @@ class Redis
|
|
187
251
|
result
|
188
252
|
end
|
189
253
|
|
190
|
-
def call_with_timeout(command,
|
254
|
+
def call_with_timeout(command, extra_timeout, &blk)
|
255
|
+
timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout
|
191
256
|
with_socket_timeout(timeout) do
|
192
257
|
call(command, &blk)
|
193
258
|
end
|
@@ -217,12 +282,13 @@ class Redis
|
|
217
282
|
end
|
218
283
|
|
219
284
|
def connected?
|
220
|
-
!!
|
285
|
+
!!(connection && connection.connected?)
|
221
286
|
end
|
222
287
|
|
223
288
|
def disconnect
|
224
289
|
connection.disconnect if connected?
|
225
290
|
end
|
291
|
+
alias close disconnect
|
226
292
|
|
227
293
|
def reconnect
|
228
294
|
disconnect
|
@@ -242,24 +308,30 @@ class Redis
|
|
242
308
|
|
243
309
|
def read
|
244
310
|
io do
|
245
|
-
connection.read
|
311
|
+
value = connection.read
|
312
|
+
@pending_reads -= 1
|
313
|
+
value
|
246
314
|
end
|
247
315
|
end
|
248
316
|
|
249
317
|
def write(command)
|
250
318
|
io do
|
319
|
+
@pending_reads += 1
|
251
320
|
connection.write(command)
|
252
321
|
end
|
253
322
|
end
|
254
323
|
|
255
324
|
def with_socket_timeout(timeout)
|
256
325
|
connect unless connected?
|
326
|
+
original = @options[:read_timeout]
|
257
327
|
|
258
328
|
begin
|
259
329
|
connection.timeout = timeout
|
330
|
+
@options[:read_timeout] = timeout # for reconnection
|
260
331
|
yield
|
261
332
|
ensure
|
262
333
|
connection.timeout = self.timeout if connected?
|
334
|
+
@options[:read_timeout] = original
|
263
335
|
end
|
264
336
|
end
|
265
337
|
|
@@ -267,30 +339,27 @@ class Redis
|
|
267
339
|
with_socket_timeout(0, &blk)
|
268
340
|
end
|
269
341
|
|
270
|
-
def with_reconnect(val=true)
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
@reconnect = original
|
276
|
-
end
|
342
|
+
def with_reconnect(val = true)
|
343
|
+
original, @reconnect = @reconnect, val
|
344
|
+
yield
|
345
|
+
ensure
|
346
|
+
@reconnect = original
|
277
347
|
end
|
278
348
|
|
279
349
|
def without_reconnect(&blk)
|
280
350
|
with_reconnect(false, &blk)
|
281
351
|
end
|
282
352
|
|
283
|
-
|
353
|
+
protected
|
284
354
|
|
285
355
|
def logging(commands)
|
286
|
-
return yield unless @logger
|
356
|
+
return yield unless @logger&.debug?
|
287
357
|
|
288
358
|
begin
|
289
359
|
commands.each do |name, *args|
|
290
360
|
logged_args = args.map do |a|
|
291
|
-
|
292
|
-
|
293
|
-
when a.respond_to?(:to_s) then a.to_s
|
361
|
+
if a.respond_to?(:inspect) then a.inspect
|
362
|
+
elsif a.respond_to?(:to_s) then a.to_s
|
294
363
|
else
|
295
364
|
# handle poorly-behaved descendants of BasicObject
|
296
365
|
klass = a.instance_exec { (class << self; self end).superclass }
|
@@ -311,16 +380,27 @@ class Redis
|
|
311
380
|
server = @connector.resolve.dup
|
312
381
|
|
313
382
|
@options[:host] = server[:host]
|
314
|
-
@options[:port] = server[:port]
|
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
|
315
397
|
|
316
|
-
|
317
|
-
rescue TimeoutError
|
318
|
-
raise CannotConnectError, "Timed out connecting to Redis on #{location}"
|
319
|
-
rescue Errno::ECONNREFUSED
|
320
|
-
raise CannotConnectError, "Error connecting to Redis on #{location} (ECONNREFUSED)"
|
398
|
+
raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
|
321
399
|
end
|
322
400
|
|
323
401
|
def ensure_connected
|
402
|
+
disconnect if @pending_reads > 0
|
403
|
+
|
324
404
|
attempts = 0
|
325
405
|
|
326
406
|
begin
|
@@ -329,19 +409,23 @@ class Redis
|
|
329
409
|
if connected?
|
330
410
|
unless inherit_socket? || Process.pid == @pid
|
331
411
|
raise InheritedError,
|
332
|
-
|
333
|
-
|
334
|
-
|
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."
|
335
415
|
end
|
336
416
|
else
|
337
417
|
connect
|
338
418
|
end
|
339
419
|
|
340
420
|
yield
|
341
|
-
rescue
|
421
|
+
rescue BaseConnectionError
|
342
422
|
disconnect
|
343
423
|
|
344
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)
|
345
429
|
retry
|
346
430
|
else
|
347
431
|
raise
|
@@ -353,20 +437,21 @@ class Redis
|
|
353
437
|
end
|
354
438
|
|
355
439
|
def _parse_options(options)
|
440
|
+
return options if options[:_parsed]
|
441
|
+
|
356
442
|
defaults = DEFAULTS.dup
|
357
443
|
options = options.dup
|
358
444
|
|
359
|
-
defaults.
|
445
|
+
defaults.each_key do |key|
|
360
446
|
# Fill in defaults if needed
|
361
|
-
if defaults[key].respond_to?(:call)
|
362
|
-
defaults[key] = defaults[key].call
|
363
|
-
end
|
447
|
+
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
364
448
|
|
365
449
|
# Symbolize only keys that are needed
|
366
|
-
options[key] = options[key.to_s] if options.
|
450
|
+
options[key] = options[key.to_s] if options.key?(key.to_s)
|
367
451
|
end
|
368
452
|
|
369
|
-
url = options[:url]
|
453
|
+
url = options[:url]
|
454
|
+
url = defaults[:url] if url.nil?
|
370
455
|
|
371
456
|
# Override defaults from URL if given
|
372
457
|
if url
|
@@ -374,25 +459,26 @@ class Redis
|
|
374
459
|
|
375
460
|
uri = URI(url)
|
376
461
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
raise ArgumentError, "invalid url" unless uri.host
|
382
|
-
|
462
|
+
case uri.scheme
|
463
|
+
when "unix"
|
464
|
+
defaults[:path] = uri.path
|
465
|
+
when "redis", "rediss"
|
383
466
|
defaults[:scheme] = uri.scheme
|
384
|
-
defaults[:host] = uri.host
|
467
|
+
defaults[:host] = uri.host if uri.host
|
385
468
|
defaults[:port] = uri.port if uri.port
|
386
|
-
defaults[:
|
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?
|
387
471
|
defaults[:db] = uri.path[1..-1].to_i if uri.path
|
388
472
|
defaults[:role] = :master
|
389
473
|
else
|
390
474
|
raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
|
391
475
|
end
|
476
|
+
|
477
|
+
defaults[:ssl] = true if uri.scheme == "rediss"
|
392
478
|
end
|
393
479
|
|
394
480
|
# Use default when option is not specified or nil
|
395
|
-
defaults.
|
481
|
+
defaults.each_key do |key|
|
396
482
|
options[key] = defaults[key] if options[key].nil?
|
397
483
|
end
|
398
484
|
|
@@ -407,43 +493,63 @@ class Redis
|
|
407
493
|
options[:port] = options[:port].to_i
|
408
494
|
end
|
409
495
|
|
410
|
-
|
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
|
+
|
411
510
|
options[:db] = options[:db].to_i
|
412
511
|
options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
|
413
512
|
|
414
513
|
case options[:tcp_keepalive]
|
415
514
|
when Hash
|
416
|
-
[
|
417
|
-
unless options[:tcp_keepalive][key].is_a?(
|
418
|
-
raise "Expected the #{key.inspect} key in :tcp_keepalive to be
|
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"
|
419
518
|
end
|
420
519
|
end
|
421
520
|
|
422
|
-
when
|
521
|
+
when Integer
|
423
522
|
if options[:tcp_keepalive] >= 60
|
424
|
-
options[:tcp_keepalive] = {:
|
523
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
425
524
|
|
426
525
|
elsif options[:tcp_keepalive] >= 30
|
427
|
-
options[:tcp_keepalive] = {:
|
526
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
428
527
|
|
429
528
|
elsif options[:tcp_keepalive] >= 5
|
430
|
-
options[:tcp_keepalive] = {:
|
529
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
431
530
|
end
|
432
531
|
end
|
433
532
|
|
533
|
+
options[:_parsed] = true
|
534
|
+
|
434
535
|
options
|
435
536
|
end
|
436
537
|
|
437
538
|
def _parse_driver(driver)
|
438
539
|
driver = driver.to_s if driver.is_a?(Symbol)
|
439
540
|
|
440
|
-
if driver.
|
541
|
+
if driver.is_a?(String)
|
441
542
|
begin
|
442
|
-
|
443
|
-
driver = Connection.const_get(driver.capitalize)
|
543
|
+
require_relative "connection/#{driver}"
|
444
544
|
rescue LoadError, NameError
|
445
|
-
|
545
|
+
begin
|
546
|
+
require "redis/connection/#{driver}"
|
547
|
+
rescue LoadError, NameError => error
|
548
|
+
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
549
|
+
end
|
446
550
|
end
|
551
|
+
|
552
|
+
driver = Connection.const_get(driver.capitalize)
|
447
553
|
end
|
448
554
|
|
449
555
|
driver
|
@@ -451,23 +557,24 @@ class Redis
|
|
451
557
|
|
452
558
|
class Connector
|
453
559
|
def initialize(options)
|
454
|
-
@options = options
|
560
|
+
@options = options.dup
|
455
561
|
end
|
456
562
|
|
457
563
|
def resolve
|
458
564
|
@options
|
459
565
|
end
|
460
566
|
|
461
|
-
def check(client)
|
462
|
-
end
|
567
|
+
def check(client); end
|
463
568
|
|
464
569
|
class Sentinel < Connector
|
465
570
|
def initialize(options)
|
466
571
|
super(options)
|
467
572
|
|
468
|
-
@
|
469
|
-
|
470
|
-
@
|
573
|
+
@options[:db] = DEFAULTS.fetch(:db)
|
574
|
+
|
575
|
+
@sentinels = @options.delete(:sentinels).dup
|
576
|
+
@role = (@options[:role] || "master").to_s
|
577
|
+
@master = @options[:host]
|
471
578
|
end
|
472
579
|
|
473
580
|
def check(client)
|
@@ -482,27 +589,33 @@ class Redis
|
|
482
589
|
end
|
483
590
|
|
484
591
|
if role != @role
|
485
|
-
disconnect
|
592
|
+
client.disconnect
|
486
593
|
raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
|
487
594
|
end
|
488
595
|
end
|
489
596
|
|
490
597
|
def resolve
|
491
598
|
result = case @role
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
599
|
+
when "master"
|
600
|
+
resolve_master
|
601
|
+
when "slave"
|
602
|
+
resolve_slave
|
603
|
+
else
|
604
|
+
raise ArgumentError, "Unknown instance role #{@role}"
|
605
|
+
end
|
499
606
|
|
500
607
|
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
501
608
|
end
|
502
609
|
|
503
610
|
def sentinel_detect
|
504
611
|
@sentinels.each do |sentinel|
|
505
|
-
client = Client.new(
|
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
|
+
}))
|
506
619
|
|
507
620
|
begin
|
508
621
|
if result = yield(client)
|
@@ -512,18 +625,19 @@ class Redis
|
|
512
625
|
|
513
626
|
return result
|
514
627
|
end
|
628
|
+
rescue BaseConnectionError
|
515
629
|
ensure
|
516
630
|
client.disconnect
|
517
631
|
end
|
518
632
|
end
|
519
633
|
|
520
|
-
|
634
|
+
raise CannotConnectError, "No sentinels available."
|
521
635
|
end
|
522
636
|
|
523
637
|
def resolve_master
|
524
638
|
sentinel_detect do |client|
|
525
639
|
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
526
|
-
{:
|
640
|
+
{ host: reply[0], port: reply[1] }
|
527
641
|
end
|
528
642
|
end
|
529
643
|
end
|
@@ -531,9 +645,19 @@ class Redis
|
|
531
645
|
def resolve_slave
|
532
646
|
sentinel_detect do |client|
|
533
647
|
if reply = client.call(["sentinel", "slaves", @master])
|
534
|
-
|
535
|
-
|
536
|
-
{
|
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
|
537
661
|
end
|
538
662
|
end
|
539
663
|
end
|