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