redis 3.3.5 → 4.8.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 +232 -2
- data/README.md +169 -89
- data/lib/redis/client.rb +177 -100
- data/lib/redis/cluster/command.rb +79 -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 +120 -0
- data/lib/redis/cluster/node_key.rb +31 -0
- data/lib/redis/cluster/node_loader.rb +34 -0
- data/lib/redis/cluster/option.rb +100 -0
- data/lib/redis/cluster/slot.rb +86 -0
- data/lib/redis/cluster/slot_loader.rb +46 -0
- data/lib/redis/cluster.rb +315 -0
- data/lib/redis/commands/bitmaps.rb +63 -0
- data/lib/redis/commands/cluster.rb +45 -0
- data/lib/redis/commands/connection.rb +58 -0
- data/lib/redis/commands/geo.rb +84 -0
- data/lib/redis/commands/hashes.rb +251 -0
- data/lib/redis/commands/hyper_log_log.rb +37 -0
- data/lib/redis/commands/keys.rb +455 -0
- data/lib/redis/commands/lists.rb +290 -0
- data/lib/redis/commands/pubsub.rb +72 -0
- data/lib/redis/commands/scripting.rb +114 -0
- data/lib/redis/commands/server.rb +188 -0
- data/lib/redis/commands/sets.rb +223 -0
- data/lib/redis/commands/sorted_sets.rb +812 -0
- data/lib/redis/commands/streams.rb +382 -0
- data/lib/redis/commands/strings.rb +313 -0
- data/lib/redis/commands/transactions.rb +139 -0
- data/lib/redis/commands.rb +240 -0
- data/lib/redis/connection/command_helper.rb +7 -10
- data/lib/redis/connection/hiredis.rb +5 -3
- data/lib/redis/connection/registry.rb +2 -1
- data/lib/redis/connection/ruby.rb +136 -128
- data/lib/redis/connection/synchrony.rb +24 -9
- data/lib/redis/connection.rb +3 -1
- data/lib/redis/distributed.rb +255 -85
- data/lib/redis/errors.rb +57 -0
- data/lib/redis/hash_ring.rb +30 -73
- data/lib/redis/pipeline.rb +178 -13
- data/lib/redis/subscribe.rb +11 -12
- data/lib/redis/version.rb +3 -1
- data/lib/redis.rb +174 -2661
- metadata +66 -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
|
+
|
2
3
|
require "socket"
|
3
4
|
require "cgi"
|
5
|
+
require "redis/errors"
|
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, :connection, :command_map
|
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
|
@@ -74,8 +87,6 @@ class Redis
|
|
74
87
|
end
|
75
88
|
|
76
89
|
attr_accessor :logger
|
77
|
-
attr_reader :connection
|
78
|
-
attr_reader :command_map
|
79
90
|
|
80
91
|
def initialize(options = {})
|
81
92
|
@options = _parse_options(options)
|
@@ -86,11 +97,14 @@ class Redis
|
|
86
97
|
|
87
98
|
@pending_reads = 0
|
88
99
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
100
|
+
@connector =
|
101
|
+
if !@options[:sentinels].nil?
|
102
|
+
Connector::Sentinel.new(@options)
|
103
|
+
elsif options.include?(:connector) && options[:connector].respond_to?(:new)
|
104
|
+
options.delete(:connector).new(@options)
|
105
|
+
else
|
106
|
+
Connector.new(@options)
|
107
|
+
end
|
94
108
|
end
|
95
109
|
|
96
110
|
def connect
|
@@ -99,7 +113,34 @@ class Redis
|
|
99
113
|
# Don't try to reconnect when the connection is fresh
|
100
114
|
with_reconnect(false) do
|
101
115
|
establish_connection
|
102
|
-
|
116
|
+
if password
|
117
|
+
if username
|
118
|
+
begin
|
119
|
+
call [:auth, username, password]
|
120
|
+
rescue CommandError => err # Likely on Redis < 6
|
121
|
+
case err.message
|
122
|
+
when /ERR wrong number of arguments for 'auth' command/
|
123
|
+
call [:auth, password]
|
124
|
+
when /WRONGPASS invalid username-password pair/
|
125
|
+
begin
|
126
|
+
call [:auth, password]
|
127
|
+
rescue CommandError
|
128
|
+
raise err
|
129
|
+
end
|
130
|
+
::Redis.deprecate!(
|
131
|
+
"[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
|
132
|
+
" the provided password was for the default user. This will start failing in redis-rb 5.0.0."
|
133
|
+
)
|
134
|
+
else
|
135
|
+
raise
|
136
|
+
end
|
137
|
+
end
|
138
|
+
else
|
139
|
+
call [:auth, password]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
call [:readonly] if @options[:readonly]
|
103
144
|
call [:select, db] if db != 0
|
104
145
|
call [:client, :setname, @options[:id]] if @options[:id]
|
105
146
|
@connector.check(self)
|
@@ -109,7 +150,7 @@ class Redis
|
|
109
150
|
end
|
110
151
|
|
111
152
|
def id
|
112
|
-
@options[:id] || "
|
153
|
+
@options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
|
113
154
|
end
|
114
155
|
|
115
156
|
def location
|
@@ -120,7 +161,7 @@ class Redis
|
|
120
161
|
reply = process([command]) { read }
|
121
162
|
raise reply if reply.is_a?(CommandError)
|
122
163
|
|
123
|
-
if block_given?
|
164
|
+
if block_given? && reply != 'QUEUED'
|
124
165
|
yield reply
|
125
166
|
else
|
126
167
|
reply
|
@@ -152,13 +193,16 @@ class Redis
|
|
152
193
|
end
|
153
194
|
|
154
195
|
def call_pipeline(pipeline)
|
196
|
+
return [] if pipeline.futures.empty?
|
197
|
+
|
155
198
|
with_reconnect pipeline.with_reconnect? do
|
156
199
|
begin
|
157
|
-
pipeline.finish(call_pipelined(pipeline
|
200
|
+
pipeline.finish(call_pipelined(pipeline)).tap do
|
158
201
|
self.db = pipeline.db if pipeline.db
|
159
202
|
end
|
160
203
|
rescue ConnectionError => e
|
161
204
|
return nil if pipeline.shutdown?
|
205
|
+
|
162
206
|
# Assume the pipeline was sent in one piece, but execution of
|
163
207
|
# SHUTDOWN caused none of the replies for commands that were executed
|
164
208
|
# prior to it from coming back around.
|
@@ -167,8 +211,8 @@ class Redis
|
|
167
211
|
end
|
168
212
|
end
|
169
213
|
|
170
|
-
def call_pipelined(
|
171
|
-
return [] if
|
214
|
+
def call_pipelined(pipeline)
|
215
|
+
return [] if pipeline.futures.empty?
|
172
216
|
|
173
217
|
# The method #ensure_connected (called from #process) reconnects once on
|
174
218
|
# I/O errors. To make an effort in making sure that commands are not
|
@@ -178,6 +222,8 @@ class Redis
|
|
178
222
|
# already successfully executed commands. To circumvent this, don't retry
|
179
223
|
# after the first reply has been read successfully.
|
180
224
|
|
225
|
+
commands = pipeline.commands
|
226
|
+
|
181
227
|
result = Array.new(commands.size)
|
182
228
|
reconnect = @reconnect
|
183
229
|
|
@@ -185,13 +231,14 @@ class Redis
|
|
185
231
|
exception = nil
|
186
232
|
|
187
233
|
process(commands) do
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
result[i
|
234
|
+
pipeline.timeouts.each_with_index do |timeout, i|
|
235
|
+
reply = if timeout
|
236
|
+
with_socket_timeout(timeout) { read }
|
237
|
+
else
|
238
|
+
read
|
239
|
+
end
|
240
|
+
result[i] = reply
|
241
|
+
@reconnect = false
|
195
242
|
exception = reply if exception.nil? && reply.is_a?(CommandError)
|
196
243
|
end
|
197
244
|
end
|
@@ -204,7 +251,8 @@ class Redis
|
|
204
251
|
result
|
205
252
|
end
|
206
253
|
|
207
|
-
def call_with_timeout(command,
|
254
|
+
def call_with_timeout(command, extra_timeout, &blk)
|
255
|
+
timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout
|
208
256
|
with_socket_timeout(timeout) do
|
209
257
|
call(command, &blk)
|
210
258
|
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
|
@@ -253,7 +302,7 @@ class Redis
|
|
253
302
|
e2 = TimeoutError.new("Connection timed out")
|
254
303
|
e2.set_backtrace(e1.backtrace)
|
255
304
|
raise e2
|
256
|
-
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL => e
|
305
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL, EOFError => e
|
257
306
|
raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
|
258
307
|
end
|
259
308
|
|
@@ -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
|
@@ -385,17 +442,16 @@ class Redis
|
|
385
442
|
defaults = DEFAULTS.dup
|
386
443
|
options = options.dup
|
387
444
|
|
388
|
-
defaults.
|
445
|
+
defaults.each_key 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
|
@@ -403,13 +459,15 @@ class Redis
|
|
403
459
|
|
404
460
|
uri = URI(url)
|
405
461
|
|
406
|
-
|
407
|
-
|
408
|
-
|
462
|
+
case uri.scheme
|
463
|
+
when "unix"
|
464
|
+
defaults[:path] = uri.path
|
465
|
+
when "redis", "rediss"
|
409
466
|
defaults[:scheme] = uri.scheme
|
410
|
-
defaults[:host] = uri.host if uri.host
|
467
|
+
defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
|
411
468
|
defaults[:port] = uri.port if uri.port
|
412
|
-
defaults[:
|
469
|
+
defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
|
470
|
+
defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
|
413
471
|
defaults[:db] = uri.path[1..-1].to_i if uri.path
|
414
472
|
defaults[:role] = :master
|
415
473
|
else
|
@@ -420,7 +478,7 @@ class Redis
|
|
420
478
|
end
|
421
479
|
|
422
480
|
# Use default when option is not specified or nil
|
423
|
-
defaults.
|
481
|
+
defaults.each_key do |key|
|
424
482
|
options[key] = defaults[key] if options[key].nil?
|
425
483
|
end
|
426
484
|
|
@@ -435,7 +493,7 @@ class Redis
|
|
435
493
|
options[:port] = options[:port].to_i
|
436
494
|
end
|
437
495
|
|
438
|
-
if options.
|
496
|
+
if options.key?(:timeout)
|
439
497
|
options[:connect_timeout] ||= options[:timeout]
|
440
498
|
options[:read_timeout] ||= options[:timeout]
|
441
499
|
options[:write_timeout] ||= options[:timeout]
|
@@ -445,12 +503,16 @@ class Redis
|
|
445
503
|
options[:read_timeout] = Float(options[:read_timeout])
|
446
504
|
options[:write_timeout] = Float(options[:write_timeout])
|
447
505
|
|
506
|
+
options[:reconnect_attempts] = options[:reconnect_attempts].to_i
|
507
|
+
options[:reconnect_delay] = options[:reconnect_delay].to_f
|
508
|
+
options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
|
509
|
+
|
448
510
|
options[:db] = options[:db].to_i
|
449
511
|
options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
|
450
512
|
|
451
513
|
case options[:tcp_keepalive]
|
452
514
|
when Hash
|
453
|
-
[
|
515
|
+
%i[time intvl probes].each do |key|
|
454
516
|
unless options[:tcp_keepalive][key].is_a?(Integer)
|
455
517
|
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
456
518
|
end
|
@@ -458,13 +520,13 @@ class Redis
|
|
458
520
|
|
459
521
|
when Integer
|
460
522
|
if options[:tcp_keepalive] >= 60
|
461
|
-
options[:tcp_keepalive] = {:
|
523
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
462
524
|
|
463
525
|
elsif options[:tcp_keepalive] >= 30
|
464
|
-
options[:tcp_keepalive] = {:
|
526
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
465
527
|
|
466
528
|
elsif options[:tcp_keepalive] >= 5
|
467
|
-
options[:tcp_keepalive] = {:
|
529
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
468
530
|
end
|
469
531
|
end
|
470
532
|
|
@@ -476,13 +538,18 @@ class Redis
|
|
476
538
|
def _parse_driver(driver)
|
477
539
|
driver = driver.to_s if driver.is_a?(Symbol)
|
478
540
|
|
479
|
-
if driver.
|
541
|
+
if driver.is_a?(String)
|
480
542
|
begin
|
481
|
-
|
482
|
-
driver = Connection.const_get(driver.capitalize)
|
543
|
+
require_relative "connection/#{driver}"
|
483
544
|
rescue LoadError, NameError
|
484
|
-
|
545
|
+
begin
|
546
|
+
require "redis/connection/#{driver}"
|
547
|
+
rescue LoadError, NameError => error
|
548
|
+
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
549
|
+
end
|
485
550
|
end
|
551
|
+
|
552
|
+
driver = Connection.const_get(driver.capitalize)
|
486
553
|
end
|
487
554
|
|
488
555
|
driver
|
@@ -497,18 +564,16 @@ class Redis
|
|
497
564
|
@options
|
498
565
|
end
|
499
566
|
|
500
|
-
def check(client)
|
501
|
-
end
|
567
|
+
def check(client); end
|
502
568
|
|
503
569
|
class Sentinel < Connector
|
504
570
|
def initialize(options)
|
505
571
|
super(options)
|
506
572
|
|
507
|
-
@options[:password] = DEFAULTS.fetch(:password)
|
508
573
|
@options[:db] = DEFAULTS.fetch(:db)
|
509
574
|
|
510
575
|
@sentinels = @options.delete(:sentinels).dup
|
511
|
-
@role = @options
|
576
|
+
@role = (@options[:role] || "master").to_s
|
512
577
|
@master = @options[:host]
|
513
578
|
end
|
514
579
|
|
@@ -531,13 +596,13 @@ class Redis
|
|
531
596
|
|
532
597
|
def resolve
|
533
598
|
result = case @role
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
599
|
+
when "master"
|
600
|
+
resolve_master
|
601
|
+
when "slave"
|
602
|
+
resolve_slave
|
603
|
+
else
|
604
|
+
raise ArgumentError, "Unknown instance role #{@role}"
|
605
|
+
end
|
541
606
|
|
542
607
|
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
543
608
|
end
|
@@ -545,10 +610,12 @@ class Redis
|
|
545
610
|
def sentinel_detect
|
546
611
|
@sentinels.each do |sentinel|
|
547
612
|
client = Client.new(@options.merge({
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
613
|
+
host: sentinel[:host] || sentinel["host"],
|
614
|
+
port: sentinel[:port] || sentinel["port"],
|
615
|
+
username: sentinel[:username] || sentinel["username"],
|
616
|
+
password: sentinel[:password] || sentinel["password"],
|
617
|
+
reconnect_attempts: 0
|
618
|
+
}))
|
552
619
|
|
553
620
|
begin
|
554
621
|
if result = yield(client)
|
@@ -570,7 +637,7 @@ class Redis
|
|
570
637
|
def resolve_master
|
571
638
|
sentinel_detect do |client|
|
572
639
|
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
573
|
-
{:
|
640
|
+
{ host: reply[0], port: reply[1] }
|
574
641
|
end
|
575
642
|
end
|
576
643
|
end
|
@@ -578,9 +645,19 @@ class Redis
|
|
578
645
|
def resolve_slave
|
579
646
|
sentinel_detect do |client|
|
580
647
|
if reply = client.call(["sentinel", "slaves", @master])
|
581
|
-
|
582
|
-
|
583
|
-
{
|
648
|
+
slaves = reply.map { |s| s.each_slice(2).to_h }
|
649
|
+
slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
|
650
|
+
slaves.reject! { |s| s.fetch('flags').include?('s_down') }
|
651
|
+
|
652
|
+
if slaves.empty?
|
653
|
+
raise CannotConnectError, 'No slaves available.'
|
654
|
+
else
|
655
|
+
slave = slaves.sample
|
656
|
+
{
|
657
|
+
host: slave.fetch('ip'),
|
658
|
+
port: slave.fetch('port')
|
659
|
+
}
|
660
|
+
end
|
584
661
|
end
|
585
662
|
end
|
586
663
|
end
|
@@ -0,0 +1,79 @@
|
|
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.transform_values do |detail|
|
35
|
+
{
|
36
|
+
first_key_position: detail[:first],
|
37
|
+
write: detail[:flags].include?('write'),
|
38
|
+
readonly: detail[:flags].include?('readonly')
|
39
|
+
}
|
40
|
+
end
|
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 'xread', 'xreadgroup'
|
57
|
+
determine_optional_key_position(command, 'streams')
|
58
|
+
else
|
59
|
+
dig_details(command, :first_key_position).to_i
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def determine_optional_key_position(command, option_name)
|
64
|
+
idx = command.map(&:to_s).map(&:downcase).index(option_name)
|
65
|
+
idx.nil? ? 0 : idx + 1
|
66
|
+
end
|
67
|
+
|
68
|
+
# @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
|
69
|
+
def extract_hash_tag(key)
|
70
|
+
s = key.index('{')
|
71
|
+
e = key.index('}', s.to_i + 1)
|
72
|
+
|
73
|
+
return '' if s.nil? || e.nil?
|
74
|
+
|
75
|
+
key[s + 1..e - 1]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redis/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
|
+
errors = nodes.map do |node|
|
14
|
+
begin
|
15
|
+
return fetch_command_details(node)
|
16
|
+
rescue CannotConnectError, ConnectionError, CommandError => error
|
17
|
+
error
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
raise InitialSetupError, errors
|
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
|