redis 4.0.1 → 4.8.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 +220 -0
- data/README.md +152 -28
- data/lib/redis/client.rb +171 -107
- 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 +5 -2
- data/lib/redis/connection/hiredis.rb +7 -5
- data/lib/redis/connection/registry.rb +2 -1
- data/lib/redis/connection/ruby.rb +139 -111
- data/lib/redis/connection/synchrony.rb +17 -10
- data/lib/redis/connection.rb +3 -1
- data/lib/redis/distributed.rb +244 -87
- data/lib/redis/errors.rb +57 -0
- data/lib/redis/hash_ring.rb +15 -14
- data/lib/redis/pipeline.rb +181 -10
- data/lib/redis/subscribe.rb +11 -12
- data/lib/redis/version.rb +3 -1
- data/lib/redis.rb +180 -2716
- metadata +45 -195
- data/.gitignore +0 -16
- data/.travis/Gemfile +0 -13
- data/.travis.yml +0 -73
- 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/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 -42
- data/redis.gemspec +0 -42
- data/test/bitpos_test.rb +0 -63
- data/test/blocking_commands_test.rb +0 -40
- data/test/client_test.rb +0 -59
- data/test/command_map_test.rb +0 -28
- data/test/commands_on_hashes_test.rb +0 -19
- data/test/commands_on_hyper_log_log_test.rb +0 -19
- data/test/commands_on_lists_test.rb +0 -18
- data/test/commands_on_sets_test.rb +0 -75
- data/test/commands_on_sorted_sets_test.rb +0 -150
- data/test/commands_on_strings_test.rb +0 -99
- data/test/commands_on_value_types_test.rb +0 -171
- 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 -44
- data/test/distributed_commands_on_hashes_test.rb +0 -8
- data/test/distributed_commands_on_hyper_log_log_test.rb +0 -31
- data/test/distributed_commands_on_lists_test.rb +0 -20
- data/test/distributed_commands_on_sets_test.rb +0 -106
- data/test/distributed_commands_on_sorted_sets_test.rb +0 -16
- data/test/distributed_commands_on_strings_test.rb +0 -69
- data/test/distributed_commands_on_value_types_test.rb +0 -93
- 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 -201
- data/test/helper_test.rb +0 -22
- data/test/internals_test.rb +0 -389
- 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 -246
- data/test/lint/value_types.rb +0 -130
- 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,27 +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
|
-
|
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
|
25
36
|
|
26
37
|
def scheme
|
27
38
|
@options[:scheme]
|
@@ -51,6 +62,10 @@ class Redis
|
|
51
62
|
@options[:read_timeout]
|
52
63
|
end
|
53
64
|
|
65
|
+
def username
|
66
|
+
@options[:username]
|
67
|
+
end
|
68
|
+
|
54
69
|
def password
|
55
70
|
@options[:password]
|
56
71
|
end
|
@@ -72,8 +87,6 @@ class Redis
|
|
72
87
|
end
|
73
88
|
|
74
89
|
attr_accessor :logger
|
75
|
-
attr_reader :connection
|
76
|
-
attr_reader :command_map
|
77
90
|
|
78
91
|
def initialize(options = {})
|
79
92
|
@options = _parse_options(options)
|
@@ -84,11 +97,14 @@ class Redis
|
|
84
97
|
|
85
98
|
@pending_reads = 0
|
86
99
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
92
108
|
end
|
93
109
|
|
94
110
|
def connect
|
@@ -97,7 +113,34 @@ class Redis
|
|
97
113
|
# Don't try to reconnect when the connection is fresh
|
98
114
|
with_reconnect(false) do
|
99
115
|
establish_connection
|
100
|
-
|
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]
|
101
144
|
call [:select, db] if db != 0
|
102
145
|
call [:client, :setname, @options[:id]] if @options[:id]
|
103
146
|
@connector.check(self)
|
@@ -107,7 +150,7 @@ class Redis
|
|
107
150
|
end
|
108
151
|
|
109
152
|
def id
|
110
|
-
@options[:id] || "
|
153
|
+
@options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
|
111
154
|
end
|
112
155
|
|
113
156
|
def location
|
@@ -118,7 +161,7 @@ class Redis
|
|
118
161
|
reply = process([command]) { read }
|
119
162
|
raise reply if reply.is_a?(CommandError)
|
120
163
|
|
121
|
-
if block_given?
|
164
|
+
if block_given? && reply != 'QUEUED'
|
122
165
|
yield reply
|
123
166
|
else
|
124
167
|
reply
|
@@ -150,13 +193,16 @@ class Redis
|
|
150
193
|
end
|
151
194
|
|
152
195
|
def call_pipeline(pipeline)
|
196
|
+
return [] if pipeline.futures.empty?
|
197
|
+
|
153
198
|
with_reconnect pipeline.with_reconnect? do
|
154
199
|
begin
|
155
|
-
pipeline.finish(call_pipelined(pipeline
|
200
|
+
pipeline.finish(call_pipelined(pipeline)).tap do
|
156
201
|
self.db = pipeline.db if pipeline.db
|
157
202
|
end
|
158
203
|
rescue ConnectionError => e
|
159
204
|
return nil if pipeline.shutdown?
|
205
|
+
|
160
206
|
# Assume the pipeline was sent in one piece, but execution of
|
161
207
|
# SHUTDOWN caused none of the replies for commands that were executed
|
162
208
|
# prior to it from coming back around.
|
@@ -165,8 +211,8 @@ class Redis
|
|
165
211
|
end
|
166
212
|
end
|
167
213
|
|
168
|
-
def call_pipelined(
|
169
|
-
return [] if
|
214
|
+
def call_pipelined(pipeline)
|
215
|
+
return [] if pipeline.futures.empty?
|
170
216
|
|
171
217
|
# The method #ensure_connected (called from #process) reconnects once on
|
172
218
|
# I/O errors. To make an effort in making sure that commands are not
|
@@ -176,6 +222,8 @@ class Redis
|
|
176
222
|
# already successfully executed commands. To circumvent this, don't retry
|
177
223
|
# after the first reply has been read successfully.
|
178
224
|
|
225
|
+
commands = pipeline.commands
|
226
|
+
|
179
227
|
result = Array.new(commands.size)
|
180
228
|
reconnect = @reconnect
|
181
229
|
|
@@ -183,13 +231,14 @@ class Redis
|
|
183
231
|
exception = nil
|
184
232
|
|
185
233
|
process(commands) do
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
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
|
193
242
|
exception = reply if exception.nil? && reply.is_a?(CommandError)
|
194
243
|
end
|
195
244
|
end
|
@@ -202,7 +251,8 @@ class Redis
|
|
202
251
|
result
|
203
252
|
end
|
204
253
|
|
205
|
-
def call_with_timeout(command,
|
254
|
+
def call_with_timeout(command, extra_timeout, &blk)
|
255
|
+
timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout
|
206
256
|
with_socket_timeout(timeout) do
|
207
257
|
call(command, &blk)
|
208
258
|
end
|
@@ -232,12 +282,13 @@ class Redis
|
|
232
282
|
end
|
233
283
|
|
234
284
|
def connected?
|
235
|
-
!!
|
285
|
+
!!(connection && connection.connected?)
|
236
286
|
end
|
237
287
|
|
238
288
|
def disconnect
|
239
289
|
connection.disconnect if connected?
|
240
290
|
end
|
291
|
+
alias close disconnect
|
241
292
|
|
242
293
|
def reconnect
|
243
294
|
disconnect
|
@@ -251,7 +302,7 @@ class Redis
|
|
251
302
|
e2 = TimeoutError.new("Connection timed out")
|
252
303
|
e2.set_backtrace(e1.backtrace)
|
253
304
|
raise e2
|
254
|
-
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
|
255
306
|
raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
|
256
307
|
end
|
257
308
|
|
@@ -272,12 +323,15 @@ class Redis
|
|
272
323
|
|
273
324
|
def with_socket_timeout(timeout)
|
274
325
|
connect unless connected?
|
326
|
+
original = @options[:read_timeout]
|
275
327
|
|
276
328
|
begin
|
277
329
|
connection.timeout = timeout
|
330
|
+
@options[:read_timeout] = timeout # for reconnection
|
278
331
|
yield
|
279
332
|
ensure
|
280
333
|
connection.timeout = self.timeout if connected?
|
334
|
+
@options[:read_timeout] = original
|
281
335
|
end
|
282
336
|
end
|
283
337
|
|
@@ -285,30 +339,27 @@ class Redis
|
|
285
339
|
with_socket_timeout(0, &blk)
|
286
340
|
end
|
287
341
|
|
288
|
-
def with_reconnect(val=true)
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
@reconnect = original
|
294
|
-
end
|
342
|
+
def with_reconnect(val = true)
|
343
|
+
original, @reconnect = @reconnect, val
|
344
|
+
yield
|
345
|
+
ensure
|
346
|
+
@reconnect = original
|
295
347
|
end
|
296
348
|
|
297
349
|
def without_reconnect(&blk)
|
298
350
|
with_reconnect(false, &blk)
|
299
351
|
end
|
300
352
|
|
301
|
-
|
353
|
+
protected
|
302
354
|
|
303
355
|
def logging(commands)
|
304
|
-
return yield unless @logger
|
356
|
+
return yield unless @logger&.debug?
|
305
357
|
|
306
358
|
begin
|
307
359
|
commands.each do |name, *args|
|
308
360
|
logged_args = args.map do |a|
|
309
|
-
|
310
|
-
|
311
|
-
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
|
312
363
|
else
|
313
364
|
# handle poorly-behaved descendants of BasicObject
|
314
365
|
klass = a.instance_exec { (class << self; self end).superclass }
|
@@ -334,40 +385,38 @@ class Redis
|
|
334
385
|
@connection = @options[:driver].connect(@options)
|
335
386
|
@pending_reads = 0
|
336
387
|
rescue TimeoutError,
|
388
|
+
SocketError,
|
389
|
+
Errno::EADDRNOTAVAIL,
|
337
390
|
Errno::ECONNREFUSED,
|
338
391
|
Errno::EHOSTDOWN,
|
339
392
|
Errno::EHOSTUNREACH,
|
340
393
|
Errno::ENETUNREACH,
|
341
394
|
Errno::ENOENT,
|
342
|
-
Errno::ETIMEDOUT
|
395
|
+
Errno::ETIMEDOUT,
|
396
|
+
Errno::EINVAL => error
|
343
397
|
|
344
|
-
raise CannotConnectError, "Error connecting to Redis on #{location} (#{
|
398
|
+
raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
|
345
399
|
end
|
346
400
|
|
347
401
|
def ensure_connected
|
348
|
-
disconnect if @pending_reads > 0
|
402
|
+
disconnect if @pending_reads > 0 || (@pid != Process.pid && !inherit_socket?)
|
349
403
|
|
350
404
|
attempts = 0
|
351
405
|
|
352
406
|
begin
|
353
407
|
attempts += 1
|
354
408
|
|
355
|
-
|
356
|
-
unless inherit_socket? || Process.pid == @pid
|
357
|
-
raise InheritedError,
|
358
|
-
"Tried to use a connection from a child process without reconnecting. " +
|
359
|
-
"You need to reconnect to Redis after forking " +
|
360
|
-
"or set :inherit_socket to true."
|
361
|
-
end
|
362
|
-
else
|
363
|
-
connect
|
364
|
-
end
|
409
|
+
connect unless connected?
|
365
410
|
|
366
411
|
yield
|
367
412
|
rescue BaseConnectionError
|
368
413
|
disconnect
|
369
414
|
|
370
415
|
if attempts <= @options[:reconnect_attempts] && @reconnect
|
416
|
+
sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
|
417
|
+
@options[:reconnect_delay_max]].min
|
418
|
+
|
419
|
+
Kernel.sleep(sleep_t)
|
371
420
|
retry
|
372
421
|
else
|
373
422
|
raise
|
@@ -384,17 +433,16 @@ class Redis
|
|
384
433
|
defaults = DEFAULTS.dup
|
385
434
|
options = options.dup
|
386
435
|
|
387
|
-
defaults.
|
436
|
+
defaults.each_key do |key|
|
388
437
|
# Fill in defaults if needed
|
389
|
-
if defaults[key].respond_to?(:call)
|
390
|
-
defaults[key] = defaults[key].call
|
391
|
-
end
|
438
|
+
defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
|
392
439
|
|
393
440
|
# Symbolize only keys that are needed
|
394
|
-
options[key] = options[key.to_s] if options.
|
441
|
+
options[key] = options[key.to_s] if options.key?(key.to_s)
|
395
442
|
end
|
396
443
|
|
397
|
-
url = options[:url]
|
444
|
+
url = options[:url]
|
445
|
+
url = defaults[:url] if url.nil?
|
398
446
|
|
399
447
|
# Override defaults from URL if given
|
400
448
|
if url
|
@@ -402,13 +450,15 @@ class Redis
|
|
402
450
|
|
403
451
|
uri = URI(url)
|
404
452
|
|
405
|
-
|
406
|
-
|
407
|
-
|
453
|
+
case uri.scheme
|
454
|
+
when "unix"
|
455
|
+
defaults[:path] = uri.path
|
456
|
+
when "redis", "rediss"
|
408
457
|
defaults[:scheme] = uri.scheme
|
409
|
-
defaults[:host] = uri.host if uri.host
|
458
|
+
defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
|
410
459
|
defaults[:port] = uri.port if uri.port
|
411
|
-
defaults[:
|
460
|
+
defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
|
461
|
+
defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
|
412
462
|
defaults[:db] = uri.path[1..-1].to_i if uri.path
|
413
463
|
defaults[:role] = :master
|
414
464
|
else
|
@@ -419,7 +469,7 @@ class Redis
|
|
419
469
|
end
|
420
470
|
|
421
471
|
# Use default when option is not specified or nil
|
422
|
-
defaults.
|
472
|
+
defaults.each_key do |key|
|
423
473
|
options[key] = defaults[key] if options[key].nil?
|
424
474
|
end
|
425
475
|
|
@@ -434,7 +484,7 @@ class Redis
|
|
434
484
|
options[:port] = options[:port].to_i
|
435
485
|
end
|
436
486
|
|
437
|
-
if options.
|
487
|
+
if options.key?(:timeout)
|
438
488
|
options[:connect_timeout] ||= options[:timeout]
|
439
489
|
options[:read_timeout] ||= options[:timeout]
|
440
490
|
options[:write_timeout] ||= options[:timeout]
|
@@ -444,12 +494,16 @@ class Redis
|
|
444
494
|
options[:read_timeout] = Float(options[:read_timeout])
|
445
495
|
options[:write_timeout] = Float(options[:write_timeout])
|
446
496
|
|
497
|
+
options[:reconnect_attempts] = options[:reconnect_attempts].to_i
|
498
|
+
options[:reconnect_delay] = options[:reconnect_delay].to_f
|
499
|
+
options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
|
500
|
+
|
447
501
|
options[:db] = options[:db].to_i
|
448
502
|
options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
|
449
503
|
|
450
504
|
case options[:tcp_keepalive]
|
451
505
|
when Hash
|
452
|
-
[
|
506
|
+
%i[time intvl probes].each do |key|
|
453
507
|
unless options[:tcp_keepalive][key].is_a?(Integer)
|
454
508
|
raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
|
455
509
|
end
|
@@ -457,13 +511,13 @@ class Redis
|
|
457
511
|
|
458
512
|
when Integer
|
459
513
|
if options[:tcp_keepalive] >= 60
|
460
|
-
options[:tcp_keepalive] = {:
|
514
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
|
461
515
|
|
462
516
|
elsif options[:tcp_keepalive] >= 30
|
463
|
-
options[:tcp_keepalive] = {:
|
517
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
|
464
518
|
|
465
519
|
elsif options[:tcp_keepalive] >= 5
|
466
|
-
options[:tcp_keepalive] = {:
|
520
|
+
options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
|
467
521
|
end
|
468
522
|
end
|
469
523
|
|
@@ -475,14 +529,14 @@ class Redis
|
|
475
529
|
def _parse_driver(driver)
|
476
530
|
driver = driver.to_s if driver.is_a?(Symbol)
|
477
531
|
|
478
|
-
if driver.
|
532
|
+
if driver.is_a?(String)
|
479
533
|
begin
|
480
534
|
require_relative "connection/#{driver}"
|
481
|
-
rescue LoadError, NameError
|
535
|
+
rescue LoadError, NameError
|
482
536
|
begin
|
483
|
-
require "connection/#{driver}"
|
484
|
-
rescue LoadError, NameError =>
|
485
|
-
raise
|
537
|
+
require "redis/connection/#{driver}"
|
538
|
+
rescue LoadError, NameError => error
|
539
|
+
raise "Cannot load driver #{driver.inspect}: #{error.message}"
|
486
540
|
end
|
487
541
|
end
|
488
542
|
|
@@ -501,18 +555,16 @@ class Redis
|
|
501
555
|
@options
|
502
556
|
end
|
503
557
|
|
504
|
-
def check(client)
|
505
|
-
end
|
558
|
+
def check(client); end
|
506
559
|
|
507
560
|
class Sentinel < Connector
|
508
561
|
def initialize(options)
|
509
562
|
super(options)
|
510
563
|
|
511
|
-
@options[:password] = DEFAULTS.fetch(:password)
|
512
564
|
@options[:db] = DEFAULTS.fetch(:db)
|
513
565
|
|
514
566
|
@sentinels = @options.delete(:sentinels).dup
|
515
|
-
@role = @options
|
567
|
+
@role = (@options[:role] || "master").to_s
|
516
568
|
@master = @options[:host]
|
517
569
|
end
|
518
570
|
|
@@ -535,13 +587,13 @@ class Redis
|
|
535
587
|
|
536
588
|
def resolve
|
537
589
|
result = case @role
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
590
|
+
when "master"
|
591
|
+
resolve_master
|
592
|
+
when "slave"
|
593
|
+
resolve_slave
|
594
|
+
else
|
595
|
+
raise ArgumentError, "Unknown instance role #{@role}"
|
596
|
+
end
|
545
597
|
|
546
598
|
result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
|
547
599
|
end
|
@@ -549,10 +601,12 @@ class Redis
|
|
549
601
|
def sentinel_detect
|
550
602
|
@sentinels.each do |sentinel|
|
551
603
|
client = Client.new(@options.merge({
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
604
|
+
host: sentinel[:host] || sentinel["host"],
|
605
|
+
port: sentinel[:port] || sentinel["port"],
|
606
|
+
username: sentinel[:username] || sentinel["username"],
|
607
|
+
password: sentinel[:password] || sentinel["password"],
|
608
|
+
reconnect_attempts: 0
|
609
|
+
}))
|
556
610
|
|
557
611
|
begin
|
558
612
|
if result = yield(client)
|
@@ -574,7 +628,7 @@ class Redis
|
|
574
628
|
def resolve_master
|
575
629
|
sentinel_detect do |client|
|
576
630
|
if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
|
577
|
-
{:
|
631
|
+
{ host: reply[0], port: reply[1] }
|
578
632
|
end
|
579
633
|
end
|
580
634
|
end
|
@@ -582,9 +636,19 @@ class Redis
|
|
582
636
|
def resolve_slave
|
583
637
|
sentinel_detect do |client|
|
584
638
|
if reply = client.call(["sentinel", "slaves", @master])
|
585
|
-
|
586
|
-
|
587
|
-
{
|
639
|
+
slaves = reply.map { |s| s.each_slice(2).to_h }
|
640
|
+
slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
|
641
|
+
slaves.reject! { |s| s.fetch('flags').include?('s_down') }
|
642
|
+
|
643
|
+
if slaves.empty?
|
644
|
+
raise CannotConnectError, 'No slaves available.'
|
645
|
+
else
|
646
|
+
slave = slaves.sample
|
647
|
+
{
|
648
|
+
host: slave.fetch('ip'),
|
649
|
+
port: slave.fetch('port')
|
650
|
+
}
|
651
|
+
end
|
588
652
|
end
|
589
653
|
end
|
590
654
|
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
|