redis 3.3.5 → 4.5.1
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 +161 -2
- data/README.md +144 -79
- data/lib/redis/client.rb +166 -90
- 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 +6 -5
- data/lib/redis/connection/registry.rb +2 -1
- data/lib/redis/connection/ruby.rb +128 -129
- data/lib/redis/connection/synchrony.rb +21 -8
- data/lib/redis/connection.rb +4 -2
- data/lib/redis/distributed.rb +194 -72
- data/lib/redis/errors.rb +48 -0
- data/lib/redis/hash_ring.rb +30 -73
- data/lib/redis/pipeline.rb +55 -15
- data/lib/redis/subscribe.rb +11 -12
- data/lib/redis/version.rb +3 -1
- data/lib/redis.rb +1451 -403
- metadata +49 -202
- data/.gitignore +0 -16
- data/.travis/Gemfile +0 -11
- data/.travis.yml +0 -89
- data/.yardopts +0 -3
- data/Gemfile +0 -4
- data/Rakefile +0 -87
- data/benchmarking/logging.rb +0 -71
- data/benchmarking/pipeline.rb +0 -51
- data/benchmarking/speed.rb +0 -21
- data/benchmarking/suite.rb +0 -24
- data/benchmarking/worker.rb +0 -71
- data/examples/basic.rb +0 -15
- data/examples/consistency.rb +0 -114
- data/examples/dist_redis.rb +0 -43
- data/examples/incr-decr.rb +0 -17
- data/examples/list.rb +0 -26
- data/examples/pubsub.rb +0 -37
- data/examples/sentinel/sentinel.conf +0 -9
- data/examples/sentinel/start +0 -49
- data/examples/sentinel.rb +0 -41
- data/examples/sets.rb +0 -36
- data/examples/unicorn/config.ru +0 -3
- data/examples/unicorn/unicorn.rb +0 -20
- data/redis.gemspec +0 -44
- data/test/bitpos_test.rb +0 -69
- data/test/blocking_commands_test.rb +0 -42
- data/test/client_test.rb +0 -59
- data/test/command_map_test.rb +0 -30
- data/test/commands_on_hashes_test.rb +0 -21
- data/test/commands_on_hyper_log_log_test.rb +0 -21
- data/test/commands_on_lists_test.rb +0 -20
- data/test/commands_on_sets_test.rb +0 -77
- data/test/commands_on_sorted_sets_test.rb +0 -137
- data/test/commands_on_strings_test.rb +0 -101
- data/test/commands_on_value_types_test.rb +0 -133
- data/test/connection_handling_test.rb +0 -277
- data/test/connection_test.rb +0 -57
- data/test/db/.gitkeep +0 -0
- data/test/distributed_blocking_commands_test.rb +0 -46
- data/test/distributed_commands_on_hashes_test.rb +0 -10
- data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
- data/test/distributed_commands_on_lists_test.rb +0 -22
- data/test/distributed_commands_on_sets_test.rb +0 -83
- data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
- data/test/distributed_commands_on_strings_test.rb +0 -59
- data/test/distributed_commands_on_value_types_test.rb +0 -95
- data/test/distributed_commands_requiring_clustering_test.rb +0 -164
- data/test/distributed_connection_handling_test.rb +0 -23
- data/test/distributed_internals_test.rb +0 -79
- data/test/distributed_key_tags_test.rb +0 -52
- data/test/distributed_persistence_control_commands_test.rb +0 -26
- data/test/distributed_publish_subscribe_test.rb +0 -92
- data/test/distributed_remote_server_control_commands_test.rb +0 -66
- data/test/distributed_scripting_test.rb +0 -102
- data/test/distributed_sorting_test.rb +0 -20
- data/test/distributed_test.rb +0 -58
- data/test/distributed_transactions_test.rb +0 -32
- data/test/encoding_test.rb +0 -18
- data/test/error_replies_test.rb +0 -59
- data/test/fork_safety_test.rb +0 -65
- data/test/helper.rb +0 -232
- data/test/helper_test.rb +0 -24
- data/test/internals_test.rb +0 -417
- data/test/lint/blocking_commands.rb +0 -150
- data/test/lint/hashes.rb +0 -162
- data/test/lint/hyper_log_log.rb +0 -60
- data/test/lint/lists.rb +0 -143
- data/test/lint/sets.rb +0 -140
- data/test/lint/sorted_sets.rb +0 -316
- data/test/lint/strings.rb +0 -260
- data/test/lint/value_types.rb +0 -122
- data/test/persistence_control_commands_test.rb +0 -26
- data/test/pipelining_commands_test.rb +0 -242
- data/test/publish_subscribe_test.rb +0 -282
- data/test/remote_server_control_commands_test.rb +0 -118
- data/test/scanning_test.rb +0 -413
- data/test/scripting_test.rb +0 -78
- data/test/sentinel_command_test.rb +0 -80
- data/test/sentinel_test.rb +0 -255
- data/test/sorting_test.rb +0 -59
- data/test/ssl_test.rb +0 -73
- 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 -88
- data/test/test.conf.erb +0 -9
- data/test/thread_safety_test.rb +0 -62
- data/test/transactions_test.rb +0 -264
- data/test/unknown_commands_test.rb +0 -14
- data/test/url_param_test.rb +0 -138
data/lib/redis/client.rb
CHANGED
@@ -1,29 +1,38 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
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,33 @@ 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 => err # Likely on Redis < 6
|
123
|
+
if err.message.match?(/ERR wrong number of arguments for \'auth\' command/)
|
124
|
+
call [:auth, password]
|
125
|
+
elsif err.message.match?(/WRONGPASS invalid username-password pair/)
|
126
|
+
begin
|
127
|
+
call [:auth, password]
|
128
|
+
rescue CommandError
|
129
|
+
raise err
|
130
|
+
end
|
131
|
+
::Kernel.warn(
|
132
|
+
"[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
|
133
|
+
" the provided password was for the default user. This will start failing in redis-rb 4.6."
|
134
|
+
)
|
135
|
+
else
|
136
|
+
raise
|
137
|
+
end
|
138
|
+
end
|
139
|
+
else
|
140
|
+
call [:auth, password]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
call [:readonly] if @options[:readonly]
|
103
145
|
call [:select, db] if db != 0
|
104
146
|
call [:client, :setname, @options[:id]] if @options[:id]
|
105
147
|
@connector.check(self)
|
@@ -120,7 +162,7 @@ class Redis
|
|
120
162
|
reply = process([command]) { read }
|
121
163
|
raise reply if reply.is_a?(CommandError)
|
122
164
|
|
123
|
-
if block_given?
|
165
|
+
if block_given? && reply != 'QUEUED'
|
124
166
|
yield reply
|
125
167
|
else
|
126
168
|
reply
|
@@ -152,13 +194,16 @@ class Redis
|
|
152
194
|
end
|
153
195
|
|
154
196
|
def call_pipeline(pipeline)
|
197
|
+
return [] if pipeline.futures.empty?
|
198
|
+
|
155
199
|
with_reconnect pipeline.with_reconnect? do
|
156
200
|
begin
|
157
|
-
pipeline.finish(call_pipelined(pipeline
|
201
|
+
pipeline.finish(call_pipelined(pipeline)).tap do
|
158
202
|
self.db = pipeline.db if pipeline.db
|
159
203
|
end
|
160
204
|
rescue ConnectionError => e
|
161
205
|
return nil if pipeline.shutdown?
|
206
|
+
|
162
207
|
# Assume the pipeline was sent in one piece, but execution of
|
163
208
|
# SHUTDOWN caused none of the replies for commands that were executed
|
164
209
|
# prior to it from coming back around.
|
@@ -167,8 +212,8 @@ class Redis
|
|
167
212
|
end
|
168
213
|
end
|
169
214
|
|
170
|
-
def call_pipelined(
|
171
|
-
return [] if
|
215
|
+
def call_pipelined(pipeline)
|
216
|
+
return [] if pipeline.futures.empty?
|
172
217
|
|
173
218
|
# The method #ensure_connected (called from #process) reconnects once on
|
174
219
|
# I/O errors. To make an effort in making sure that commands are not
|
@@ -178,6 +223,8 @@ class Redis
|
|
178
223
|
# already successfully executed commands. To circumvent this, don't retry
|
179
224
|
# after the first reply has been read successfully.
|
180
225
|
|
226
|
+
commands = pipeline.commands
|
227
|
+
|
181
228
|
result = Array.new(commands.size)
|
182
229
|
reconnect = @reconnect
|
183
230
|
|
@@ -185,13 +232,14 @@ class Redis
|
|
185
232
|
exception = nil
|
186
233
|
|
187
234
|
process(commands) do
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
result[i
|
235
|
+
pipeline.timeouts.each_with_index do |timeout, i|
|
236
|
+
reply = if timeout
|
237
|
+
with_socket_timeout(timeout) { read }
|
238
|
+
else
|
239
|
+
read
|
240
|
+
end
|
241
|
+
result[i] = reply
|
242
|
+
@reconnect = false
|
195
243
|
exception = reply if exception.nil? && reply.is_a?(CommandError)
|
196
244
|
end
|
197
245
|
end
|
@@ -234,12 +282,13 @@ class Redis
|
|
234
282
|
end
|
235
283
|
|
236
284
|
def connected?
|
237
|
-
!!
|
285
|
+
!!(connection && connection.connected?)
|
238
286
|
end
|
239
287
|
|
240
288
|
def disconnect
|
241
289
|
connection.disconnect if connected?
|
242
290
|
end
|
291
|
+
alias close disconnect
|
243
292
|
|
244
293
|
def reconnect
|
245
294
|
disconnect
|
@@ -274,12 +323,15 @@ class Redis
|
|
274
323
|
|
275
324
|
def with_socket_timeout(timeout)
|
276
325
|
connect unless connected?
|
326
|
+
original = @options[:read_timeout]
|
277
327
|
|
278
328
|
begin
|
279
329
|
connection.timeout = timeout
|
330
|
+
@options[:read_timeout] = timeout # for reconnection
|
280
331
|
yield
|
281
332
|
ensure
|
282
333
|
connection.timeout = self.timeout if connected?
|
334
|
+
@options[:read_timeout] = original
|
283
335
|
end
|
284
336
|
end
|
285
337
|
|
@@ -287,30 +339,27 @@ class Redis
|
|
287
339
|
with_socket_timeout(0, &blk)
|
288
340
|
end
|
289
341
|
|
290
|
-
def with_reconnect(val=true)
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
@reconnect = original
|
296
|
-
end
|
342
|
+
def with_reconnect(val = true)
|
343
|
+
original, @reconnect = @reconnect, val
|
344
|
+
yield
|
345
|
+
ensure
|
346
|
+
@reconnect = original
|
297
347
|
end
|
298
348
|
|
299
349
|
def without_reconnect(&blk)
|
300
350
|
with_reconnect(false, &blk)
|
301
351
|
end
|
302
352
|
|
303
|
-
|
353
|
+
protected
|
304
354
|
|
305
355
|
def logging(commands)
|
306
|
-
return yield unless @logger
|
356
|
+
return yield unless @logger&.debug?
|
307
357
|
|
308
358
|
begin
|
309
359
|
commands.each do |name, *args|
|
310
360
|
logged_args = args.map do |a|
|
311
|
-
|
312
|
-
|
313
|
-
when a.respond_to?(:to_s) then a.to_s
|
361
|
+
if a.respond_to?(:inspect) then a.inspect
|
362
|
+
elsif a.respond_to?(:to_s) then a.to_s
|
314
363
|
else
|
315
364
|
# handle poorly-behaved descendants of BasicObject
|
316
365
|
klass = a.instance_exec { (class << self; self end).superclass }
|
@@ -336,13 +385,17 @@ class Redis
|
|
336
385
|
@connection = @options[:driver].connect(@options)
|
337
386
|
@pending_reads = 0
|
338
387
|
rescue TimeoutError,
|
388
|
+
SocketError,
|
389
|
+
Errno::EADDRNOTAVAIL,
|
339
390
|
Errno::ECONNREFUSED,
|
340
391
|
Errno::EHOSTDOWN,
|
341
392
|
Errno::EHOSTUNREACH,
|
342
393
|
Errno::ENETUNREACH,
|
343
|
-
Errno::
|
394
|
+
Errno::ENOENT,
|
395
|
+
Errno::ETIMEDOUT,
|
396
|
+
Errno::EINVAL => error
|
344
397
|
|
345
|
-
raise CannotConnectError, "Error connecting to Redis on #{location} (#{
|
398
|
+
raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
|
346
399
|
end
|
347
400
|
|
348
401
|
def ensure_connected
|
@@ -356,9 +409,9 @@ class Redis
|
|
356
409
|
if connected?
|
357
410
|
unless inherit_socket? || Process.pid == @pid
|
358
411
|
raise InheritedError,
|
359
|
-
|
360
|
-
|
361
|
-
|
412
|
+
"Tried to use a connection from a child process without reconnecting. " \
|
413
|
+
"You need to reconnect to Redis after forking " \
|
414
|
+
"or set :inherit_socket to true."
|
362
415
|
end
|
363
416
|
else
|
364
417
|
connect
|
@@ -369,6 +422,10 @@ class Redis
|
|
369
422
|
disconnect
|
370
423
|
|
371
424
|
if attempts <= @options[:reconnect_attempts] && @reconnect
|
425
|
+
sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
|
426
|
+
@options[:reconnect_delay_max]].min
|
427
|
+
|
428
|
+
Kernel.sleep(sleep_t)
|
372
429
|
retry
|
373
430
|
else
|
374
431
|
raise
|
@@ -387,15 +444,14 @@ class Redis
|
|
387
444
|
|
388
445
|
defaults.keys.each do |key|
|
389
446
|
# Fill in defaults if needed
|
390
|
-
if defaults[key].respond_to?(:call)
|
391
|
-
defaults[key] = defaults[key].call
|
392
|
-
end
|
447
|
+
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
393
448
|
|
394
449
|
# Symbolize only keys that are needed
|
395
|
-
options[key] = options[key.to_s] if options.
|
450
|
+
options[key] = options[key.to_s] if options.key?(key.to_s)
|
396
451
|
end
|
397
452
|
|
398
|
-
url = options[:url]
|
453
|
+
url = options[:url]
|
454
|
+
url = defaults[:url] if url.nil?
|
399
455
|
|
400
456
|
# Override defaults from URL if given
|
401
457
|
if url
|
@@ -404,12 +460,13 @@ class Redis
|
|
404
460
|
uri = URI(url)
|
405
461
|
|
406
462
|
if uri.scheme == "unix"
|
407
|
-
defaults[:path]
|
463
|
+
defaults[:path] = uri.path
|
408
464
|
elsif uri.scheme == "redis" || uri.scheme == "rediss"
|
409
465
|
defaults[:scheme] = uri.scheme
|
410
466
|
defaults[:host] = uri.host if uri.host
|
411
467
|
defaults[:port] = uri.port if uri.port
|
412
|
-
defaults[:
|
468
|
+
defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
|
469
|
+
defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
|
413
470
|
defaults[:db] = uri.path[1..-1].to_i if uri.path
|
414
471
|
defaults[:role] = :master
|
415
472
|
else
|
@@ -435,7 +492,7 @@ class Redis
|
|
435
492
|
options[:port] = options[:port].to_i
|
436
493
|
end
|
437
494
|
|
438
|
-
if options.
|
495
|
+
if options.key?(:timeout)
|
439
496
|
options[:connect_timeout] ||= options[:timeout]
|
440
497
|
options[:read_timeout] ||= options[:timeout]
|
441
498
|
options[:write_timeout] ||= options[:timeout]
|
@@ -445,12 +502,16 @@ class Redis
|
|
445
502
|
options[:read_timeout] = Float(options[:read_timeout])
|
446
503
|
options[:write_timeout] = Float(options[:write_timeout])
|
447
504
|
|
505
|
+
options[:reconnect_attempts] = options[:reconnect_attempts].to_i
|
506
|
+
options[:reconnect_delay] = options[:reconnect_delay].to_f
|
507
|
+
options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
|
508
|
+
|
448
509
|
options[:db] = options[:db].to_i
|
449
510
|
options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
|
450
511
|
|
451
512
|
case options[:tcp_keepalive]
|
452
513
|
when Hash
|
453
|
-
[
|
514
|
+
%i[time intvl probes].each do |key|
|
454
515
|
unless options[:tcp_keepalive][key].is_a?(Integer)
|
455
516
|
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
456
517
|
end
|
@@ -458,13 +519,13 @@ class Redis
|
|
458
519
|
|
459
520
|
when Integer
|
460
521
|
if options[:tcp_keepalive] >= 60
|
461
|
-
options[:tcp_keepalive] = {:
|
522
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
462
523
|
|
463
524
|
elsif options[:tcp_keepalive] >= 30
|
464
|
-
options[:tcp_keepalive] = {:
|
525
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
465
526
|
|
466
527
|
elsif options[:tcp_keepalive] >= 5
|
467
|
-
options[:tcp_keepalive] = {:
|
528
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
468
529
|
end
|
469
530
|
end
|
470
531
|
|
@@ -476,13 +537,18 @@ class Redis
|
|
476
537
|
def _parse_driver(driver)
|
477
538
|
driver = driver.to_s if driver.is_a?(Symbol)
|
478
539
|
|
479
|
-
if driver.
|
540
|
+
if driver.is_a?(String)
|
480
541
|
begin
|
481
|
-
|
482
|
-
driver = Connection.const_get(driver.capitalize)
|
542
|
+
require_relative "connection/#{driver}"
|
483
543
|
rescue LoadError, NameError
|
484
|
-
|
544
|
+
begin
|
545
|
+
require "redis/connection/#{driver}"
|
546
|
+
rescue LoadError, NameError => error
|
547
|
+
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
548
|
+
end
|
485
549
|
end
|
550
|
+
|
551
|
+
driver = Connection.const_get(driver.capitalize)
|
486
552
|
end
|
487
553
|
|
488
554
|
driver
|
@@ -497,18 +563,16 @@ class Redis
|
|
497
563
|
@options
|
498
564
|
end
|
499
565
|
|
500
|
-
def check(client)
|
501
|
-
end
|
566
|
+
def check(client); end
|
502
567
|
|
503
568
|
class Sentinel < Connector
|
504
569
|
def initialize(options)
|
505
570
|
super(options)
|
506
571
|
|
507
|
-
@options[:password] = DEFAULTS.fetch(:password)
|
508
572
|
@options[:db] = DEFAULTS.fetch(:db)
|
509
573
|
|
510
574
|
@sentinels = @options.delete(:sentinels).dup
|
511
|
-
@role = @options
|
575
|
+
@role = (@options[:role] || "master").to_s
|
512
576
|
@master = @options[:host]
|
513
577
|
end
|
514
578
|
|
@@ -531,13 +595,13 @@ class Redis
|
|
531
595
|
|
532
596
|
def resolve
|
533
597
|
result = case @role
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
598
|
+
when "master"
|
599
|
+
resolve_master
|
600
|
+
when "slave"
|
601
|
+
resolve_slave
|
602
|
+
else
|
603
|
+
raise ArgumentError, "Unknown instance role #{@role}"
|
604
|
+
end
|
541
605
|
|
542
606
|
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
543
607
|
end
|
@@ -545,10 +609,12 @@ class Redis
|
|
545
609
|
def sentinel_detect
|
546
610
|
@sentinels.each do |sentinel|
|
547
611
|
client = Client.new(@options.merge({
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
612
|
+
host: sentinel[:host] || sentinel["host"],
|
613
|
+
port: sentinel[:port] || sentinel["port"],
|
614
|
+
username: sentinel[:username] || sentinel["username"],
|
615
|
+
password: sentinel[:password] || sentinel["password"],
|
616
|
+
reconnect_attempts: 0
|
617
|
+
}))
|
552
618
|
|
553
619
|
begin
|
554
620
|
if result = yield(client)
|
@@ -570,7 +636,7 @@ class Redis
|
|
570
636
|
def resolve_master
|
571
637
|
sentinel_detect do |client|
|
572
638
|
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
573
|
-
{:
|
639
|
+
{ host: reply[0], port: reply[1] }
|
574
640
|
end
|
575
641
|
end
|
576
642
|
end
|
@@ -578,9 +644,19 @@ class Redis
|
|
578
644
|
def resolve_slave
|
579
645
|
sentinel_detect do |client|
|
580
646
|
if reply = client.call(["sentinel", "slaves", @master])
|
581
|
-
|
582
|
-
|
583
|
-
{
|
647
|
+
slaves = reply.map { |s| s.each_slice(2).to_h }
|
648
|
+
slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
|
649
|
+
slaves.reject! { |s| s.fetch('flags').include?('s_down') }
|
650
|
+
|
651
|
+
if slaves.empty?
|
652
|
+
raise CannotConnectError, 'No slaves available.'
|
653
|
+
else
|
654
|
+
slave = slaves.sample
|
655
|
+
{
|
656
|
+
host: slave.fetch('ip'),
|
657
|
+
port: slave.fetch('port')
|
658
|
+
}
|
659
|
+
end
|
584
660
|
end
|
585
661
|
end
|
586
662
|
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,33 @@
|
|
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
|
+
nodes.each do |node|
|
14
|
+
begin
|
15
|
+
return fetch_command_details(node)
|
16
|
+
rescue CannotConnectError, ConnectionError, CommandError
|
17
|
+
next # can retry on another node
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
|
22
|
+
end
|
23
|
+
|
24
|
+
def fetch_command_details(node)
|
25
|
+
node.call(%i[command]).map do |reply|
|
26
|
+
[reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
|
27
|
+
end.to_h
|
28
|
+
end
|
29
|
+
|
30
|
+
private_class_method :fetch_command_details
|
31
|
+
end
|
32
|
+
end
|
33
|
+
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
|