redis 4.0.0.rc1 → 4.4.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 +143 -3
- data/README.md +127 -18
- data/lib/redis/client.rb +150 -93
- 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 +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 +3 -2
- data/lib/redis/connection/hiredis.rb +4 -3
- data/lib/redis/connection/registry.rb +2 -1
- data/lib/redis/connection/ruby.rb +123 -105
- data/lib/redis/connection/synchrony.rb +18 -5
- data/lib/redis/connection.rb +2 -0
- data/lib/redis/distributed.rb +955 -0
- data/lib/redis/errors.rb +48 -0
- data/lib/redis/hash_ring.rb +89 -0
- data/lib/redis/pipeline.rb +55 -9
- data/lib/redis/subscribe.rb +11 -12
- data/lib/redis/version.rb +3 -1
- data/lib/redis.rb +1242 -381
- metadata +34 -141
- data/.gitignore +0 -16
- data/.travis/Gemfile +0 -11
- data/.travis.yml +0 -71
- data/.yardopts +0 -3
- data/Gemfile +0 -3
- 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/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 -42
- data/redis.gemspec +0 -40
- data/test/bitpos_test.rb +0 -63
- data/test/blocking_commands_test.rb +0 -183
- data/test/client_test.rb +0 -59
- data/test/command_map_test.rb +0 -28
- data/test/commands_on_hashes_test.rb +0 -174
- data/test/commands_on_hyper_log_log_test.rb +0 -70
- data/test/commands_on_lists_test.rb +0 -154
- data/test/commands_on_sets_test.rb +0 -208
- data/test/commands_on_sorted_sets_test.rb +0 -444
- data/test/commands_on_strings_test.rb +0 -338
- data/test/commands_on_value_types_test.rb +0 -246
- data/test/connection_handling_test.rb +0 -275
- data/test/db/.gitkeep +0 -0
- 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 -179
- data/test/helper_test.rb +0 -22
- data/test/internals_test.rb +0 -435
- data/test/persistence_control_commands_test.rb +0 -24
- data/test/pipelining_commands_test.rb +0 -238
- 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/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 -262
- data/test/unknown_commands_test.rb +0 -12
- data/test/url_param_test.rb +0 -136
data/lib/redis/client.rb
CHANGED
@@ -1,29 +1,38 @@
|
|
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
|
-
|
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
|
27
36
|
|
28
37
|
def scheme
|
29
38
|
@options[:scheme]
|
@@ -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
|
@@ -86,11 +99,14 @@ class Redis
|
|
86
99
|
|
87
100
|
@pending_reads = 0
|
88
101
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
94
110
|
end
|
95
111
|
|
96
112
|
def connect
|
@@ -99,7 +115,19 @@ class Redis
|
|
99
115
|
# Don't try to reconnect when the connection is fresh
|
100
116
|
with_reconnect(false) do
|
101
117
|
establish_connection
|
102
|
-
|
118
|
+
if password
|
119
|
+
if username
|
120
|
+
begin
|
121
|
+
call [:auth, username, password]
|
122
|
+
rescue CommandError # Likely on Redis < 6
|
123
|
+
call [:auth, password]
|
124
|
+
end
|
125
|
+
else
|
126
|
+
call [:auth, password]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
call [:readonly] if @options[:readonly]
|
103
131
|
call [:select, db] if db != 0
|
104
132
|
call [:client, :setname, @options[:id]] if @options[:id]
|
105
133
|
@connector.check(self)
|
@@ -120,7 +148,7 @@ class Redis
|
|
120
148
|
reply = process([command]) { read }
|
121
149
|
raise reply if reply.is_a?(CommandError)
|
122
150
|
|
123
|
-
if block_given?
|
151
|
+
if block_given? && reply != 'QUEUED'
|
124
152
|
yield reply
|
125
153
|
else
|
126
154
|
reply
|
@@ -152,13 +180,16 @@ class Redis
|
|
152
180
|
end
|
153
181
|
|
154
182
|
def call_pipeline(pipeline)
|
183
|
+
return [] if pipeline.futures.empty?
|
184
|
+
|
155
185
|
with_reconnect pipeline.with_reconnect? do
|
156
186
|
begin
|
157
|
-
pipeline.finish(call_pipelined(pipeline
|
187
|
+
pipeline.finish(call_pipelined(pipeline)).tap do
|
158
188
|
self.db = pipeline.db if pipeline.db
|
159
189
|
end
|
160
190
|
rescue ConnectionError => e
|
161
191
|
return nil if pipeline.shutdown?
|
192
|
+
|
162
193
|
# Assume the pipeline was sent in one piece, but execution of
|
163
194
|
# SHUTDOWN caused none of the replies for commands that were executed
|
164
195
|
# prior to it from coming back around.
|
@@ -167,8 +198,8 @@ class Redis
|
|
167
198
|
end
|
168
199
|
end
|
169
200
|
|
170
|
-
def call_pipelined(
|
171
|
-
return [] if
|
201
|
+
def call_pipelined(pipeline)
|
202
|
+
return [] if pipeline.futures.empty?
|
172
203
|
|
173
204
|
# The method #ensure_connected (called from #process) reconnects once on
|
174
205
|
# I/O errors. To make an effort in making sure that commands are not
|
@@ -178,6 +209,8 @@ class Redis
|
|
178
209
|
# already successfully executed commands. To circumvent this, don't retry
|
179
210
|
# after the first reply has been read successfully.
|
180
211
|
|
212
|
+
commands = pipeline.commands
|
213
|
+
|
181
214
|
result = Array.new(commands.size)
|
182
215
|
reconnect = @reconnect
|
183
216
|
|
@@ -185,13 +218,14 @@ class Redis
|
|
185
218
|
exception = nil
|
186
219
|
|
187
220
|
process(commands) do
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
result[i
|
221
|
+
pipeline.timeouts.each_with_index do |timeout, i|
|
222
|
+
reply = if timeout
|
223
|
+
with_socket_timeout(timeout) { read }
|
224
|
+
else
|
225
|
+
read
|
226
|
+
end
|
227
|
+
result[i] = reply
|
228
|
+
@reconnect = false
|
195
229
|
exception = reply if exception.nil? && reply.is_a?(CommandError)
|
196
230
|
end
|
197
231
|
end
|
@@ -234,12 +268,13 @@ class Redis
|
|
234
268
|
end
|
235
269
|
|
236
270
|
def connected?
|
237
|
-
!!
|
271
|
+
!!(connection && connection.connected?)
|
238
272
|
end
|
239
273
|
|
240
274
|
def disconnect
|
241
275
|
connection.disconnect if connected?
|
242
276
|
end
|
277
|
+
alias close disconnect
|
243
278
|
|
244
279
|
def reconnect
|
245
280
|
disconnect
|
@@ -274,12 +309,15 @@ class Redis
|
|
274
309
|
|
275
310
|
def with_socket_timeout(timeout)
|
276
311
|
connect unless connected?
|
312
|
+
original = @options[:read_timeout]
|
277
313
|
|
278
314
|
begin
|
279
315
|
connection.timeout = timeout
|
316
|
+
@options[:read_timeout] = timeout # for reconnection
|
280
317
|
yield
|
281
318
|
ensure
|
282
319
|
connection.timeout = self.timeout if connected?
|
320
|
+
@options[:read_timeout] = original
|
283
321
|
end
|
284
322
|
end
|
285
323
|
|
@@ -287,30 +325,27 @@ class Redis
|
|
287
325
|
with_socket_timeout(0, &blk)
|
288
326
|
end
|
289
327
|
|
290
|
-
def with_reconnect(val=true)
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
@reconnect = original
|
296
|
-
end
|
328
|
+
def with_reconnect(val = true)
|
329
|
+
original, @reconnect = @reconnect, val
|
330
|
+
yield
|
331
|
+
ensure
|
332
|
+
@reconnect = original
|
297
333
|
end
|
298
334
|
|
299
335
|
def without_reconnect(&blk)
|
300
336
|
with_reconnect(false, &blk)
|
301
337
|
end
|
302
338
|
|
303
|
-
|
339
|
+
protected
|
304
340
|
|
305
341
|
def logging(commands)
|
306
|
-
return yield unless @logger
|
342
|
+
return yield unless @logger&.debug?
|
307
343
|
|
308
344
|
begin
|
309
345
|
commands.each do |name, *args|
|
310
346
|
logged_args = args.map do |a|
|
311
|
-
|
312
|
-
|
313
|
-
when a.respond_to?(:to_s) then a.to_s
|
347
|
+
if a.respond_to?(:inspect) then a.inspect
|
348
|
+
elsif a.respond_to?(:to_s) then a.to_s
|
314
349
|
else
|
315
350
|
# handle poorly-behaved descendants of BasicObject
|
316
351
|
klass = a.instance_exec { (class << self; self end).superclass }
|
@@ -336,13 +371,17 @@ class Redis
|
|
336
371
|
@connection = @options[:driver].connect(@options)
|
337
372
|
@pending_reads = 0
|
338
373
|
rescue TimeoutError,
|
374
|
+
SocketError,
|
375
|
+
Errno::EADDRNOTAVAIL,
|
339
376
|
Errno::ECONNREFUSED,
|
340
377
|
Errno::EHOSTDOWN,
|
341
378
|
Errno::EHOSTUNREACH,
|
342
379
|
Errno::ENETUNREACH,
|
343
|
-
Errno::
|
380
|
+
Errno::ENOENT,
|
381
|
+
Errno::ETIMEDOUT,
|
382
|
+
Errno::EINVAL => error
|
344
383
|
|
345
|
-
raise CannotConnectError, "Error connecting to Redis on #{location} (#{
|
384
|
+
raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
|
346
385
|
end
|
347
386
|
|
348
387
|
def ensure_connected
|
@@ -356,9 +395,9 @@ class Redis
|
|
356
395
|
if connected?
|
357
396
|
unless inherit_socket? || Process.pid == @pid
|
358
397
|
raise InheritedError,
|
359
|
-
|
360
|
-
|
361
|
-
|
398
|
+
"Tried to use a connection from a child process without reconnecting. " \
|
399
|
+
"You need to reconnect to Redis after forking " \
|
400
|
+
"or set :inherit_socket to true."
|
362
401
|
end
|
363
402
|
else
|
364
403
|
connect
|
@@ -369,6 +408,10 @@ class Redis
|
|
369
408
|
disconnect
|
370
409
|
|
371
410
|
if attempts <= @options[:reconnect_attempts] && @reconnect
|
411
|
+
sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
|
412
|
+
@options[:reconnect_delay_max]].min
|
413
|
+
|
414
|
+
Kernel.sleep(sleep_t)
|
372
415
|
retry
|
373
416
|
else
|
374
417
|
raise
|
@@ -387,15 +430,14 @@ class Redis
|
|
387
430
|
|
388
431
|
defaults.keys.each do |key|
|
389
432
|
# Fill in defaults if needed
|
390
|
-
if defaults[key].respond_to?(:call)
|
391
|
-
defaults[key] = defaults[key].call
|
392
|
-
end
|
433
|
+
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
393
434
|
|
394
435
|
# Symbolize only keys that are needed
|
395
|
-
options[key] = options[key.to_s] if options.
|
436
|
+
options[key] = options[key.to_s] if options.key?(key.to_s)
|
396
437
|
end
|
397
438
|
|
398
|
-
url = options[:url]
|
439
|
+
url = options[:url]
|
440
|
+
url = defaults[:url] if url.nil?
|
399
441
|
|
400
442
|
# Override defaults from URL if given
|
401
443
|
if url
|
@@ -404,12 +446,13 @@ class Redis
|
|
404
446
|
uri = URI(url)
|
405
447
|
|
406
448
|
if uri.scheme == "unix"
|
407
|
-
defaults[:path]
|
449
|
+
defaults[:path] = uri.path
|
408
450
|
elsif uri.scheme == "redis" || uri.scheme == "rediss"
|
409
451
|
defaults[:scheme] = uri.scheme
|
410
452
|
defaults[:host] = uri.host if uri.host
|
411
453
|
defaults[:port] = uri.port if uri.port
|
412
|
-
defaults[:
|
454
|
+
defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
|
455
|
+
defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
|
413
456
|
defaults[:db] = uri.path[1..-1].to_i if uri.path
|
414
457
|
defaults[:role] = :master
|
415
458
|
else
|
@@ -435,7 +478,7 @@ class Redis
|
|
435
478
|
options[:port] = options[:port].to_i
|
436
479
|
end
|
437
480
|
|
438
|
-
if options.
|
481
|
+
if options.key?(:timeout)
|
439
482
|
options[:connect_timeout] ||= options[:timeout]
|
440
483
|
options[:read_timeout] ||= options[:timeout]
|
441
484
|
options[:write_timeout] ||= options[:timeout]
|
@@ -445,26 +488,30 @@ class Redis
|
|
445
488
|
options[:read_timeout] = Float(options[:read_timeout])
|
446
489
|
options[:write_timeout] = Float(options[:write_timeout])
|
447
490
|
|
491
|
+
options[:reconnect_attempts] = options[:reconnect_attempts].to_i
|
492
|
+
options[:reconnect_delay] = options[:reconnect_delay].to_f
|
493
|
+
options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
|
494
|
+
|
448
495
|
options[:db] = options[:db].to_i
|
449
496
|
options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
|
450
497
|
|
451
498
|
case options[:tcp_keepalive]
|
452
499
|
when Hash
|
453
|
-
[
|
454
|
-
unless options[:tcp_keepalive][key].is_a?(
|
455
|
-
raise "Expected the #{key.inspect} key in :tcp_keepalive to be
|
500
|
+
%i[time intvl probes].each do |key|
|
501
|
+
unless options[:tcp_keepalive][key].is_a?(Integer)
|
502
|
+
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
456
503
|
end
|
457
504
|
end
|
458
505
|
|
459
|
-
when
|
506
|
+
when Integer
|
460
507
|
if options[:tcp_keepalive] >= 60
|
461
|
-
options[:tcp_keepalive] = {:
|
508
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
462
509
|
|
463
510
|
elsif options[:tcp_keepalive] >= 30
|
464
|
-
options[:tcp_keepalive] = {:
|
511
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
465
512
|
|
466
513
|
elsif options[:tcp_keepalive] >= 5
|
467
|
-
options[:tcp_keepalive] = {:
|
514
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
468
515
|
end
|
469
516
|
end
|
470
517
|
|
@@ -476,14 +523,14 @@ class Redis
|
|
476
523
|
def _parse_driver(driver)
|
477
524
|
driver = driver.to_s if driver.is_a?(Symbol)
|
478
525
|
|
479
|
-
if driver.
|
526
|
+
if driver.is_a?(String)
|
480
527
|
begin
|
481
528
|
require_relative "connection/#{driver}"
|
482
|
-
rescue LoadError, NameError
|
529
|
+
rescue LoadError, NameError
|
483
530
|
begin
|
484
|
-
require "connection/#{driver}"
|
485
|
-
rescue LoadError, NameError =>
|
486
|
-
raise
|
531
|
+
require "redis/connection/#{driver}"
|
532
|
+
rescue LoadError, NameError => error
|
533
|
+
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
487
534
|
end
|
488
535
|
end
|
489
536
|
|
@@ -502,18 +549,16 @@ class Redis
|
|
502
549
|
@options
|
503
550
|
end
|
504
551
|
|
505
|
-
def check(client)
|
506
|
-
end
|
552
|
+
def check(client); end
|
507
553
|
|
508
554
|
class Sentinel < Connector
|
509
555
|
def initialize(options)
|
510
556
|
super(options)
|
511
557
|
|
512
|
-
@options[:password] = DEFAULTS.fetch(:password)
|
513
558
|
@options[:db] = DEFAULTS.fetch(:db)
|
514
559
|
|
515
560
|
@sentinels = @options.delete(:sentinels).dup
|
516
|
-
@role = @options
|
561
|
+
@role = (@options[:role] || "master").to_s
|
517
562
|
@master = @options[:host]
|
518
563
|
end
|
519
564
|
|
@@ -536,13 +581,13 @@ class Redis
|
|
536
581
|
|
537
582
|
def resolve
|
538
583
|
result = case @role
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
584
|
+
when "master"
|
585
|
+
resolve_master
|
586
|
+
when "slave"
|
587
|
+
resolve_slave
|
588
|
+
else
|
589
|
+
raise ArgumentError, "Unknown instance role #{@role}"
|
590
|
+
end
|
546
591
|
|
547
592
|
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
548
593
|
end
|
@@ -550,10 +595,12 @@ class Redis
|
|
550
595
|
def sentinel_detect
|
551
596
|
@sentinels.each do |sentinel|
|
552
597
|
client = Client.new(@options.merge({
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
598
|
+
host: sentinel[:host] || sentinel["host"],
|
599
|
+
port: sentinel[:port] || sentinel["port"],
|
600
|
+
username: sentinel[:username] || sentinel["username"],
|
601
|
+
password: sentinel[:password] || sentinel["password"],
|
602
|
+
reconnect_attempts: 0
|
603
|
+
}))
|
557
604
|
|
558
605
|
begin
|
559
606
|
if result = yield(client)
|
@@ -575,7 +622,7 @@ class Redis
|
|
575
622
|
def resolve_master
|
576
623
|
sentinel_detect do |client|
|
577
624
|
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
578
|
-
{:
|
625
|
+
{ host: reply[0], port: reply[1] }
|
579
626
|
end
|
580
627
|
end
|
581
628
|
end
|
@@ -583,9 +630,19 @@ class Redis
|
|
583
630
|
def resolve_slave
|
584
631
|
sentinel_detect do |client|
|
585
632
|
if reply = client.call(["sentinel", "slaves", @master])
|
586
|
-
|
587
|
-
|
588
|
-
{
|
633
|
+
slaves = reply.map { |s| s.each_slice(2).to_h }
|
634
|
+
slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
|
635
|
+
slaves.reject! { |s| s.fetch('flags').include?('s_down') }
|
636
|
+
|
637
|
+
if slaves.empty?
|
638
|
+
raise CannotConnectError, 'No slaves available.'
|
639
|
+
else
|
640
|
+
slave = slaves.sample
|
641
|
+
{
|
642
|
+
host: slave.fetch('ip'),
|
643
|
+
port: slave.fetch('port')
|
644
|
+
}
|
645
|
+
end
|
589
646
|
end
|
590
647
|
end
|
591
648
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors'
|
4
|
+
|
5
|
+
class Redis
|
6
|
+
class Cluster
|
7
|
+
# Keep details about Redis commands for Redis Cluster Client.
|
8
|
+
# @see https://redis.io/commands/command
|
9
|
+
class Command
|
10
|
+
def initialize(details)
|
11
|
+
@details = pick_details(details)
|
12
|
+
end
|
13
|
+
|
14
|
+
def extract_first_key(command)
|
15
|
+
i = determine_first_key_position(command)
|
16
|
+
return '' if i == 0
|
17
|
+
|
18
|
+
key = command[i].to_s
|
19
|
+
hash_tag = extract_hash_tag(key)
|
20
|
+
hash_tag.empty? ? key : hash_tag
|
21
|
+
end
|
22
|
+
|
23
|
+
def should_send_to_master?(command)
|
24
|
+
dig_details(command, :write)
|
25
|
+
end
|
26
|
+
|
27
|
+
def should_send_to_slave?(command)
|
28
|
+
dig_details(command, :readonly)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def pick_details(details)
|
34
|
+
details.map do |command, detail|
|
35
|
+
[command, {
|
36
|
+
first_key_position: detail[:first],
|
37
|
+
write: detail[:flags].include?('write'),
|
38
|
+
readonly: detail[:flags].include?('readonly')
|
39
|
+
}]
|
40
|
+
end.to_h
|
41
|
+
end
|
42
|
+
|
43
|
+
def dig_details(command, key)
|
44
|
+
name = command.first.to_s
|
45
|
+
return unless @details.key?(name)
|
46
|
+
|
47
|
+
@details.fetch(name).fetch(key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def determine_first_key_position(command)
|
51
|
+
case command.first.to_s.downcase
|
52
|
+
when 'eval', 'evalsha', 'migrate', 'zinterstore', 'zunionstore' then 3
|
53
|
+
when 'object' then 2
|
54
|
+
when 'memory'
|
55
|
+
command[1].to_s.casecmp('usage').zero? ? 2 : 0
|
56
|
+
when 'scan', 'sscan', 'hscan', 'zscan'
|
57
|
+
determine_optional_key_position(command, 'match')
|
58
|
+
when 'xread', 'xreadgroup'
|
59
|
+
determine_optional_key_position(command, 'streams')
|
60
|
+
else
|
61
|
+
dig_details(command, :first_key_position).to_i
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def determine_optional_key_position(command, option_name)
|
66
|
+
idx = command.map(&:to_s).map(&:downcase).index(option_name)
|
67
|
+
idx.nil? ? 0 : idx + 1
|
68
|
+
end
|
69
|
+
|
70
|
+
# @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
|
71
|
+
def extract_hash_tag(key)
|
72
|
+
s = key.index('{')
|
73
|
+
e = key.index('}', s.to_i + 1)
|
74
|
+
|
75
|
+
return '' if s.nil? || e.nil?
|
76
|
+
|
77
|
+
key[s + 1..e - 1]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors'
|
4
|
+
|
5
|
+
class Redis
|
6
|
+
class Cluster
|
7
|
+
# Load details about Redis commands for Redis Cluster Client
|
8
|
+
# @see https://redis.io/commands/command
|
9
|
+
module CommandLoader
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def load(nodes)
|
13
|
+
details = {}
|
14
|
+
|
15
|
+
nodes.each do |node|
|
16
|
+
details = fetch_command_details(node)
|
17
|
+
details.empty? ? next : break
|
18
|
+
end
|
19
|
+
|
20
|
+
details
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch_command_details(node)
|
24
|
+
node.call(%i[command]).map do |reply|
|
25
|
+
[reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
|
26
|
+
end.to_h
|
27
|
+
rescue CannotConnectError, ConnectionError, CommandError
|
28
|
+
{} # can retry on another node
|
29
|
+
end
|
30
|
+
|
31
|
+
private_class_method :fetch_command_details
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
class Cluster
|
5
|
+
# Key to slot converter for Redis Cluster Client
|
6
|
+
#
|
7
|
+
# We can test it by `CLUSTER KEYSLOT` command.
|
8
|
+
#
|
9
|
+
# @see https://github.com/antirez/redis-rb-cluster
|
10
|
+
# Reference implementation in Ruby
|
11
|
+
# @see https://redis.io/topics/cluster-spec#appendix
|
12
|
+
# Reference implementation in ANSI C
|
13
|
+
# @see https://redis.io/commands/cluster-keyslot
|
14
|
+
# CLUSTER KEYSLOT command reference
|
15
|
+
#
|
16
|
+
# Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com>
|
17
|
+
module KeySlotConverter
|
18
|
+
XMODEM_CRC16_LOOKUP = [
|
19
|
+
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
|
20
|
+
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
|
21
|
+
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
|
22
|
+
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
|
23
|
+
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
|
24
|
+
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
|
25
|
+
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
|
26
|
+
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
|
27
|
+
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
|
28
|
+
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
|
29
|
+
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
|
30
|
+
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
|
31
|
+
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
|
32
|
+
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
|
33
|
+
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
|
34
|
+
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
|
35
|
+
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
|
36
|
+
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
|
37
|
+
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
|
38
|
+
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
|
39
|
+
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
|
40
|
+
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
|
41
|
+
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
|
42
|
+
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
|
43
|
+
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
|
44
|
+
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
|
45
|
+
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
|
46
|
+
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
|
47
|
+
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
|
48
|
+
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
|
49
|
+
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
|
50
|
+
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
|
51
|
+
].freeze
|
52
|
+
|
53
|
+
HASH_SLOTS = 16_384
|
54
|
+
|
55
|
+
module_function
|
56
|
+
|
57
|
+
# Convert key into slot.
|
58
|
+
#
|
59
|
+
# @param key [String] the key of the redis command
|
60
|
+
#
|
61
|
+
# @return [Integer] slot number
|
62
|
+
def convert(key)
|
63
|
+
crc = 0
|
64
|
+
key.each_byte do |b|
|
65
|
+
crc = ((crc << 8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((crc >> 8) ^ b) & 0xff]
|
66
|
+
end
|
67
|
+
|
68
|
+
crc % HASH_SLOTS
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|