redis 3.0.0 → 4.5.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 +7 -0
- data/CHANGELOG.md +315 -0
- data/README.md +301 -58
- data/lib/redis/client.rb +383 -88
- data/lib/redis/cluster/command.rb +81 -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 +108 -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 +291 -0
- data/lib/redis/connection/command_helper.rb +7 -10
- data/lib/redis/connection/hiredis.rb +12 -8
- data/lib/redis/connection/registry.rb +2 -1
- data/lib/redis/connection/ruby.rb +266 -74
- data/lib/redis/connection/synchrony.rb +41 -14
- data/lib/redis/connection.rb +4 -2
- data/lib/redis/distributed.rb +258 -76
- data/lib/redis/errors.rb +48 -0
- data/lib/redis/hash_ring.rb +31 -73
- data/lib/redis/pipeline.rb +74 -18
- data/lib/redis/subscribe.rb +24 -13
- data/lib/redis/version.rb +3 -1
- data/lib/redis.rb +2068 -464
- metadata +63 -160
- data/.gitignore +0 -10
- data/.order +0 -169
- data/.travis/Gemfile +0 -11
- data/.travis.yml +0 -50
- data/.yardopts +0 -3
- data/Rakefile +0 -392
- data/benchmarking/logging.rb +0 -62
- 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/dist_redis.rb +0 -43
- data/examples/incr-decr.rb +0 -17
- data/examples/list.rb +0 -26
- data/examples/pubsub.rb +0 -31
- data/examples/sets.rb +0 -36
- data/examples/unicorn/config.ru +0 -3
- data/examples/unicorn/unicorn.rb +0 -20
- data/redis.gemspec +0 -41
- 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_lists_test.rb +0 -20
- data/test/commands_on_sets_test.rb +0 -77
- data/test/commands_on_sorted_sets_test.rb +0 -109
- data/test/commands_on_strings_test.rb +0 -83
- data/test/commands_on_value_types_test.rb +0 -99
- data/test/connection_handling_test.rb +0 -189
- data/test/db/.gitignore +0 -1
- data/test/distributed_blocking_commands_test.rb +0 -46
- data/test/distributed_commands_on_hashes_test.rb +0 -10
- 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 -48
- data/test/distributed_commands_on_value_types_test.rb +0 -87
- data/test/distributed_commands_requiring_clustering_test.rb +0 -148
- data/test/distributed_connection_handling_test.rb +0 -23
- data/test/distributed_internals_test.rb +0 -15
- 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 -53
- 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/helper.rb +0 -188
- data/test/helper_test.rb +0 -22
- data/test/internals_test.rb +0 -214
- data/test/lint/blocking_commands.rb +0 -124
- data/test/lint/hashes.rb +0 -162
- data/test/lint/lists.rb +0 -143
- data/test/lint/sets.rb +0 -96
- data/test/lint/sorted_sets.rb +0 -201
- data/test/lint/strings.rb +0 -157
- data/test/lint/value_types.rb +0 -106
- data/test/persistence_control_commands_test.rb +0 -26
- data/test/pipelining_commands_test.rb +0 -195
- data/test/publish_subscribe_test.rb +0 -153
- data/test/remote_server_control_commands_test.rb +0 -104
- data/test/scripting_test.rb +0 -78
- data/test/sorting_test.rb +0 -45
- 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 -92
- data/test/support/wire/synchrony.rb +0 -24
- data/test/support/wire/thread.rb +0 -5
- data/test/synchrony_driver.rb +0 -57
- data/test/test.conf +0 -9
- data/test/thread_safety_test.rb +0 -32
- data/test/transactions_test.rb +0 -244
- data/test/unknown_commands_test.rb +0 -14
- data/test/url_param_test.rb +0 -64
data/lib/redis/client.rb
CHANGED
@@ -1,17 +1,38 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "errors"
|
4
|
+
require "socket"
|
5
|
+
require "cgi"
|
2
6
|
|
3
7
|
class Redis
|
4
8
|
class Client
|
5
|
-
|
9
|
+
# Defaults are also used for converting string keys to symbols.
|
6
10
|
DEFAULTS = {
|
7
|
-
:
|
8
|
-
:
|
9
|
-
:
|
10
|
-
:
|
11
|
-
:
|
12
|
-
:
|
13
|
-
:
|
14
|
-
|
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
|
15
36
|
|
16
37
|
def scheme
|
17
38
|
@options[:scheme]
|
@@ -29,8 +50,20 @@ class Redis
|
|
29
50
|
@options[:path]
|
30
51
|
end
|
31
52
|
|
53
|
+
def read_timeout
|
54
|
+
@options[:read_timeout]
|
55
|
+
end
|
56
|
+
|
57
|
+
def connect_timeout
|
58
|
+
@options[:connect_timeout]
|
59
|
+
end
|
60
|
+
|
32
61
|
def timeout
|
33
|
-
@options[:
|
62
|
+
@options[:read_timeout]
|
63
|
+
end
|
64
|
+
|
65
|
+
def username
|
66
|
+
@options[:username]
|
34
67
|
end
|
35
68
|
|
36
69
|
def password
|
@@ -45,9 +78,17 @@ class Redis
|
|
45
78
|
@options[:db] = db.to_i
|
46
79
|
end
|
47
80
|
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
+
attr_reader :connection
|
91
|
+
attr_reader :command_map
|
51
92
|
|
52
93
|
def initialize(options = {})
|
53
94
|
@options = _parse_options(options)
|
@@ -55,14 +96,47 @@ class Redis
|
|
55
96
|
@logger = @options[:logger]
|
56
97
|
@connection = nil
|
57
98
|
@command_map = {}
|
99
|
+
|
100
|
+
@pending_reads = 0
|
101
|
+
|
102
|
+
@connector =
|
103
|
+
if !@options[:sentinels].nil?
|
104
|
+
Connector::Sentinel.new(@options)
|
105
|
+
elsif options.include?(:connector) && options[:connector].respond_to?(:new)
|
106
|
+
options.delete(:connector).new(@options)
|
107
|
+
else
|
108
|
+
Connector.new(@options)
|
109
|
+
end
|
58
110
|
end
|
59
111
|
|
60
112
|
def connect
|
61
113
|
@pid = Process.pid
|
62
114
|
|
63
|
-
|
64
|
-
|
65
|
-
|
115
|
+
# Don't try to reconnect when the connection is fresh
|
116
|
+
with_reconnect(false) do
|
117
|
+
establish_connection
|
118
|
+
if password
|
119
|
+
if username
|
120
|
+
begin
|
121
|
+
call [:auth, username, password]
|
122
|
+
rescue CommandError => err # Likely on Redis < 6
|
123
|
+
if err.message.match?(/ERR wrong number of arguments for \'auth\' command/)
|
124
|
+
call [:auth, password]
|
125
|
+
else
|
126
|
+
raise
|
127
|
+
end
|
128
|
+
end
|
129
|
+
else
|
130
|
+
call [:auth, password]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
call [:readonly] if @options[:readonly]
|
135
|
+
call [:select, db] if db != 0
|
136
|
+
call [:client, :setname, @options[:id]] if @options[:id]
|
137
|
+
@connector.check(self)
|
138
|
+
end
|
139
|
+
|
66
140
|
self
|
67
141
|
end
|
68
142
|
|
@@ -74,21 +148,21 @@ class Redis
|
|
74
148
|
path || "#{host}:#{port}"
|
75
149
|
end
|
76
150
|
|
77
|
-
def call(command
|
151
|
+
def call(command)
|
78
152
|
reply = process([command]) { read }
|
79
153
|
raise reply if reply.is_a?(CommandError)
|
80
154
|
|
81
|
-
if
|
82
|
-
|
155
|
+
if block_given? && reply != 'QUEUED'
|
156
|
+
yield reply
|
83
157
|
else
|
84
158
|
reply
|
85
159
|
end
|
86
160
|
end
|
87
161
|
|
88
|
-
def call_loop(command)
|
162
|
+
def call_loop(command, timeout = 0)
|
89
163
|
error = nil
|
90
164
|
|
91
|
-
result =
|
165
|
+
result = with_socket_timeout(timeout) do
|
92
166
|
process([command]) do
|
93
167
|
loop do
|
94
168
|
reply = read
|
@@ -110,11 +184,16 @@ class Redis
|
|
110
184
|
end
|
111
185
|
|
112
186
|
def call_pipeline(pipeline)
|
187
|
+
return [] if pipeline.futures.empty?
|
188
|
+
|
113
189
|
with_reconnect pipeline.with_reconnect? do
|
114
190
|
begin
|
115
|
-
pipeline.finish(call_pipelined(pipeline
|
191
|
+
pipeline.finish(call_pipelined(pipeline)).tap do
|
192
|
+
self.db = pipeline.db if pipeline.db
|
193
|
+
end
|
116
194
|
rescue ConnectionError => e
|
117
195
|
return nil if pipeline.shutdown?
|
196
|
+
|
118
197
|
# Assume the pipeline was sent in one piece, but execution of
|
119
198
|
# SHUTDOWN caused none of the replies for commands that were executed
|
120
199
|
# prior to it from coming back around.
|
@@ -123,8 +202,8 @@ class Redis
|
|
123
202
|
end
|
124
203
|
end
|
125
204
|
|
126
|
-
def call_pipelined(
|
127
|
-
return [] if
|
205
|
+
def call_pipelined(pipeline)
|
206
|
+
return [] if pipeline.futures.empty?
|
128
207
|
|
129
208
|
# The method #ensure_connected (called from #process) reconnects once on
|
130
209
|
# I/O errors. To make an effort in making sure that commands are not
|
@@ -134,19 +213,28 @@ class Redis
|
|
134
213
|
# already successfully executed commands. To circumvent this, don't retry
|
135
214
|
# after the first reply has been read successfully.
|
136
215
|
|
216
|
+
commands = pipeline.commands
|
217
|
+
|
137
218
|
result = Array.new(commands.size)
|
138
219
|
reconnect = @reconnect
|
139
220
|
|
140
221
|
begin
|
141
|
-
|
142
|
-
result[0] = read
|
143
|
-
|
144
|
-
@reconnect = false
|
222
|
+
exception = nil
|
145
223
|
|
146
|
-
|
147
|
-
|
224
|
+
process(commands) do
|
225
|
+
pipeline.timeouts.each_with_index do |timeout, i|
|
226
|
+
reply = if timeout
|
227
|
+
with_socket_timeout(timeout) { read }
|
228
|
+
else
|
229
|
+
read
|
230
|
+
end
|
231
|
+
result[i] = reply
|
232
|
+
@reconnect = false
|
233
|
+
exception = reply if exception.nil? && reply.is_a?(CommandError)
|
148
234
|
end
|
149
235
|
end
|
236
|
+
|
237
|
+
raise exception if exception
|
150
238
|
ensure
|
151
239
|
@reconnect = reconnect
|
152
240
|
end
|
@@ -154,14 +242,18 @@ class Redis
|
|
154
242
|
result
|
155
243
|
end
|
156
244
|
|
157
|
-
def
|
158
|
-
|
245
|
+
def call_with_timeout(command, timeout, &blk)
|
246
|
+
with_socket_timeout(timeout) do
|
159
247
|
call(command, &blk)
|
160
248
|
end
|
161
249
|
rescue ConnectionError
|
162
250
|
retry
|
163
251
|
end
|
164
252
|
|
253
|
+
def call_without_timeout(command, &blk)
|
254
|
+
call_with_timeout(command, 0, &blk)
|
255
|
+
end
|
256
|
+
|
165
257
|
def process(commands)
|
166
258
|
logging(commands) do
|
167
259
|
ensure_connected do
|
@@ -171,7 +263,7 @@ class Redis
|
|
171
263
|
command[0] = command_map[command.first]
|
172
264
|
end
|
173
265
|
|
174
|
-
|
266
|
+
write(command)
|
175
267
|
end
|
176
268
|
|
177
269
|
yield if block_given?
|
@@ -180,12 +272,13 @@ class Redis
|
|
180
272
|
end
|
181
273
|
|
182
274
|
def connected?
|
183
|
-
connection && connection.connected?
|
275
|
+
!!(connection && connection.connected?)
|
184
276
|
end
|
185
277
|
|
186
278
|
def disconnect
|
187
279
|
connection.disconnect if connected?
|
188
280
|
end
|
281
|
+
alias close disconnect
|
189
282
|
|
190
283
|
def reconnect
|
191
284
|
disconnect
|
@@ -194,95 +287,135 @@ class Redis
|
|
194
287
|
|
195
288
|
def io
|
196
289
|
yield
|
197
|
-
rescue TimeoutError
|
198
|
-
|
290
|
+
rescue TimeoutError => e1
|
291
|
+
# Add a message to the exception without destroying the original stack
|
292
|
+
e2 = TimeoutError.new("Connection timed out")
|
293
|
+
e2.set_backtrace(e1.backtrace)
|
294
|
+
raise e2
|
199
295
|
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL => e
|
200
296
|
raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
|
201
297
|
end
|
202
298
|
|
203
299
|
def read
|
204
300
|
io do
|
205
|
-
connection.read
|
301
|
+
value = connection.read
|
302
|
+
@pending_reads -= 1
|
303
|
+
value
|
206
304
|
end
|
207
305
|
end
|
208
306
|
|
209
307
|
def write(command)
|
210
308
|
io do
|
309
|
+
@pending_reads += 1
|
211
310
|
connection.write(command)
|
212
311
|
end
|
213
312
|
end
|
214
313
|
|
215
|
-
def
|
314
|
+
def with_socket_timeout(timeout)
|
216
315
|
connect unless connected?
|
316
|
+
original = @options[:read_timeout]
|
217
317
|
|
218
318
|
begin
|
219
|
-
connection.timeout =
|
319
|
+
connection.timeout = timeout
|
320
|
+
@options[:read_timeout] = timeout # for reconnection
|
220
321
|
yield
|
221
322
|
ensure
|
222
|
-
connection.timeout = timeout if connected?
|
323
|
+
connection.timeout = self.timeout if connected?
|
324
|
+
@options[:read_timeout] = original
|
223
325
|
end
|
224
326
|
end
|
225
327
|
|
226
|
-
def
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
328
|
+
def without_socket_timeout(&blk)
|
329
|
+
with_socket_timeout(0, &blk)
|
330
|
+
end
|
331
|
+
|
332
|
+
def with_reconnect(val = true)
|
333
|
+
original, @reconnect = @reconnect, val
|
334
|
+
yield
|
335
|
+
ensure
|
336
|
+
@reconnect = original
|
233
337
|
end
|
234
338
|
|
235
339
|
def without_reconnect(&blk)
|
236
340
|
with_reconnect(false, &blk)
|
237
341
|
end
|
238
342
|
|
239
|
-
|
343
|
+
protected
|
240
344
|
|
241
345
|
def logging(commands)
|
242
|
-
return yield unless @logger
|
346
|
+
return yield unless @logger&.debug?
|
243
347
|
|
244
348
|
begin
|
245
349
|
commands.each do |name, *args|
|
246
|
-
|
350
|
+
logged_args = args.map do |a|
|
351
|
+
if a.respond_to?(:inspect) then a.inspect
|
352
|
+
elsif a.respond_to?(:to_s) then a.to_s
|
353
|
+
else
|
354
|
+
# handle poorly-behaved descendants of BasicObject
|
355
|
+
klass = a.instance_exec { (class << self; self end).superclass }
|
356
|
+
"\#<#{klass}:#{a.__id__}>"
|
357
|
+
end
|
358
|
+
end
|
359
|
+
@logger.debug("[Redis] command=#{name.to_s.upcase} args=#{logged_args.join(' ')}")
|
247
360
|
end
|
248
361
|
|
249
362
|
t1 = Time.now
|
250
363
|
yield
|
251
364
|
ensure
|
252
|
-
@logger.debug("Redis
|
365
|
+
@logger.debug("[Redis] call_time=%0.2f ms" % ((Time.now - t1) * 1000)) if t1
|
253
366
|
end
|
254
367
|
end
|
255
368
|
|
256
369
|
def establish_connection
|
257
|
-
|
370
|
+
server = @connector.resolve.dup
|
258
371
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
372
|
+
@options[:host] = server[:host]
|
373
|
+
@options[:port] = Integer(server[:port]) if server.include?(:port)
|
374
|
+
|
375
|
+
@connection = @options[:driver].connect(@options)
|
376
|
+
@pending_reads = 0
|
377
|
+
rescue TimeoutError,
|
378
|
+
SocketError,
|
379
|
+
Errno::EADDRNOTAVAIL,
|
380
|
+
Errno::ECONNREFUSED,
|
381
|
+
Errno::EHOSTDOWN,
|
382
|
+
Errno::EHOSTUNREACH,
|
383
|
+
Errno::ENETUNREACH,
|
384
|
+
Errno::ENOENT,
|
385
|
+
Errno::ETIMEDOUT,
|
386
|
+
Errno::EINVAL => error
|
387
|
+
|
388
|
+
raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
|
263
389
|
end
|
264
390
|
|
265
391
|
def ensure_connected
|
266
|
-
|
392
|
+
disconnect if @pending_reads > 0
|
393
|
+
|
394
|
+
attempts = 0
|
267
395
|
|
268
396
|
begin
|
397
|
+
attempts += 1
|
398
|
+
|
269
399
|
if connected?
|
270
|
-
|
400
|
+
unless inherit_socket? || Process.pid == @pid
|
271
401
|
raise InheritedError,
|
272
|
-
|
273
|
-
|
402
|
+
"Tried to use a connection from a child process without reconnecting. " \
|
403
|
+
"You need to reconnect to Redis after forking " \
|
404
|
+
"or set :inherit_socket to true."
|
274
405
|
end
|
275
406
|
else
|
276
407
|
connect
|
277
408
|
end
|
278
409
|
|
279
|
-
tries += 1
|
280
|
-
|
281
410
|
yield
|
282
|
-
rescue
|
411
|
+
rescue BaseConnectionError
|
283
412
|
disconnect
|
284
413
|
|
285
|
-
if
|
414
|
+
if attempts <= @options[:reconnect_attempts] && @reconnect
|
415
|
+
sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
|
416
|
+
@options[:reconnect_delay_max]].min
|
417
|
+
|
418
|
+
Kernel.sleep(sleep_t)
|
286
419
|
retry
|
287
420
|
else
|
288
421
|
raise
|
@@ -294,9 +427,21 @@ class Redis
|
|
294
427
|
end
|
295
428
|
|
296
429
|
def _parse_options(options)
|
430
|
+
return options if options[:_parsed]
|
431
|
+
|
297
432
|
defaults = DEFAULTS.dup
|
433
|
+
options = options.dup
|
434
|
+
|
435
|
+
defaults.keys.each do |key|
|
436
|
+
# Fill in defaults if needed
|
437
|
+
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
438
|
+
|
439
|
+
# Symbolize only keys that are needed
|
440
|
+
options[key] = options[key.to_s] if options.key?(key.to_s)
|
441
|
+
end
|
298
442
|
|
299
|
-
url = options[:url]
|
443
|
+
url = options[:url]
|
444
|
+
url = defaults[:url] if url.nil?
|
300
445
|
|
301
446
|
# Override defaults from URL if given
|
302
447
|
if url
|
@@ -305,57 +450,207 @@ class Redis
|
|
305
450
|
uri = URI(url)
|
306
451
|
|
307
452
|
if uri.scheme == "unix"
|
308
|
-
defaults[:path]
|
309
|
-
|
310
|
-
# Require the URL to have at least a host
|
311
|
-
raise ArgumentError, "invalid url" unless uri.host
|
312
|
-
|
453
|
+
defaults[:path] = uri.path
|
454
|
+
elsif uri.scheme == "redis" || uri.scheme == "rediss"
|
313
455
|
defaults[:scheme] = uri.scheme
|
314
|
-
defaults[:host] = uri.host
|
456
|
+
defaults[:host] = uri.host if uri.host
|
315
457
|
defaults[:port] = uri.port if uri.port
|
316
|
-
defaults[:
|
458
|
+
defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
|
459
|
+
defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
|
317
460
|
defaults[:db] = uri.path[1..-1].to_i if uri.path
|
461
|
+
defaults[:role] = :master
|
462
|
+
else
|
463
|
+
raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
|
318
464
|
end
|
465
|
+
|
466
|
+
defaults[:ssl] = true if uri.scheme == "rediss"
|
319
467
|
end
|
320
468
|
|
321
|
-
|
469
|
+
# Use default when option is not specified or nil
|
470
|
+
defaults.keys.each do |key|
|
471
|
+
options[key] = defaults[key] if options[key].nil?
|
472
|
+
end
|
322
473
|
|
323
474
|
if options[:path]
|
475
|
+
# Unix socket
|
324
476
|
options[:scheme] = "unix"
|
325
477
|
options.delete(:host)
|
326
478
|
options.delete(:port)
|
327
479
|
else
|
480
|
+
# TCP socket
|
328
481
|
options[:host] = options[:host].to_s
|
329
482
|
options[:port] = options[:port].to_i
|
330
483
|
end
|
331
484
|
|
332
|
-
|
485
|
+
if options.key?(:timeout)
|
486
|
+
options[:connect_timeout] ||= options[:timeout]
|
487
|
+
options[:read_timeout] ||= options[:timeout]
|
488
|
+
options[:write_timeout] ||= options[:timeout]
|
489
|
+
end
|
490
|
+
|
491
|
+
options[:connect_timeout] = Float(options[:connect_timeout])
|
492
|
+
options[:read_timeout] = Float(options[:read_timeout])
|
493
|
+
options[:write_timeout] = Float(options[:write_timeout])
|
494
|
+
|
495
|
+
options[:reconnect_attempts] = options[:reconnect_attempts].to_i
|
496
|
+
options[:reconnect_delay] = options[:reconnect_delay].to_f
|
497
|
+
options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
|
498
|
+
|
333
499
|
options[:db] = options[:db].to_i
|
334
500
|
options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
|
335
501
|
|
502
|
+
case options[:tcp_keepalive]
|
503
|
+
when Hash
|
504
|
+
%i[time intvl probes].each do |key|
|
505
|
+
unless options[:tcp_keepalive][key].is_a?(Integer)
|
506
|
+
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
when Integer
|
511
|
+
if options[:tcp_keepalive] >= 60
|
512
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
513
|
+
|
514
|
+
elsif options[:tcp_keepalive] >= 30
|
515
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
516
|
+
|
517
|
+
elsif options[:tcp_keepalive] >= 5
|
518
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
options[:_parsed] = true
|
523
|
+
|
336
524
|
options
|
337
525
|
end
|
338
526
|
|
339
527
|
def _parse_driver(driver)
|
340
528
|
driver = driver.to_s if driver.is_a?(Symbol)
|
341
529
|
|
342
|
-
if driver.
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
require "redis/connection/synchrony"
|
352
|
-
driver = Connection::Synchrony
|
353
|
-
else
|
354
|
-
raise "Unknown driver: #{driver}"
|
530
|
+
if driver.is_a?(String)
|
531
|
+
begin
|
532
|
+
require_relative "connection/#{driver}"
|
533
|
+
rescue LoadError, NameError
|
534
|
+
begin
|
535
|
+
require "redis/connection/#{driver}"
|
536
|
+
rescue LoadError, NameError => error
|
537
|
+
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
538
|
+
end
|
355
539
|
end
|
540
|
+
|
541
|
+
driver = Connection.const_get(driver.capitalize)
|
356
542
|
end
|
357
543
|
|
358
544
|
driver
|
359
545
|
end
|
546
|
+
|
547
|
+
class Connector
|
548
|
+
def initialize(options)
|
549
|
+
@options = options.dup
|
550
|
+
end
|
551
|
+
|
552
|
+
def resolve
|
553
|
+
@options
|
554
|
+
end
|
555
|
+
|
556
|
+
def check(client); end
|
557
|
+
|
558
|
+
class Sentinel < Connector
|
559
|
+
def initialize(options)
|
560
|
+
super(options)
|
561
|
+
|
562
|
+
@options[:db] = DEFAULTS.fetch(:db)
|
563
|
+
|
564
|
+
@sentinels = @options.delete(:sentinels).dup
|
565
|
+
@role = (@options[:role] || "master").to_s
|
566
|
+
@master = @options[:host]
|
567
|
+
end
|
568
|
+
|
569
|
+
def check(client)
|
570
|
+
# Check the instance is really of the role we are looking for.
|
571
|
+
# We can't assume the command is supported since it was introduced
|
572
|
+
# recently and this client should work with old stuff.
|
573
|
+
begin
|
574
|
+
role = client.call([:role])[0]
|
575
|
+
rescue Redis::CommandError
|
576
|
+
# Assume the test is passed if we can't get a reply from ROLE...
|
577
|
+
role = @role
|
578
|
+
end
|
579
|
+
|
580
|
+
if role != @role
|
581
|
+
client.disconnect
|
582
|
+
raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
def resolve
|
587
|
+
result = case @role
|
588
|
+
when "master"
|
589
|
+
resolve_master
|
590
|
+
when "slave"
|
591
|
+
resolve_slave
|
592
|
+
else
|
593
|
+
raise ArgumentError, "Unknown instance role #{@role}"
|
594
|
+
end
|
595
|
+
|
596
|
+
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
597
|
+
end
|
598
|
+
|
599
|
+
def sentinel_detect
|
600
|
+
@sentinels.each do |sentinel|
|
601
|
+
client = Client.new(@options.merge({
|
602
|
+
host: sentinel[:host] || sentinel["host"],
|
603
|
+
port: sentinel[:port] || sentinel["port"],
|
604
|
+
username: sentinel[:username] || sentinel["username"],
|
605
|
+
password: sentinel[:password] || sentinel["password"],
|
606
|
+
reconnect_attempts: 0
|
607
|
+
}))
|
608
|
+
|
609
|
+
begin
|
610
|
+
if result = yield(client)
|
611
|
+
# This sentinel responded. Make sure we ask it first next time.
|
612
|
+
@sentinels.delete(sentinel)
|
613
|
+
@sentinels.unshift(sentinel)
|
614
|
+
|
615
|
+
return result
|
616
|
+
end
|
617
|
+
rescue BaseConnectionError
|
618
|
+
ensure
|
619
|
+
client.disconnect
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
623
|
+
raise CannotConnectError, "No sentinels available."
|
624
|
+
end
|
625
|
+
|
626
|
+
def resolve_master
|
627
|
+
sentinel_detect do |client|
|
628
|
+
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
629
|
+
{ host: reply[0], port: reply[1] }
|
630
|
+
end
|
631
|
+
end
|
632
|
+
end
|
633
|
+
|
634
|
+
def resolve_slave
|
635
|
+
sentinel_detect do |client|
|
636
|
+
if reply = client.call(["sentinel", "slaves", @master])
|
637
|
+
slaves = reply.map { |s| s.each_slice(2).to_h }
|
638
|
+
slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
|
639
|
+
slaves.reject! { |s| s.fetch('flags').include?('s_down') }
|
640
|
+
|
641
|
+
if slaves.empty?
|
642
|
+
raise CannotConnectError, 'No slaves available.'
|
643
|
+
else
|
644
|
+
slave = slaves.sample
|
645
|
+
{
|
646
|
+
host: slave.fetch('ip'),
|
647
|
+
port: slave.fetch('port')
|
648
|
+
}
|
649
|
+
end
|
650
|
+
end
|
651
|
+
end
|
652
|
+
end
|
653
|
+
end
|
654
|
+
end
|
360
655
|
end
|
361
656
|
end
|