redis 4.1.4 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,6 +18,7 @@ class Redis
18
18
  @node_opts = build_node_options(node_addrs)
19
19
  @replica = options.delete(:replica) == true
20
20
  add_common_node_option_if_needed(options, @node_opts, :scheme)
21
+ add_common_node_option_if_needed(options, @node_opts, :username)
21
22
  add_common_node_option_if_needed(options, @node_opts, :password)
22
23
  @options = options
23
24
  end
@@ -43,6 +44,7 @@ class Redis
43
44
 
44
45
  def build_node_options(addrs)
45
46
  raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
47
+
46
48
  addrs.map { |addr| parse_node_addr(addr) }
47
49
  end
48
50
 
@@ -62,21 +64,25 @@ class Redis
62
64
  raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
63
65
 
64
66
  db = uri.path.split('/')[1]&.to_i
65
- { scheme: uri.scheme, password: uri.password, host: uri.host, port: uri.port, db: db }.reject { |_, v| v.nil? }
67
+
68
+ { scheme: uri.scheme, username: uri.user, password: uri.password, host: uri.host, port: uri.port, db: db }
69
+ .reject { |_, v| v.nil? || v == '' }
66
70
  rescue URI::InvalidURIError => err
67
71
  raise InvalidClientOptionError, err.message
68
72
  end
69
73
 
70
74
  def parse_node_option(addr)
71
75
  addr = addr.map { |k, v| [k.to_sym, v] }.to_h
72
- raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys' if addr.values_at(:host, :port).any?(&:nil?)
76
+ if addr.values_at(:host, :port).any?(&:nil?)
77
+ raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
78
+ end
73
79
 
74
80
  addr
75
81
  end
76
82
 
77
83
  # Redis cluster node returns only host and port information.
78
84
  # So we should complement additional information such as:
79
- # scheme, password and so on.
85
+ # scheme, username, password and so on.
80
86
  def add_common_node_option_if_needed(options, node_opts, key)
81
87
  return options if options[key].nil? && node_opts.first[key].nil?
82
88
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
-
5
3
  class Redis
6
4
  class Cluster
7
5
  # Keep slot and node key map for Redis Cluster Client
@@ -28,11 +26,20 @@ class Redis
28
26
  return nil unless exists?(slot)
29
27
  return find_node_key_of_master(slot) if replica_disabled?
30
28
 
31
- @map[slot][:slaves].to_a.sample
29
+ @map[slot][:slaves].sample
32
30
  end
33
31
 
34
32
  def put(slot, node_key)
35
- assign_node_key(@map, slot, node_key)
33
+ # Since we're sharing a hash for build_slot_node_key_map, duplicate it
34
+ # if it already exists instead of preserving as-is.
35
+ @map[slot] = @map[slot] ? @map[slot].dup : { master: nil, slaves: [] }
36
+
37
+ if master?(node_key)
38
+ @map[slot][:master] = node_key
39
+ elsif !@map[slot][:slaves].include?(node_key)
40
+ @map[slot][:slaves] << node_key
41
+ end
42
+
36
43
  nil
37
44
  end
38
45
 
@@ -52,20 +59,27 @@ class Redis
52
59
 
53
60
  # available_slots is mapping of node_key to list of slot ranges
54
61
  def build_slot_node_key_map(available_slots)
55
- available_slots.each_with_object({}) do |(node_key, slots_arr), acc|
56
- slots_arr.each do |slots|
57
- slots.each { |slot| assign_node_key(acc, slot, node_key) }
62
+ by_ranges = {}
63
+ available_slots.each do |node_key, slots_arr|
64
+ by_ranges[slots_arr] ||= { master: nil, slaves: [] }
65
+
66
+ if master?(node_key)
67
+ by_ranges[slots_arr][:master] = node_key
68
+ elsif !by_ranges[slots_arr][:slaves].include?(node_key)
69
+ by_ranges[slots_arr][:slaves] << node_key
58
70
  end
59
71
  end
60
- end
61
72
 
62
- def assign_node_key(mappings, slot, node_key)
63
- mappings[slot] ||= { master: nil, slaves: ::Set.new }
64
- if master?(node_key)
65
- mappings[slot][:master] = node_key
66
- else
67
- mappings[slot][:slaves].add(node_key)
73
+ by_slot = {}
74
+ by_ranges.each do |slots_arr, nodes|
75
+ slots_arr.each do |slots|
76
+ slots.each do |slot|
77
+ by_slot[slot] = nodes
78
+ end
79
+ end
68
80
  end
81
+
82
+ by_slot
69
83
  end
70
84
  end
71
85
  end
@@ -25,9 +25,8 @@ class Redis
25
25
  def fetch_slot_info(node)
26
26
  hash_with_default_arr = Hash.new { |h, k| h[k] = [] }
27
27
  node.call(%i[cluster slots])
28
- .flat_map { |arr| parse_slot_info(arr, default_ip: node.host) }
29
- .each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] }
30
-
28
+ .flat_map { |arr| parse_slot_info(arr, default_ip: node.host) }
29
+ .each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] }
31
30
  rescue CannotConnectError, ConnectionError, CommandError
32
31
  {} # can retry on another node
33
32
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative "connection/registry"
3
4
 
4
5
  # If a connection driver was required before this file, the array
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  class Redis
3
4
  module Connection
4
5
  module CommandHelper
5
-
6
6
  COMMAND_DELIMITER = "\r\n"
7
7
 
8
8
  def build_command(args)
@@ -29,7 +29,7 @@ class Redis
29
29
  command.join(COMMAND_DELIMITER)
30
30
  end
31
31
 
32
- protected
32
+ protected
33
33
 
34
34
  def encode(string)
35
35
  string.force_encoding(Encoding.default_external)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative "registry"
3
4
  require_relative "../errors"
4
5
  require "hiredis/connection"
@@ -7,7 +8,6 @@ require "timeout"
7
8
  class Redis
8
9
  module Connection
9
10
  class Hiredis
10
-
11
11
  def self.connect(config)
12
12
  connection = ::Hiredis::Connection.new
13
13
  connect_timeout = (config.fetch(:connect_timeout, 0) * 1_000_000).to_i
@@ -32,7 +32,7 @@ class Redis
32
32
  end
33
33
 
34
34
  def connected?
35
- @connection && @connection.connected?
35
+ @connection&.connected?
36
36
  end
37
37
 
38
38
  def timeout=(timeout)
@@ -58,7 +58,7 @@ class Redis
58
58
  rescue Errno::EAGAIN
59
59
  raise TimeoutError
60
60
  rescue RuntimeError => err
61
- raise ProtocolError.new(err.message)
61
+ raise ProtocolError, err.message
62
62
  end
63
63
  end
64
64
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  class Redis
3
4
  module Connection
4
-
5
5
  # Store a list of loaded connection drivers in the Connection module.
6
6
  # Redis::Client uses the last required driver by default, and will be aware
7
7
  # of the loaded connection drivers if the user chooses to override the
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative "registry"
3
4
  require_relative "command_helper"
4
5
  require_relative "../errors"
@@ -14,8 +15,7 @@ end
14
15
  class Redis
15
16
  module Connection
16
17
  module SocketMixin
17
-
18
- CRLF = "\r\n".freeze
18
+ CRLF = "\r\n"
19
19
 
20
20
  def initialize(*args)
21
21
  super(*args)
@@ -25,97 +25,74 @@ class Redis
25
25
  end
26
26
 
27
27
  def timeout=(timeout)
28
- if timeout && timeout > 0
29
- @timeout = timeout
30
- else
31
- @timeout = nil
32
- end
28
+ @timeout = (timeout if timeout && timeout > 0)
33
29
  end
34
30
 
35
31
  def write_timeout=(timeout)
36
- if timeout && timeout > 0
37
- @write_timeout = timeout
38
- else
39
- @write_timeout = nil
40
- end
32
+ @write_timeout = (timeout if timeout && timeout > 0)
41
33
  end
42
34
 
43
35
  def read(nbytes)
44
36
  result = @buffer.slice!(0, nbytes)
45
37
 
46
- while result.bytesize < nbytes
47
- result << _read_from_socket(nbytes - result.bytesize)
48
- end
38
+ result << _read_from_socket(nbytes - result.bytesize) while result.bytesize < nbytes
49
39
 
50
40
  result
51
41
  end
52
42
 
53
43
  def gets
54
- crlf = nil
55
-
56
- while (crlf = @buffer.index(CRLF)) == nil
57
- @buffer << _read_from_socket(16384)
44
+ while (crlf = @buffer.index(CRLF)).nil?
45
+ @buffer << _read_from_socket(16_384)
58
46
  end
59
47
 
60
48
  @buffer.slice!(0, crlf + CRLF.bytesize)
61
49
  end
62
50
 
63
51
  def _read_from_socket(nbytes)
64
-
65
- begin
66
- read_nonblock(nbytes)
67
-
68
- rescue IO::WaitReadable
69
- if IO.select([self], nil, nil, @timeout)
70
- retry
71
- else
72
- raise Redis::TimeoutError
73
- end
74
- rescue IO::WaitWritable
75
- if IO.select(nil, [self], nil, @timeout)
76
- retry
77
- else
78
- raise Redis::TimeoutError
79
- end
80
- end
81
-
82
- rescue EOFError
83
- raise Errno::ECONNRESET
84
- end
85
-
86
- def _write_to_socket(data)
87
- begin
88
- write_nonblock(data)
89
-
90
- rescue IO::WaitWritable
91
- if IO.select(nil, [self], nil, @write_timeout)
92
- retry
93
- else
94
- raise Redis::TimeoutError
95
- end
96
- rescue IO::WaitReadable
97
- if IO.select([self], nil, nil, @write_timeout)
98
- retry
99
- else
100
- raise Redis::TimeoutError
52
+ loop do
53
+ case chunk = read_nonblock(nbytes, exception: false)
54
+ when :wait_readable
55
+ unless wait_readable(@timeout)
56
+ raise Redis::TimeoutError
57
+ end
58
+ when :wait_writable
59
+ unless wait_writable(@timeout)
60
+ raise Redis::TimeoutError
61
+ end
62
+ when nil
63
+ raise Errno::ECONNRESET
64
+ when String
65
+ return chunk
101
66
  end
102
67
  end
103
-
104
- rescue EOFError
105
- raise Errno::ECONNRESET
106
68
  end
107
69
 
108
- def write(data)
109
- return super(data) unless @write_timeout
70
+ def write(buffer)
71
+ return super(buffer) unless @write_timeout
110
72
 
111
- length = data.bytesize
112
- total_count = 0
73
+ bytes_to_write = buffer.bytesize
74
+ total_bytes_written = 0
113
75
  loop do
114
- count = _write_to_socket(data)
76
+ case bytes_written = write_nonblock(buffer, exception: false)
77
+ when :wait_readable
78
+ unless wait_readable(@write_timeout)
79
+ raise Redis::TimeoutError
80
+ end
81
+ when :wait_writable
82
+ unless wait_writable(@write_timeout)
83
+ raise Redis::TimeoutError
84
+ end
85
+ when nil
86
+ raise Errno::ECONNRESET
87
+ when Integer
88
+ total_bytes_written += bytes_written
115
89
 
116
- total_count += count
117
- return total_count if total_count >= length
118
- data = data.byteslice(count..-1)
90
+ if total_bytes_written >= bytes_to_write
91
+ return total_bytes_written
92
+ end
93
+
94
+ buffer = buffer.byteslice(bytes_written..-1)
95
+ end
119
96
  end
120
97
  end
121
98
  end
@@ -125,7 +102,6 @@ class Redis
125
102
  require "timeout"
126
103
 
127
104
  class TCPSocket < ::TCPSocket
128
-
129
105
  include SocketMixin
130
106
 
131
107
  def self.connect(host, port, timeout)
@@ -141,7 +117,6 @@ class Redis
141
117
  if defined?(::UNIXSocket)
142
118
 
143
119
  class UNIXSocket < ::UNIXSocket
144
-
145
120
  include SocketMixin
146
121
 
147
122
  def self.connect(path, timeout)
@@ -153,13 +128,12 @@ class Redis
153
128
  raise TimeoutError
154
129
  end
155
130
 
156
- # JRuby raises Errno::EAGAIN on #read_nonblock even when IO.select
131
+ # JRuby raises Errno::EAGAIN on #read_nonblock even when it
157
132
  # says it is readable (1.6.6, in both 1.8 and 1.9 mode).
158
133
  # Use the blocking #readpartial method instead.
159
134
 
160
135
  def _read_from_socket(nbytes)
161
136
  readpartial(nbytes)
162
-
163
137
  rescue EOFError
164
138
  raise Errno::ECONNRESET
165
139
  end
@@ -170,19 +144,16 @@ class Redis
170
144
  else
171
145
 
172
146
  class TCPSocket < ::Socket
173
-
174
147
  include SocketMixin
175
148
 
176
- def self.connect_addrinfo(ai, port, timeout)
177
- sock = new(::Socket.const_get(ai[0]), Socket::SOCK_STREAM, 0)
178
- sockaddr = ::Socket.pack_sockaddr_in(port, ai[3])
149
+ def self.connect_addrinfo(addrinfo, port, timeout)
150
+ sock = new(::Socket.const_get(addrinfo[0]), Socket::SOCK_STREAM, 0)
151
+ sockaddr = ::Socket.pack_sockaddr_in(port, addrinfo[3])
179
152
 
180
153
  begin
181
154
  sock.connect_nonblock(sockaddr)
182
155
  rescue Errno::EINPROGRESS
183
- if IO.select(nil, [sock], nil, timeout) == nil
184
- raise TimeoutError
185
- end
156
+ raise TimeoutError unless sock.wait_writable(timeout)
186
157
 
187
158
  begin
188
159
  sock.connect_nonblock(sockaddr)
@@ -221,14 +192,13 @@ class Redis
221
192
  return connect_addrinfo(ai, port, timeout)
222
193
  rescue SystemCallError
223
194
  # Raise if this was our last attempt.
224
- raise if addrinfo.length == i+1
195
+ raise if addrinfo.length == i + 1
225
196
  end
226
197
  end
227
198
  end
228
199
  end
229
200
 
230
201
  class UNIXSocket < ::Socket
231
-
232
202
  include SocketMixin
233
203
 
234
204
  def self.connect(path, timeout)
@@ -238,9 +208,7 @@ class Redis
238
208
  begin
239
209
  sock.connect_nonblock(sockaddr)
240
210
  rescue Errno::EINPROGRESS
241
- if IO.select(nil, [sock], nil, timeout) == nil
242
- raise TimeoutError
243
- end
211
+ raise TimeoutError unless sock.wait_writable(timeout)
244
212
 
245
213
  begin
246
214
  sock.connect_nonblock(sockaddr)
@@ -258,12 +226,26 @@ class Redis
258
226
  class SSLSocket < ::OpenSSL::SSL::SSLSocket
259
227
  include SocketMixin
260
228
 
229
+ unless method_defined?(:wait_readable)
230
+ def wait_readable(timeout = nil)
231
+ to_io.wait_readable(timeout)
232
+ end
233
+ end
234
+
235
+ unless method_defined?(:wait_writable)
236
+ def wait_writable(timeout = nil)
237
+ to_io.wait_writable(timeout)
238
+ end
239
+ end
240
+
261
241
  def self.connect(host, port, timeout, ssl_params)
262
242
  # Note: this is using Redis::Connection::TCPSocket
263
243
  tcp_sock = TCPSocket.connect(host, port, timeout)
264
244
 
265
245
  ctx = OpenSSL::SSL::SSLContext.new
266
- ctx.set_params(ssl_params) if ssl_params && !ssl_params.empty?
246
+
247
+ # The provided parameters are merged into OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
248
+ ctx.set_params(ssl_params || {})
267
249
 
268
250
  ssl_sock = new(tcp_sock, ctx)
269
251
  ssl_sock.hostname = host
@@ -277,20 +259,23 @@ class Redis
277
259
  # Instead, you have to retry.
278
260
  ssl_sock.connect_nonblock
279
261
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
280
- if IO.select([ssl_sock], nil, nil, timeout)
262
+ if ssl_sock.wait_readable(timeout)
281
263
  retry
282
264
  else
283
265
  raise TimeoutError
284
266
  end
285
267
  rescue IO::WaitWritable
286
- if IO.select(nil, [ssl_sock], nil, timeout)
268
+ if ssl_sock.wait_writable(timeout)
287
269
  retry
288
270
  else
289
271
  raise TimeoutError
290
272
  end
291
273
  end
292
274
 
293
- unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE || (ctx.respond_to?(:verify_hostname) && !ctx.verify_hostname)
275
+ unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE || (
276
+ ctx.respond_to?(:verify_hostname) &&
277
+ !ctx.verify_hostname
278
+ )
294
279
  ssl_sock.post_connection_check(host)
295
280
  end
296
281
 
@@ -302,15 +287,16 @@ class Redis
302
287
  class Ruby
303
288
  include Redis::Connection::CommandHelper
304
289
 
305
- MINUS = "-".freeze
306
- PLUS = "+".freeze
307
- COLON = ":".freeze
308
- DOLLAR = "$".freeze
309
- ASTERISK = "*".freeze
290
+ MINUS = "-"
291
+ PLUS = "+"
292
+ COLON = ":"
293
+ DOLLAR = "$"
294
+ ASTERISK = "*"
310
295
 
311
296
  def self.connect(config)
312
297
  if config[:scheme] == "unix"
313
298
  raise ArgumentError, "SSL incompatible with unix sockets" if config[:ssl]
299
+
314
300
  sock = UNIXSocket.connect(config[:path], config[:connect_timeout])
315
301
  elsif config[:scheme] == "rediss" || config[:ssl]
316
302
  sock = SSLSocket.connect(config[:host], config[:port], config[:connect_timeout], config[:ssl_params])
@@ -326,7 +312,7 @@ class Redis
326
312
  instance
327
313
  end
328
314
 
329
- if [:SOL_SOCKET, :SO_KEEPALIVE, :SOL_TCP, :TCP_KEEPIDLE, :TCP_KEEPINTVL, :TCP_KEEPCNT].all?{|c| Socket.const_defined? c}
315
+ if %i[SOL_SOCKET SO_KEEPALIVE SOL_TCP TCP_KEEPIDLE TCP_KEEPINTVL TCP_KEEPCNT].all? { |c| Socket.const_defined? c }
330
316
  def set_tcp_keepalive(keepalive)
331
317
  return unless keepalive.is_a?(Hash)
332
318
 
@@ -338,14 +324,13 @@ class Redis
338
324
 
339
325
  def get_tcp_keepalive
340
326
  {
341
- :time => @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE).int,
342
- :intvl => @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL).int,
343
- :probes => @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT).int,
327
+ time: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE).int,
328
+ intvl: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL).int,
329
+ probes: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT).int
344
330
  }
345
331
  end
346
332
  else
347
- def set_tcp_keepalive(keepalive)
348
- end
333
+ def set_tcp_keepalive(keepalive); end
349
334
 
350
335
  def get_tcp_keepalive
351
336
  {
@@ -354,13 +339,12 @@ class Redis
354
339
  end
355
340
 
356
341
  # disables Nagle's Algorithm, prevents multiple round trips with MULTI
357
- if [:IPPROTO_TCP, :TCP_NODELAY].all?{|c| Socket.const_defined? c}
342
+ if %i[IPPROTO_TCP TCP_NODELAY].all? { |c| Socket.const_defined? c }
358
343
  def set_tcp_nodelay
359
344
  @sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
360
345
  end
361
346
  else
362
- def set_tcp_nodelay
363
- end
347
+ def set_tcp_nodelay; end
364
348
  end
365
349
 
366
350
  def initialize(sock)
@@ -368,7 +352,7 @@ class Redis
368
352
  end
369
353
 
370
354
  def connected?
371
- !! @sock
355
+ !!@sock
372
356
  end
373
357
 
374
358
  def disconnect
@@ -379,9 +363,7 @@ class Redis
379
363
  end
380
364
 
381
365
  def timeout=(timeout)
382
- if @sock.respond_to?(:timeout=)
383
- @sock.timeout = timeout
384
- end
366
+ @sock.timeout = timeout if @sock.respond_to?(:timeout=)
385
367
  end
386
368
 
387
369
  def write_timeout=(timeout)
@@ -396,7 +378,6 @@ class Redis
396
378
  line = @sock.gets
397
379
  reply_type = line.slice!(0, 1)
398
380
  format_reply(reply_type, line)
399
-
400
381
  rescue Errno::EAGAIN
401
382
  raise TimeoutError
402
383
  end
@@ -408,7 +389,7 @@ class Redis
408
389
  when COLON then format_integer_reply(line)
409
390
  when DOLLAR then format_bulk_reply(line)
410
391
  when ASTERISK then format_multi_bulk_reply(line)
411
- else raise ProtocolError.new(reply_type)
392
+ else raise ProtocolError, reply_type
412
393
  end
413
394
  end
414
395
 
@@ -427,6 +408,7 @@ class Redis
427
408
  def format_bulk_reply(line)
428
409
  bulklen = line.to_i
429
410
  return if bulklen == -1
411
+
430
412
  reply = encode(@sock.read(bulklen))
431
413
  @sock.read(2) # Discard CRLF.
432
414
  reply