redis 4.1.1 → 4.2.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.
@@ -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
 
@@ -50,19 +57,29 @@ class Redis
50
57
  @node_flags[node_key] == ROLE_SLAVE
51
58
  end
52
59
 
60
+ # available_slots is mapping of node_key to list of slot ranges
53
61
  def build_slot_node_key_map(available_slots)
54
- available_slots.each_with_object({}) do |(node_key, slots), acc|
55
- 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
70
+ end
56
71
  end
57
- end
58
72
 
59
- def assign_node_key(mappings, slot, node_key)
60
- mappings[slot] ||= { master: nil, slaves: ::Set.new }
61
- if master?(node_key)
62
- mappings[slot][:master] = node_key
63
- else
64
- 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
65
80
  end
81
+
82
+ by_slot
66
83
  end
67
84
  end
68
85
  end
@@ -13,7 +13,7 @@ class Redis
13
13
  info = {}
14
14
 
15
15
  nodes.each do |node|
16
- info = Hash[*fetch_slot_info(node)]
16
+ info = fetch_slot_info(node)
17
17
  info.empty? ? next : break
18
18
  end
19
19
 
@@ -23,9 +23,10 @@ class Redis
23
23
  end
24
24
 
25
25
  def fetch_slot_info(node)
26
+ hash_with_default_arr = Hash.new { |h, k| h[k] = [] }
26
27
  node.call(%i[cluster slots])
27
- .map { |arr| parse_slot_info(arr, default_ip: node.host) }
28
- .flatten
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] }
29
30
  rescue CannotConnectError, ConnectionError, CommandError
30
31
  {} # can retry on another node
31
32
  end
@@ -34,7 +35,6 @@ class Redis
34
35
  first_slot, last_slot = arr[0..1]
35
36
  slot_range = (first_slot..last_slot).freeze
36
37
  arr[2..-1].map { |addr| [stringify_node_key(addr, default_ip), slot_range] }
37
- .flatten
38
38
  end
39
39
 
40
40
  def stringify_node_key(arr, default_ip)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "connection/registry"
2
4
 
3
5
  # If a connection driver was required before this file, the array
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Redis
2
4
  module Connection
3
5
  module CommandHelper
4
-
5
6
  COMMAND_DELIMITER = "\r\n"
6
7
 
7
8
  def build_command(args)
@@ -28,7 +29,7 @@ class Redis
28
29
  command.join(COMMAND_DELIMITER)
29
30
  end
30
31
 
31
- protected
32
+ protected
32
33
 
33
34
  def encode(string)
34
35
  string.force_encoding(Encoding.default_external)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "registry"
2
4
  require_relative "../errors"
3
5
  require "hiredis/connection"
@@ -6,7 +8,6 @@ require "timeout"
6
8
  class Redis
7
9
  module Connection
8
10
  class Hiredis
9
-
10
11
  def self.connect(config)
11
12
  connection = ::Hiredis::Connection.new
12
13
  connect_timeout = (config.fetch(:connect_timeout, 0) * 1_000_000).to_i
@@ -31,7 +32,7 @@ class Redis
31
32
  end
32
33
 
33
34
  def connected?
34
- @connection && @connection.connected?
35
+ @connection&.connected?
35
36
  end
36
37
 
37
38
  def timeout=(timeout)
@@ -57,7 +58,7 @@ class Redis
57
58
  rescue Errno::EAGAIN
58
59
  raise TimeoutError
59
60
  rescue RuntimeError => err
60
- raise ProtocolError.new(err.message)
61
+ raise ProtocolError, err.message
61
62
  end
62
63
  end
63
64
  end
@@ -1,6 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Redis
2
4
  module Connection
3
-
4
5
  # Store a list of loaded connection drivers in the Connection module.
5
6
  # Redis::Client uses the last required driver by default, and will be aware
6
7
  # of the loaded connection drivers if the user chooses to override the
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "registry"
2
4
  require_relative "command_helper"
3
5
  require_relative "../errors"
@@ -13,8 +15,7 @@ end
13
15
  class Redis
14
16
  module Connection
15
17
  module SocketMixin
16
-
17
- CRLF = "\r\n".freeze
18
+ CRLF = "\r\n"
18
19
 
19
20
  def initialize(*args)
20
21
  super(*args)
@@ -24,46 +25,32 @@ class Redis
24
25
  end
25
26
 
26
27
  def timeout=(timeout)
27
- if timeout && timeout > 0
28
- @timeout = timeout
29
- else
30
- @timeout = nil
31
- end
28
+ @timeout = (timeout if timeout && timeout > 0)
32
29
  end
33
30
 
34
31
  def write_timeout=(timeout)
35
- if timeout && timeout > 0
36
- @write_timeout = timeout
37
- else
38
- @write_timeout = nil
39
- end
32
+ @write_timeout = (timeout if timeout && timeout > 0)
40
33
  end
41
34
 
42
35
  def read(nbytes)
43
36
  result = @buffer.slice!(0, nbytes)
44
37
 
45
- while result.bytesize < nbytes
46
- result << _read_from_socket(nbytes - result.bytesize)
47
- end
38
+ result << _read_from_socket(nbytes - result.bytesize) while result.bytesize < nbytes
48
39
 
49
40
  result
50
41
  end
51
42
 
52
43
  def gets
53
- crlf = nil
54
-
55
- while (crlf = @buffer.index(CRLF)) == nil
56
- @buffer << _read_from_socket(1024)
44
+ while (crlf = @buffer.index(CRLF)).nil?
45
+ @buffer << _read_from_socket(16_384)
57
46
  end
58
47
 
59
48
  @buffer.slice!(0, crlf + CRLF.bytesize)
60
49
  end
61
50
 
62
51
  def _read_from_socket(nbytes)
63
-
64
52
  begin
65
53
  read_nonblock(nbytes)
66
-
67
54
  rescue IO::WaitReadable
68
55
  if IO.select([self], nil, nil, @timeout)
69
56
  retry
@@ -77,7 +64,6 @@ class Redis
77
64
  raise Redis::TimeoutError
78
65
  end
79
66
  end
80
-
81
67
  rescue EOFError
82
68
  raise Errno::ECONNRESET
83
69
  end
@@ -85,7 +71,6 @@ class Redis
85
71
  def _write_to_socket(data)
86
72
  begin
87
73
  write_nonblock(data)
88
-
89
74
  rescue IO::WaitWritable
90
75
  if IO.select(nil, [self], nil, @write_timeout)
91
76
  retry
@@ -99,7 +84,6 @@ class Redis
99
84
  raise Redis::TimeoutError
100
85
  end
101
86
  end
102
-
103
87
  rescue EOFError
104
88
  raise Errno::ECONNRESET
105
89
  end
@@ -114,6 +98,7 @@ class Redis
114
98
 
115
99
  total_count += count
116
100
  return total_count if total_count >= length
101
+
117
102
  data = data.byteslice(count..-1)
118
103
  end
119
104
  end
@@ -124,7 +109,6 @@ class Redis
124
109
  require "timeout"
125
110
 
126
111
  class TCPSocket < ::TCPSocket
127
-
128
112
  include SocketMixin
129
113
 
130
114
  def self.connect(host, port, timeout)
@@ -140,7 +124,6 @@ class Redis
140
124
  if defined?(::UNIXSocket)
141
125
 
142
126
  class UNIXSocket < ::UNIXSocket
143
-
144
127
  include SocketMixin
145
128
 
146
129
  def self.connect(path, timeout)
@@ -158,7 +141,6 @@ class Redis
158
141
 
159
142
  def _read_from_socket(nbytes)
160
143
  readpartial(nbytes)
161
-
162
144
  rescue EOFError
163
145
  raise Errno::ECONNRESET
164
146
  end
@@ -169,19 +151,16 @@ class Redis
169
151
  else
170
152
 
171
153
  class TCPSocket < ::Socket
172
-
173
154
  include SocketMixin
174
155
 
175
- def self.connect_addrinfo(ai, port, timeout)
176
- sock = new(::Socket.const_get(ai[0]), Socket::SOCK_STREAM, 0)
177
- sockaddr = ::Socket.pack_sockaddr_in(port, ai[3])
156
+ def self.connect_addrinfo(addrinfo, port, timeout)
157
+ sock = new(::Socket.const_get(addrinfo[0]), Socket::SOCK_STREAM, 0)
158
+ sockaddr = ::Socket.pack_sockaddr_in(port, addrinfo[3])
178
159
 
179
160
  begin
180
161
  sock.connect_nonblock(sockaddr)
181
162
  rescue Errno::EINPROGRESS
182
- if IO.select(nil, [sock], nil, timeout) == nil
183
- raise TimeoutError
184
- end
163
+ raise TimeoutError if IO.select(nil, [sock], nil, timeout).nil?
185
164
 
186
165
  begin
187
166
  sock.connect_nonblock(sockaddr)
@@ -220,14 +199,13 @@ class Redis
220
199
  return connect_addrinfo(ai, port, timeout)
221
200
  rescue SystemCallError
222
201
  # Raise if this was our last attempt.
223
- raise if addrinfo.length == i+1
202
+ raise if addrinfo.length == i + 1
224
203
  end
225
204
  end
226
205
  end
227
206
  end
228
207
 
229
208
  class UNIXSocket < ::Socket
230
-
231
209
  include SocketMixin
232
210
 
233
211
  def self.connect(path, timeout)
@@ -237,9 +215,7 @@ class Redis
237
215
  begin
238
216
  sock.connect_nonblock(sockaddr)
239
217
  rescue Errno::EINPROGRESS
240
- if IO.select(nil, [sock], nil, timeout) == nil
241
- raise TimeoutError
242
- end
218
+ raise TimeoutError if IO.select(nil, [sock], nil, timeout).nil?
243
219
 
244
220
  begin
245
221
  sock.connect_nonblock(sockaddr)
@@ -262,13 +238,39 @@ class Redis
262
238
  tcp_sock = TCPSocket.connect(host, port, timeout)
263
239
 
264
240
  ctx = OpenSSL::SSL::SSLContext.new
265
- ctx.set_params(ssl_params) if ssl_params && !ssl_params.empty?
241
+
242
+ # The provided parameters are merged into OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
243
+ ctx.set_params(ssl_params || {})
266
244
 
267
245
  ssl_sock = new(tcp_sock, ctx)
268
246
  ssl_sock.hostname = host
269
- ssl_sock.connect
270
247
 
271
- unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE
248
+ begin
249
+ # Initiate the socket connection in the background. If it doesn't fail
250
+ # immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS)
251
+ # indicating the connection is in progress.
252
+ # Unlike waiting for a tcp socket to connect, you can't time out ssl socket
253
+ # connections during the connect phase properly, because IO.select only partially works.
254
+ # Instead, you have to retry.
255
+ ssl_sock.connect_nonblock
256
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
257
+ if IO.select([ssl_sock], nil, nil, timeout)
258
+ retry
259
+ else
260
+ raise TimeoutError
261
+ end
262
+ rescue IO::WaitWritable
263
+ if IO.select(nil, [ssl_sock], nil, timeout)
264
+ retry
265
+ else
266
+ raise TimeoutError
267
+ end
268
+ end
269
+
270
+ unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE || (
271
+ ctx.respond_to?(:verify_hostname) &&
272
+ !ctx.verify_hostname
273
+ )
272
274
  ssl_sock.post_connection_check(host)
273
275
  end
274
276
 
@@ -280,15 +282,16 @@ class Redis
280
282
  class Ruby
281
283
  include Redis::Connection::CommandHelper
282
284
 
283
- MINUS = "-".freeze
284
- PLUS = "+".freeze
285
- COLON = ":".freeze
286
- DOLLAR = "$".freeze
287
- ASTERISK = "*".freeze
285
+ MINUS = "-"
286
+ PLUS = "+"
287
+ COLON = ":"
288
+ DOLLAR = "$"
289
+ ASTERISK = "*"
288
290
 
289
291
  def self.connect(config)
290
292
  if config[:scheme] == "unix"
291
293
  raise ArgumentError, "SSL incompatible with unix sockets" if config[:ssl]
294
+
292
295
  sock = UNIXSocket.connect(config[:path], config[:connect_timeout])
293
296
  elsif config[:scheme] == "rediss" || config[:ssl]
294
297
  sock = SSLSocket.connect(config[:host], config[:port], config[:connect_timeout], config[:ssl_params])
@@ -300,10 +303,11 @@ class Redis
300
303
  instance.timeout = config[:read_timeout]
301
304
  instance.write_timeout = config[:write_timeout]
302
305
  instance.set_tcp_keepalive config[:tcp_keepalive]
306
+ instance.set_tcp_nodelay if sock.is_a? TCPSocket
303
307
  instance
304
308
  end
305
309
 
306
- if [:SOL_SOCKET, :SO_KEEPALIVE, :SOL_TCP, :TCP_KEEPIDLE, :TCP_KEEPINTVL, :TCP_KEEPCNT].all?{|c| Socket.const_defined? c}
310
+ if %i[SOL_SOCKET SO_KEEPALIVE SOL_TCP TCP_KEEPIDLE TCP_KEEPINTVL TCP_KEEPCNT].all? { |c| Socket.const_defined? c }
307
311
  def set_tcp_keepalive(keepalive)
308
312
  return unless keepalive.is_a?(Hash)
309
313
 
@@ -315,14 +319,13 @@ class Redis
315
319
 
316
320
  def get_tcp_keepalive
317
321
  {
318
- :time => @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE).int,
319
- :intvl => @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL).int,
320
- :probes => @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT).int,
322
+ time: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE).int,
323
+ intvl: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL).int,
324
+ probes: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT).int
321
325
  }
322
326
  end
323
327
  else
324
- def set_tcp_keepalive(keepalive)
325
- end
328
+ def set_tcp_keepalive(keepalive); end
326
329
 
327
330
  def get_tcp_keepalive
328
331
  {
@@ -330,12 +333,21 @@ class Redis
330
333
  end
331
334
  end
332
335
 
336
+ # disables Nagle's Algorithm, prevents multiple round trips with MULTI
337
+ if %i[IPPROTO_TCP TCP_NODELAY].all? { |c| Socket.const_defined? c }
338
+ def set_tcp_nodelay
339
+ @sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
340
+ end
341
+ else
342
+ def set_tcp_nodelay; end
343
+ end
344
+
333
345
  def initialize(sock)
334
346
  @sock = sock
335
347
  end
336
348
 
337
349
  def connected?
338
- !! @sock
350
+ !!@sock
339
351
  end
340
352
 
341
353
  def disconnect
@@ -346,9 +358,7 @@ class Redis
346
358
  end
347
359
 
348
360
  def timeout=(timeout)
349
- if @sock.respond_to?(:timeout=)
350
- @sock.timeout = timeout
351
- end
361
+ @sock.timeout = timeout if @sock.respond_to?(:timeout=)
352
362
  end
353
363
 
354
364
  def write_timeout=(timeout)
@@ -363,7 +373,6 @@ class Redis
363
373
  line = @sock.gets
364
374
  reply_type = line.slice!(0, 1)
365
375
  format_reply(reply_type, line)
366
-
367
376
  rescue Errno::EAGAIN
368
377
  raise TimeoutError
369
378
  end
@@ -375,7 +384,7 @@ class Redis
375
384
  when COLON then format_integer_reply(line)
376
385
  when DOLLAR then format_bulk_reply(line)
377
386
  when ASTERISK then format_multi_bulk_reply(line)
378
- else raise ProtocolError.new(reply_type)
387
+ else raise ProtocolError, reply_type
379
388
  end
380
389
  end
381
390
 
@@ -394,6 +403,7 @@ class Redis
394
403
  def format_bulk_reply(line)
395
404
  bulklen = line.to_i
396
405
  return if bulklen == -1
406
+
397
407
  reply = encode(@sock.read(bulklen))
398
408
  @sock.read(2) # Discard CRLF.
399
409
  reply