redis 4.0.3 → 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 +5 -5
- data/CHANGELOG.md +110 -0
- data/README.md +126 -17
- data/lib/redis/client.rb +130 -82
- data/lib/redis/cluster/command_loader.rb +8 -7
- data/lib/redis/cluster/node.rb +5 -1
- data/lib/redis/cluster/node_key.rb +3 -7
- data/lib/redis/cluster/node_loader.rb +2 -0
- data/lib/redis/cluster/option.rb +31 -14
- data/lib/redis/cluster/slot.rb +30 -13
- data/lib/redis/cluster/slot_loader.rb +6 -4
- data/lib/redis/cluster.rb +23 -17
- data/lib/redis/connection/command_helper.rb +5 -2
- data/lib/redis/connection/hiredis.rb +4 -3
- data/lib/redis/connection/registry.rb +2 -1
- data/lib/redis/connection/ruby.rb +139 -106
- data/lib/redis/connection/synchrony.rb +9 -4
- data/lib/redis/connection.rb +2 -0
- data/lib/redis/distributed.rb +171 -70
- data/lib/redis/errors.rb +2 -0
- data/lib/redis/hash_ring.rb +15 -14
- data/lib/redis/pipeline.rb +46 -8
- data/lib/redis/subscribe.rb +11 -12
- data/lib/redis/version.rb +3 -1
- data/lib/redis.rb +1239 -426
- metadata +16 -262
- data/.gitignore +0 -19
- data/.travis/Gemfile +0 -18
- data/.travis.yml +0 -61
- data/.yardopts +0 -3
- data/Gemfile +0 -8
- 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/bin/build +0 -71
- data/bors.toml +0 -14
- 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/makefile +0 -74
- data/redis.gemspec +0 -43
- data/test/bitpos_test.rb +0 -63
- data/test/blocking_commands_test.rb +0 -40
- data/test/client_test.rb +0 -76
- data/test/cluster_abnormal_state_test.rb +0 -38
- data/test/cluster_blocking_commands_test.rb +0 -15
- data/test/cluster_client_internals_test.rb +0 -77
- data/test/cluster_client_key_hash_tags_test.rb +0 -88
- data/test/cluster_client_options_test.rb +0 -147
- data/test/cluster_client_pipelining_test.rb +0 -59
- data/test/cluster_client_replicas_test.rb +0 -36
- data/test/cluster_client_slots_test.rb +0 -94
- data/test/cluster_client_transactions_test.rb +0 -71
- data/test/cluster_commands_on_cluster_test.rb +0 -165
- data/test/cluster_commands_on_connection_test.rb +0 -40
- data/test/cluster_commands_on_geo_test.rb +0 -74
- data/test/cluster_commands_on_hashes_test.rb +0 -11
- data/test/cluster_commands_on_hyper_log_log_test.rb +0 -17
- data/test/cluster_commands_on_keys_test.rb +0 -134
- data/test/cluster_commands_on_lists_test.rb +0 -15
- data/test/cluster_commands_on_pub_sub_test.rb +0 -101
- data/test/cluster_commands_on_scripting_test.rb +0 -56
- data/test/cluster_commands_on_server_test.rb +0 -221
- data/test/cluster_commands_on_sets_test.rb +0 -39
- data/test/cluster_commands_on_sorted_sets_test.rb +0 -35
- data/test/cluster_commands_on_streams_test.rb +0 -196
- data/test/cluster_commands_on_strings_test.rb +0 -15
- data/test/cluster_commands_on_transactions_test.rb +0 -41
- data/test/cluster_commands_on_value_types_test.rb +0 -14
- data/test/command_map_test.rb +0 -28
- data/test/commands_on_geo_test.rb +0 -116
- data/test/commands_on_hashes_test.rb +0 -7
- data/test/commands_on_hyper_log_log_test.rb +0 -7
- data/test/commands_on_lists_test.rb +0 -7
- data/test/commands_on_sets_test.rb +0 -7
- data/test/commands_on_sorted_sets_test.rb +0 -7
- data/test/commands_on_strings_test.rb +0 -7
- data/test/commands_on_value_types_test.rb +0 -207
- data/test/connection_handling_test.rb +0 -275
- data/test/connection_test.rb +0 -57
- data/test/db/.gitkeep +0 -0
- data/test/distributed_blocking_commands_test.rb +0 -52
- data/test/distributed_commands_on_hashes_test.rb +0 -21
- data/test/distributed_commands_on_hyper_log_log_test.rb +0 -26
- data/test/distributed_commands_on_lists_test.rb +0 -19
- data/test/distributed_commands_on_sets_test.rb +0 -105
- data/test/distributed_commands_on_sorted_sets_test.rb +0 -59
- data/test/distributed_commands_on_strings_test.rb +0 -79
- data/test/distributed_commands_on_value_types_test.rb +0 -129
- data/test/distributed_commands_requiring_clustering_test.rb +0 -162
- data/test/distributed_connection_handling_test.rb +0 -21
- data/test/distributed_internals_test.rb +0 -68
- data/test/distributed_key_tags_test.rb +0 -50
- data/test/distributed_persistence_control_commands_test.rb +0 -24
- data/test/distributed_publish_subscribe_test.rb +0 -90
- data/test/distributed_remote_server_control_commands_test.rb +0 -64
- data/test/distributed_scripting_test.rb +0 -100
- data/test/distributed_sorting_test.rb +0 -18
- data/test/distributed_test.rb +0 -56
- data/test/distributed_transactions_test.rb +0 -30
- data/test/encoding_test.rb +0 -14
- data/test/error_replies_test.rb +0 -57
- data/test/fork_safety_test.rb +0 -60
- data/test/helper.rb +0 -345
- data/test/helper_test.rb +0 -22
- data/test/internals_test.rb +0 -408
- data/test/lint/blocking_commands.rb +0 -174
- data/test/lint/hashes.rb +0 -203
- data/test/lint/hyper_log_log.rb +0 -74
- data/test/lint/lists.rb +0 -159
- data/test/lint/sets.rb +0 -282
- data/test/lint/sorted_sets.rb +0 -497
- data/test/lint/strings.rb +0 -348
- data/test/lint/value_types.rb +0 -130
- data/test/persistence_control_commands_test.rb +0 -24
- data/test/pipelining_commands_test.rb +0 -246
- data/test/publish_subscribe_test.rb +0 -280
- data/test/remote_server_control_commands_test.rb +0 -175
- data/test/scanning_test.rb +0 -407
- data/test/scripting_test.rb +0 -76
- data/test/sentinel_command_test.rb +0 -78
- data/test/sentinel_test.rb +0 -253
- data/test/sorting_test.rb +0 -57
- data/test/ssl_test.rb +0 -69
- data/test/support/cluster/orchestrator.rb +0 -199
- 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 -130
- data/test/support/ssl/gen_certs.sh +0 -31
- data/test/support/ssl/trusted-ca.crt +0 -25
- data/test/support/ssl/trusted-ca.key +0 -27
- data/test/support/ssl/trusted-cert.crt +0 -81
- data/test/support/ssl/trusted-cert.key +0 -28
- data/test/support/ssl/untrusted-ca.crt +0 -26
- data/test/support/ssl/untrusted-ca.key +0 -27
- data/test/support/ssl/untrusted-cert.crt +0 -82
- data/test/support/ssl/untrusted-cert.key +0 -28
- data/test/support/wire/synchrony.rb +0 -24
- data/test/support/wire/thread.rb +0 -5
- data/test/synchrony_driver.rb +0 -85
- data/test/test.conf.erb +0 -9
- data/test/thread_safety_test.rb +0 -60
- data/test/transactions_test.rb +0 -272
- data/test/unknown_commands_test.rb +0 -12
- data/test/url_param_test.rb +0 -136
data/lib/redis/client.rb
CHANGED
@@ -1,27 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "errors"
|
2
4
|
require "socket"
|
3
5
|
require "cgi"
|
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
|
-
|
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
|
25
34
|
|
26
35
|
attr_reader :options
|
27
36
|
|
@@ -53,6 +62,10 @@ class Redis
|
|
53
62
|
@options[:read_timeout]
|
54
63
|
end
|
55
64
|
|
65
|
+
def username
|
66
|
+
@options[:username]
|
67
|
+
end
|
68
|
+
|
56
69
|
def password
|
57
70
|
@options[:password]
|
58
71
|
end
|
@@ -87,7 +100,7 @@ class Redis
|
|
87
100
|
@pending_reads = 0
|
88
101
|
|
89
102
|
@connector =
|
90
|
-
if options.
|
103
|
+
if !@options[:sentinels].nil?
|
91
104
|
Connector::Sentinel.new(@options)
|
92
105
|
elsif options.include?(:connector) && options[:connector].respond_to?(:new)
|
93
106
|
options.delete(:connector).new(@options)
|
@@ -102,7 +115,23 @@ class Redis
|
|
102
115
|
# Don't try to reconnect when the connection is fresh
|
103
116
|
with_reconnect(false) do
|
104
117
|
establish_connection
|
105
|
-
|
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]
|
106
135
|
call [:select, db] if db != 0
|
107
136
|
call [:client, :setname, @options[:id]] if @options[:id]
|
108
137
|
@connector.check(self)
|
@@ -123,7 +152,7 @@ class Redis
|
|
123
152
|
reply = process([command]) { read }
|
124
153
|
raise reply if reply.is_a?(CommandError)
|
125
154
|
|
126
|
-
if block_given?
|
155
|
+
if block_given? && reply != 'QUEUED'
|
127
156
|
yield reply
|
128
157
|
else
|
129
158
|
reply
|
@@ -155,16 +184,16 @@ class Redis
|
|
155
184
|
end
|
156
185
|
|
157
186
|
def call_pipeline(pipeline)
|
158
|
-
|
159
|
-
return [] if commands.empty?
|
187
|
+
return [] if pipeline.futures.empty?
|
160
188
|
|
161
189
|
with_reconnect pipeline.with_reconnect? do
|
162
190
|
begin
|
163
|
-
pipeline.finish(call_pipelined(
|
191
|
+
pipeline.finish(call_pipelined(pipeline)).tap do
|
164
192
|
self.db = pipeline.db if pipeline.db
|
165
193
|
end
|
166
194
|
rescue ConnectionError => e
|
167
195
|
return nil if pipeline.shutdown?
|
196
|
+
|
168
197
|
# Assume the pipeline was sent in one piece, but execution of
|
169
198
|
# SHUTDOWN caused none of the replies for commands that were executed
|
170
199
|
# prior to it from coming back around.
|
@@ -173,8 +202,8 @@ class Redis
|
|
173
202
|
end
|
174
203
|
end
|
175
204
|
|
176
|
-
def call_pipelined(
|
177
|
-
return [] if
|
205
|
+
def call_pipelined(pipeline)
|
206
|
+
return [] if pipeline.futures.empty?
|
178
207
|
|
179
208
|
# The method #ensure_connected (called from #process) reconnects once on
|
180
209
|
# I/O errors. To make an effort in making sure that commands are not
|
@@ -184,6 +213,8 @@ class Redis
|
|
184
213
|
# already successfully executed commands. To circumvent this, don't retry
|
185
214
|
# after the first reply has been read successfully.
|
186
215
|
|
216
|
+
commands = pipeline.commands
|
217
|
+
|
187
218
|
result = Array.new(commands.size)
|
188
219
|
reconnect = @reconnect
|
189
220
|
|
@@ -191,8 +222,12 @@ class Redis
|
|
191
222
|
exception = nil
|
192
223
|
|
193
224
|
process(commands) do
|
194
|
-
|
195
|
-
reply =
|
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
|
196
231
|
result[i] = reply
|
197
232
|
@reconnect = false
|
198
233
|
exception = reply if exception.nil? && reply.is_a?(CommandError)
|
@@ -237,12 +272,13 @@ class Redis
|
|
237
272
|
end
|
238
273
|
|
239
274
|
def connected?
|
240
|
-
!!
|
275
|
+
!!(connection && connection.connected?)
|
241
276
|
end
|
242
277
|
|
243
278
|
def disconnect
|
244
279
|
connection.disconnect if connected?
|
245
280
|
end
|
281
|
+
alias close disconnect
|
246
282
|
|
247
283
|
def reconnect
|
248
284
|
disconnect
|
@@ -277,12 +313,15 @@ class Redis
|
|
277
313
|
|
278
314
|
def with_socket_timeout(timeout)
|
279
315
|
connect unless connected?
|
316
|
+
original = @options[:read_timeout]
|
280
317
|
|
281
318
|
begin
|
282
319
|
connection.timeout = timeout
|
320
|
+
@options[:read_timeout] = timeout # for reconnection
|
283
321
|
yield
|
284
322
|
ensure
|
285
323
|
connection.timeout = self.timeout if connected?
|
324
|
+
@options[:read_timeout] = original
|
286
325
|
end
|
287
326
|
end
|
288
327
|
|
@@ -290,30 +329,27 @@ class Redis
|
|
290
329
|
with_socket_timeout(0, &blk)
|
291
330
|
end
|
292
331
|
|
293
|
-
def with_reconnect(val=true)
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
@reconnect = original
|
299
|
-
end
|
332
|
+
def with_reconnect(val = true)
|
333
|
+
original, @reconnect = @reconnect, val
|
334
|
+
yield
|
335
|
+
ensure
|
336
|
+
@reconnect = original
|
300
337
|
end
|
301
338
|
|
302
339
|
def without_reconnect(&blk)
|
303
340
|
with_reconnect(false, &blk)
|
304
341
|
end
|
305
342
|
|
306
|
-
|
343
|
+
protected
|
307
344
|
|
308
345
|
def logging(commands)
|
309
|
-
return yield unless @logger
|
346
|
+
return yield unless @logger&.debug?
|
310
347
|
|
311
348
|
begin
|
312
349
|
commands.each do |name, *args|
|
313
350
|
logged_args = args.map do |a|
|
314
|
-
|
315
|
-
|
316
|
-
when a.respond_to?(:to_s) then a.to_s
|
351
|
+
if a.respond_to?(:inspect) then a.inspect
|
352
|
+
elsif a.respond_to?(:to_s) then a.to_s
|
317
353
|
else
|
318
354
|
# handle poorly-behaved descendants of BasicObject
|
319
355
|
klass = a.instance_exec { (class << self; self end).superclass }
|
@@ -340,14 +376,16 @@ class Redis
|
|
340
376
|
@pending_reads = 0
|
341
377
|
rescue TimeoutError,
|
342
378
|
SocketError,
|
379
|
+
Errno::EADDRNOTAVAIL,
|
343
380
|
Errno::ECONNREFUSED,
|
344
381
|
Errno::EHOSTDOWN,
|
345
382
|
Errno::EHOSTUNREACH,
|
346
383
|
Errno::ENETUNREACH,
|
347
384
|
Errno::ENOENT,
|
348
|
-
Errno::ETIMEDOUT
|
385
|
+
Errno::ETIMEDOUT,
|
386
|
+
Errno::EINVAL => error
|
349
387
|
|
350
|
-
raise CannotConnectError, "Error connecting to Redis on #{location} (#{
|
388
|
+
raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
|
351
389
|
end
|
352
390
|
|
353
391
|
def ensure_connected
|
@@ -361,9 +399,9 @@ class Redis
|
|
361
399
|
if connected?
|
362
400
|
unless inherit_socket? || Process.pid == @pid
|
363
401
|
raise InheritedError,
|
364
|
-
|
365
|
-
|
366
|
-
|
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."
|
367
405
|
end
|
368
406
|
else
|
369
407
|
connect
|
@@ -374,7 +412,7 @@ class Redis
|
|
374
412
|
disconnect
|
375
413
|
|
376
414
|
if attempts <= @options[:reconnect_attempts] && @reconnect
|
377
|
-
sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
|
415
|
+
sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
|
378
416
|
@options[:reconnect_delay_max]].min
|
379
417
|
|
380
418
|
Kernel.sleep(sleep_t)
|
@@ -396,15 +434,14 @@ class Redis
|
|
396
434
|
|
397
435
|
defaults.keys.each do |key|
|
398
436
|
# Fill in defaults if needed
|
399
|
-
if defaults[key].respond_to?(:call)
|
400
|
-
defaults[key] = defaults[key].call
|
401
|
-
end
|
437
|
+
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
402
438
|
|
403
439
|
# Symbolize only keys that are needed
|
404
|
-
options[key] = options[key.to_s] if options.
|
440
|
+
options[key] = options[key.to_s] if options.key?(key.to_s)
|
405
441
|
end
|
406
442
|
|
407
|
-
url = options[:url]
|
443
|
+
url = options[:url]
|
444
|
+
url = defaults[:url] if url.nil?
|
408
445
|
|
409
446
|
# Override defaults from URL if given
|
410
447
|
if url
|
@@ -413,12 +450,13 @@ class Redis
|
|
413
450
|
uri = URI(url)
|
414
451
|
|
415
452
|
if uri.scheme == "unix"
|
416
|
-
defaults[:path]
|
453
|
+
defaults[:path] = uri.path
|
417
454
|
elsif uri.scheme == "redis" || uri.scheme == "rediss"
|
418
455
|
defaults[:scheme] = uri.scheme
|
419
456
|
defaults[:host] = uri.host if uri.host
|
420
457
|
defaults[:port] = uri.port if uri.port
|
421
|
-
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?
|
422
460
|
defaults[:db] = uri.path[1..-1].to_i if uri.path
|
423
461
|
defaults[:role] = :master
|
424
462
|
else
|
@@ -444,7 +482,7 @@ class Redis
|
|
444
482
|
options[:port] = options[:port].to_i
|
445
483
|
end
|
446
484
|
|
447
|
-
if options.
|
485
|
+
if options.key?(:timeout)
|
448
486
|
options[:connect_timeout] ||= options[:timeout]
|
449
487
|
options[:read_timeout] ||= options[:timeout]
|
450
488
|
options[:write_timeout] ||= options[:timeout]
|
@@ -463,7 +501,7 @@ class Redis
|
|
463
501
|
|
464
502
|
case options[:tcp_keepalive]
|
465
503
|
when Hash
|
466
|
-
[
|
504
|
+
%i[time intvl probes].each do |key|
|
467
505
|
unless options[:tcp_keepalive][key].is_a?(Integer)
|
468
506
|
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
469
507
|
end
|
@@ -471,13 +509,13 @@ class Redis
|
|
471
509
|
|
472
510
|
when Integer
|
473
511
|
if options[:tcp_keepalive] >= 60
|
474
|
-
options[:tcp_keepalive] = {:
|
512
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
475
513
|
|
476
514
|
elsif options[:tcp_keepalive] >= 30
|
477
|
-
options[:tcp_keepalive] = {:
|
515
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
478
516
|
|
479
517
|
elsif options[:tcp_keepalive] >= 5
|
480
|
-
options[:tcp_keepalive] = {:
|
518
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
481
519
|
end
|
482
520
|
end
|
483
521
|
|
@@ -489,14 +527,14 @@ class Redis
|
|
489
527
|
def _parse_driver(driver)
|
490
528
|
driver = driver.to_s if driver.is_a?(Symbol)
|
491
529
|
|
492
|
-
if driver.
|
530
|
+
if driver.is_a?(String)
|
493
531
|
begin
|
494
532
|
require_relative "connection/#{driver}"
|
495
|
-
rescue LoadError, NameError
|
533
|
+
rescue LoadError, NameError
|
496
534
|
begin
|
497
|
-
require "connection/#{driver}"
|
498
|
-
rescue LoadError, NameError =>
|
499
|
-
raise
|
535
|
+
require "redis/connection/#{driver}"
|
536
|
+
rescue LoadError, NameError => error
|
537
|
+
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
500
538
|
end
|
501
539
|
end
|
502
540
|
|
@@ -515,18 +553,16 @@ class Redis
|
|
515
553
|
@options
|
516
554
|
end
|
517
555
|
|
518
|
-
def check(client)
|
519
|
-
end
|
556
|
+
def check(client); end
|
520
557
|
|
521
558
|
class Sentinel < Connector
|
522
559
|
def initialize(options)
|
523
560
|
super(options)
|
524
561
|
|
525
|
-
@options[:password] = DEFAULTS.fetch(:password)
|
526
562
|
@options[:db] = DEFAULTS.fetch(:db)
|
527
563
|
|
528
564
|
@sentinels = @options.delete(:sentinels).dup
|
529
|
-
@role = @options
|
565
|
+
@role = (@options[:role] || "master").to_s
|
530
566
|
@master = @options[:host]
|
531
567
|
end
|
532
568
|
|
@@ -549,13 +585,13 @@ class Redis
|
|
549
585
|
|
550
586
|
def resolve
|
551
587
|
result = case @role
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
588
|
+
when "master"
|
589
|
+
resolve_master
|
590
|
+
when "slave"
|
591
|
+
resolve_slave
|
592
|
+
else
|
593
|
+
raise ArgumentError, "Unknown instance role #{@role}"
|
594
|
+
end
|
559
595
|
|
560
596
|
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
561
597
|
end
|
@@ -563,10 +599,12 @@ class Redis
|
|
563
599
|
def sentinel_detect
|
564
600
|
@sentinels.each do |sentinel|
|
565
601
|
client = Client.new(@options.merge({
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
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
|
+
}))
|
570
608
|
|
571
609
|
begin
|
572
610
|
if result = yield(client)
|
@@ -588,7 +626,7 @@ class Redis
|
|
588
626
|
def resolve_master
|
589
627
|
sentinel_detect do |client|
|
590
628
|
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
591
|
-
{:
|
629
|
+
{ host: reply[0], port: reply[1] }
|
592
630
|
end
|
593
631
|
end
|
594
632
|
end
|
@@ -596,9 +634,19 @@ class Redis
|
|
596
634
|
def resolve_slave
|
597
635
|
sentinel_detect do |client|
|
598
636
|
if reply = client.call(["sentinel", "slaves", @master])
|
599
|
-
|
600
|
-
|
601
|
-
{
|
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
|
602
650
|
end
|
603
651
|
end
|
604
652
|
end
|
@@ -10,23 +10,24 @@ class Redis
|
|
10
10
|
module_function
|
11
11
|
|
12
12
|
def load(nodes)
|
13
|
-
details = {}
|
14
|
-
|
15
13
|
nodes.each do |node|
|
16
|
-
|
17
|
-
|
14
|
+
begin
|
15
|
+
return fetch_command_details(node)
|
16
|
+
rescue CannotConnectError, ConnectionError, CommandError
|
17
|
+
next # can retry on another node
|
18
|
+
end
|
18
19
|
end
|
19
20
|
|
20
|
-
|
21
|
+
raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
|
21
22
|
end
|
22
23
|
|
23
24
|
def fetch_command_details(node)
|
24
25
|
node.call(%i[command]).map do |reply|
|
25
26
|
[reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
|
26
27
|
end.to_h
|
27
|
-
rescue CannotConnectError, ConnectionError, CommandError
|
28
|
-
{} # can retry on another node
|
29
28
|
end
|
29
|
+
|
30
|
+
private_class_method :fetch_command_details
|
30
31
|
end
|
31
32
|
end
|
32
33
|
end
|
data/lib/redis/cluster/node.rb
CHANGED
@@ -39,6 +39,7 @@ class Redis
|
|
39
39
|
def call_master(command, &block)
|
40
40
|
try_map do |node_key, client|
|
41
41
|
next if slave?(node_key)
|
42
|
+
|
42
43
|
client.call(command, &block)
|
43
44
|
end.values
|
44
45
|
end
|
@@ -48,6 +49,7 @@ class Redis
|
|
48
49
|
|
49
50
|
try_map do |node_key, client|
|
50
51
|
next if master?(node_key)
|
52
|
+
|
51
53
|
client.call(command, &block)
|
52
54
|
end.values
|
53
55
|
end
|
@@ -74,8 +76,9 @@ class Redis
|
|
74
76
|
clients = options.map do |node_key, option|
|
75
77
|
next if replica_disabled? && slave?(node_key)
|
76
78
|
|
79
|
+
option = option.merge(readonly: true) if slave?(node_key)
|
80
|
+
|
77
81
|
client = Client.new(option)
|
78
|
-
client.call(%i[readonly]) if slave?(node_key)
|
79
82
|
[node_key, client]
|
80
83
|
end
|
81
84
|
|
@@ -97,6 +100,7 @@ class Redis
|
|
97
100
|
end
|
98
101
|
|
99
102
|
return results if errors.empty?
|
103
|
+
|
100
104
|
raise CommandErrorCollection, errors
|
101
105
|
end
|
102
106
|
end
|
@@ -6,17 +6,13 @@ class Redis
|
|
6
6
|
# It is different from node id.
|
7
7
|
# Node id is internal identifying code in Redis Cluster.
|
8
8
|
module NodeKey
|
9
|
-
DEFAULT_SCHEME = 'redis'
|
10
|
-
SECURE_SCHEME = 'rediss'
|
11
9
|
DELIMITER = ':'
|
12
10
|
|
13
11
|
module_function
|
14
12
|
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
.map { |k| k.split(DELIMITER) }
|
19
|
-
.map { |k| URI::Generic.build(scheme: scheme, host: k[0], port: k[1].to_i).to_s }
|
13
|
+
def optionize(node_key)
|
14
|
+
host, port = split(node_key)
|
15
|
+
{ host: host, port: port }
|
20
16
|
end
|
21
17
|
|
22
18
|
def split(node_key)
|
data/lib/redis/cluster/option.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative '../errors'
|
4
4
|
require_relative 'node_key'
|
5
|
+
require 'uri'
|
5
6
|
|
6
7
|
class Redis
|
7
8
|
class Cluster
|
@@ -14,36 +15,36 @@ class Redis
|
|
14
15
|
def initialize(options)
|
15
16
|
options = options.dup
|
16
17
|
node_addrs = options.delete(:cluster)
|
17
|
-
@
|
18
|
+
@node_opts = build_node_options(node_addrs)
|
18
19
|
@replica = options.delete(:replica) == true
|
20
|
+
add_common_node_option_if_needed(options, @node_opts, :scheme)
|
21
|
+
add_common_node_option_if_needed(options, @node_opts, :username)
|
22
|
+
add_common_node_option_if_needed(options, @node_opts, :password)
|
19
23
|
@options = options
|
20
24
|
end
|
21
25
|
|
22
26
|
def per_node_key
|
23
|
-
@
|
27
|
+
@node_opts.map { |opt| [NodeKey.build_from_host_port(opt[:host], opt[:port]), @options.merge(opt)] }
|
24
28
|
.to_h
|
25
29
|
end
|
26
30
|
|
27
|
-
def secure?
|
28
|
-
@node_uris.any? { |uri| uri.scheme == SECURE_SCHEME } || @options[:ssl_params] || false
|
29
|
-
end
|
30
|
-
|
31
31
|
def use_replica?
|
32
32
|
@replica
|
33
33
|
end
|
34
34
|
|
35
35
|
def update_node(addrs)
|
36
|
-
@
|
36
|
+
@node_opts = build_node_options(addrs)
|
37
37
|
end
|
38
38
|
|
39
39
|
def add_node(host, port)
|
40
|
-
@
|
40
|
+
@node_opts << { host: host, port: port }
|
41
41
|
end
|
42
42
|
|
43
43
|
private
|
44
44
|
|
45
|
-
def
|
45
|
+
def build_node_options(addrs)
|
46
46
|
raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
|
47
|
+
|
47
48
|
addrs.map { |addr| parse_node_addr(addr) }
|
48
49
|
end
|
49
50
|
|
@@ -52,7 +53,7 @@ class Redis
|
|
52
53
|
when String
|
53
54
|
parse_node_url(addr)
|
54
55
|
when Hash
|
55
|
-
|
56
|
+
parse_node_option(addr)
|
56
57
|
else
|
57
58
|
raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash'
|
58
59
|
end
|
@@ -61,15 +62,31 @@ class Redis
|
|
61
62
|
def parse_node_url(addr)
|
62
63
|
uri = URI(addr)
|
63
64
|
raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
|
64
|
-
|
65
|
+
|
66
|
+
db = uri.path.split('/')[1]&.to_i
|
67
|
+
|
68
|
+
{ scheme: uri.scheme, username: uri.user, password: uri.password, host: uri.host, port: uri.port, db: db }
|
69
|
+
.reject { |_, v| v.nil? || v == '' }
|
65
70
|
rescue URI::InvalidURIError => err
|
66
71
|
raise InvalidClientOptionError, err.message
|
67
72
|
end
|
68
73
|
|
69
|
-
def
|
74
|
+
def parse_node_option(addr)
|
70
75
|
addr = addr.map { |k, v| [k.to_sym, v] }.to_h
|
71
|
-
|
72
|
-
|
76
|
+
if addr.values_at(:host, :port).any?(&:nil?)
|
77
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
|
78
|
+
end
|
79
|
+
|
80
|
+
addr
|
81
|
+
end
|
82
|
+
|
83
|
+
# Redis cluster node returns only host and port information.
|
84
|
+
# So we should complement additional information such as:
|
85
|
+
# scheme, username, password and so on.
|
86
|
+
def add_common_node_option_if_needed(options, node_opts, key)
|
87
|
+
return options if options[key].nil? && node_opts.first[key].nil?
|
88
|
+
|
89
|
+
options[key] ||= node_opts.first[key]
|
73
90
|
end
|
74
91
|
end
|
75
92
|
end
|