redis 4.1.0 → 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.
@@ -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)
data/lib/redis/cluster.rb CHANGED
@@ -78,10 +78,13 @@ class Redis
78
78
  end
79
79
 
80
80
  def call_pipeline(pipeline)
81
- node_keys, command_keys = extract_keys_in_pipeline(pipeline)
82
- raise CrossSlotPipeliningError, command_keys if node_keys.size > 1
83
- node = find_node(node_keys.first)
84
- try_send(node, :call_pipeline, pipeline)
81
+ node_keys = pipeline.commands.map { |cmd| find_node_key(cmd, primary_only: true) }.compact.uniq
82
+ if node_keys.size > 1
83
+ raise(CrossSlotPipeliningError,
84
+ pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?).uniq)
85
+ end
86
+
87
+ try_send(find_node(node_keys.first), :call_pipeline, pipeline)
85
88
  end
86
89
 
87
90
  def call_with_timeout(command, timeout, &block)
@@ -112,12 +115,11 @@ class Redis
112
115
  node = Node.new(option.per_node_key)
113
116
  available_slots = SlotLoader.load(node)
114
117
  node_flags = NodeLoader.load_flags(node)
115
- available_node_urls = NodeKey.to_node_urls(available_slots.keys, secure: option.secure?)
116
- option.update_node(available_node_urls)
118
+ option.update_node(available_slots.keys.map { |k| NodeKey.optionize(k) })
117
119
  [Node.new(option.per_node_key, node_flags, option.use_replica?),
118
120
  Slot.new(available_slots, node_flags, option.use_replica?)]
119
121
  ensure
120
- node.map(&:disconnect)
122
+ node&.each(&:disconnect)
121
123
  end
122
124
 
123
125
  def fetch_command_details(nodes)
@@ -128,7 +130,7 @@ class Redis
128
130
  def send_command(command, &block)
129
131
  cmd = command.first.to_s.downcase
130
132
  case cmd
131
- when 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
133
+ when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
132
134
  @node.call_all(command, &block).first
133
135
  when 'flushall', 'flushdb'
134
136
  @node.call_master(command, &block).first
@@ -216,9 +218,14 @@ class Redis
216
218
  node.public_send(method_name, *args, &block)
217
219
  rescue CommandError => err
218
220
  if err.message.start_with?('MOVED')
219
- assign_redirection_node(err.message).public_send(method_name, *args, &block)
221
+ raise if retry_count <= 0
222
+
223
+ node = assign_redirection_node(err.message)
224
+ retry_count -= 1
225
+ retry
220
226
  elsif err.message.start_with?('ASK')
221
227
  raise if retry_count <= 0
228
+
222
229
  node = assign_asking_node(err.message)
223
230
  node.call(%i[asking])
224
231
  retry_count -= 1
@@ -226,6 +233,9 @@ class Redis
226
233
  else
227
234
  raise
228
235
  end
236
+ rescue CannotConnectError
237
+ update_cluster_info!
238
+ raise
229
239
  end
230
240
 
231
241
  def assign_redirection_node(err_msg)
@@ -245,14 +255,14 @@ class Redis
245
255
  find_node(node_key)
246
256
  end
247
257
 
248
- def find_node_key(command)
258
+ def find_node_key(command, primary_only: false)
249
259
  key = @command.extract_first_key(command)
250
260
  return if key.empty?
251
261
 
252
262
  slot = KeySlotConverter.convert(key)
253
263
  return unless @slot.exists?(slot)
254
264
 
255
- if @command.should_send_to_master?(command)
265
+ if @command.should_send_to_master?(command) || primary_only
256
266
  @slot.find_node_key_of_master(slot)
257
267
  else
258
268
  @slot.find_node_key_of_slave(slot)
@@ -261,6 +271,7 @@ class Redis
261
271
 
262
272
  def find_node(node_key)
263
273
  return @node.sample if node_key.nil?
274
+
264
275
  @node.find_by(node_key)
265
276
  rescue Node::ReloadNeeded
266
277
  update_cluster_info!(node_key)
@@ -276,11 +287,5 @@ class Redis
276
287
  @node.map(&:disconnect)
277
288
  @node, @slot = fetch_cluster_info!(@option)
278
289
  end
279
-
280
- def extract_keys_in_pipeline(pipeline)
281
- node_keys = pipeline.commands.map { |cmd| find_node_key(cmd) }.compact.uniq
282
- command_keys = pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?)
283
- [node_keys, command_keys]
284
- end
285
290
  end
286
291
  end
@@ -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)
@@ -11,11 +12,13 @@ class Redis
11
12
  if i.is_a? Array
12
13
  i.each do |j|
13
14
  j = j.to_s
15
+ j = j.encoding == Encoding::BINARY ? j : j.b
14
16
  command << "$#{j.bytesize}"
15
17
  command << j
16
18
  end
17
19
  else
18
20
  i = i.to_s
21
+ i = i.encoding == Encoding::BINARY ? i : i.b
19
22
  command << "$#{i.bytesize}"
20
23
  command << i
21
24
  end
@@ -28,7 +31,7 @@ class Redis
28
31
  command.join(COMMAND_DELIMITER)
29
32
  end
30
33
 
31
- protected
34
+ protected
32
35
 
33
36
  def encode(string)
34
37
  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,108 +15,85 @@ 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)
21
22
 
22
23
  @timeout = @write_timeout = nil
23
- @buffer = "".dup
24
+ @buffer = "".b
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
+ buffer = String.new(capacity: nbytes, encoding: Encoding::ASCII_8BIT)
39
+ result << _read_from_socket(nbytes - result.bytesize, buffer) while result.bytesize < nbytes
48
40
 
49
41
  result
50
42
  end
51
43
 
52
44
  def gets
53
- crlf = nil
54
-
55
- while (crlf = @buffer.index(CRLF)) == nil
56
- @buffer << _read_from_socket(1024)
45
+ while (crlf = @buffer.index(CRLF)).nil?
46
+ @buffer << _read_from_socket(16_384)
57
47
  end
58
48
 
59
49
  @buffer.slice!(0, crlf + CRLF.bytesize)
60
50
  end
61
51
 
62
- def _read_from_socket(nbytes)
63
-
64
- begin
65
- read_nonblock(nbytes)
66
-
67
- rescue IO::WaitReadable
68
- if IO.select([self], nil, nil, @timeout)
69
- retry
70
- else
71
- raise Redis::TimeoutError
72
- end
73
- rescue IO::WaitWritable
74
- if IO.select(nil, [self], nil, @timeout)
75
- retry
76
- else
77
- raise Redis::TimeoutError
78
- end
79
- end
80
-
81
- rescue EOFError
82
- raise Errno::ECONNRESET
83
- end
84
-
85
- def _write_to_socket(data)
86
- begin
87
- write_nonblock(data)
88
-
89
- rescue IO::WaitWritable
90
- if IO.select(nil, [self], nil, @write_timeout)
91
- retry
92
- else
93
- raise Redis::TimeoutError
94
- end
95
- rescue IO::WaitReadable
96
- if IO.select([self], nil, nil, @write_timeout)
97
- retry
98
- else
99
- raise Redis::TimeoutError
52
+ def _read_from_socket(nbytes, buffer = nil)
53
+ loop do
54
+ case chunk = read_nonblock(nbytes, buffer, exception: false)
55
+ when :wait_readable
56
+ unless wait_readable(@timeout)
57
+ raise Redis::TimeoutError
58
+ end
59
+ when :wait_writable
60
+ unless wait_writable(@timeout)
61
+ raise Redis::TimeoutError
62
+ end
63
+ when nil
64
+ raise Errno::ECONNRESET
65
+ when String
66
+ return chunk
100
67
  end
101
68
  end
102
-
103
- rescue EOFError
104
- raise Errno::ECONNRESET
105
69
  end
106
70
 
107
- def write(data)
108
- return super(data) unless @write_timeout
71
+ def write(buffer)
72
+ return super(buffer) unless @write_timeout
109
73
 
110
- length = data.bytesize
111
- total_count = 0
74
+ bytes_to_write = buffer.bytesize
75
+ total_bytes_written = 0
112
76
  loop do
113
- count = _write_to_socket(data)
77
+ case bytes_written = write_nonblock(buffer, exception: false)
78
+ when :wait_readable
79
+ unless wait_readable(@write_timeout)
80
+ raise Redis::TimeoutError
81
+ end
82
+ when :wait_writable
83
+ unless wait_writable(@write_timeout)
84
+ raise Redis::TimeoutError
85
+ end
86
+ when nil
87
+ raise Errno::ECONNRESET
88
+ when Integer
89
+ total_bytes_written += bytes_written
90
+
91
+ if total_bytes_written >= bytes_to_write
92
+ return total_bytes_written
93
+ end
114
94
 
115
- total_count += count
116
- return total_count if total_count >= length
117
- data = data.byteslice(count..-1)
95
+ buffer = buffer.byteslice(bytes_written..-1)
96
+ end
118
97
  end
119
98
  end
120
99
  end
@@ -124,7 +103,6 @@ class Redis
124
103
  require "timeout"
125
104
 
126
105
  class TCPSocket < ::TCPSocket
127
-
128
106
  include SocketMixin
129
107
 
130
108
  def self.connect(host, port, timeout)
@@ -140,7 +118,6 @@ class Redis
140
118
  if defined?(::UNIXSocket)
141
119
 
142
120
  class UNIXSocket < ::UNIXSocket
143
-
144
121
  include SocketMixin
145
122
 
146
123
  def self.connect(path, timeout)
@@ -152,13 +129,12 @@ class Redis
152
129
  raise TimeoutError
153
130
  end
154
131
 
155
- # JRuby raises Errno::EAGAIN on #read_nonblock even when IO.select
132
+ # JRuby raises Errno::EAGAIN on #read_nonblock even when it
156
133
  # says it is readable (1.6.6, in both 1.8 and 1.9 mode).
157
134
  # Use the blocking #readpartial method instead.
158
135
 
159
136
  def _read_from_socket(nbytes)
160
137
  readpartial(nbytes)
161
-
162
138
  rescue EOFError
163
139
  raise Errno::ECONNRESET
164
140
  end
@@ -169,19 +145,16 @@ class Redis
169
145
  else
170
146
 
171
147
  class TCPSocket < ::Socket
172
-
173
148
  include SocketMixin
174
149
 
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])
150
+ def self.connect_addrinfo(addrinfo, port, timeout)
151
+ sock = new(::Socket.const_get(addrinfo[0]), Socket::SOCK_STREAM, 0)
152
+ sockaddr = ::Socket.pack_sockaddr_in(port, addrinfo[3])
178
153
 
179
154
  begin
180
155
  sock.connect_nonblock(sockaddr)
181
156
  rescue Errno::EINPROGRESS
182
- if IO.select(nil, [sock], nil, timeout) == nil
183
- raise TimeoutError
184
- end
157
+ raise TimeoutError unless sock.wait_writable(timeout)
185
158
 
186
159
  begin
187
160
  sock.connect_nonblock(sockaddr)
@@ -220,14 +193,13 @@ class Redis
220
193
  return connect_addrinfo(ai, port, timeout)
221
194
  rescue SystemCallError
222
195
  # Raise if this was our last attempt.
223
- raise if addrinfo.length == i+1
196
+ raise if addrinfo.length == i + 1
224
197
  end
225
198
  end
226
199
  end
227
200
  end
228
201
 
229
202
  class UNIXSocket < ::Socket
230
-
231
203
  include SocketMixin
232
204
 
233
205
  def self.connect(path, timeout)
@@ -237,9 +209,7 @@ class Redis
237
209
  begin
238
210
  sock.connect_nonblock(sockaddr)
239
211
  rescue Errno::EINPROGRESS
240
- if IO.select(nil, [sock], nil, timeout) == nil
241
- raise TimeoutError
242
- end
212
+ raise TimeoutError unless sock.wait_writable(timeout)
243
213
 
244
214
  begin
245
215
  sock.connect_nonblock(sockaddr)
@@ -257,18 +227,56 @@ class Redis
257
227
  class SSLSocket < ::OpenSSL::SSL::SSLSocket
258
228
  include SocketMixin
259
229
 
230
+ unless method_defined?(:wait_readable)
231
+ def wait_readable(timeout = nil)
232
+ to_io.wait_readable(timeout)
233
+ end
234
+ end
235
+
236
+ unless method_defined?(:wait_writable)
237
+ def wait_writable(timeout = nil)
238
+ to_io.wait_writable(timeout)
239
+ end
240
+ end
241
+
260
242
  def self.connect(host, port, timeout, ssl_params)
261
243
  # Note: this is using Redis::Connection::TCPSocket
262
244
  tcp_sock = TCPSocket.connect(host, port, timeout)
263
245
 
264
246
  ctx = OpenSSL::SSL::SSLContext.new
265
- ctx.set_params(ssl_params) if ssl_params && !ssl_params.empty?
247
+
248
+ # The provided parameters are merged into OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
249
+ ctx.set_params(ssl_params || {})
266
250
 
267
251
  ssl_sock = new(tcp_sock, ctx)
268
252
  ssl_sock.hostname = host
269
- ssl_sock.connect
270
253
 
271
- unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE
254
+ begin
255
+ # Initiate the socket connection in the background. If it doesn't fail
256
+ # immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS)
257
+ # indicating the connection is in progress.
258
+ # Unlike waiting for a tcp socket to connect, you can't time out ssl socket
259
+ # connections during the connect phase properly, because IO.select only partially works.
260
+ # Instead, you have to retry.
261
+ ssl_sock.connect_nonblock
262
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
263
+ if ssl_sock.wait_readable(timeout)
264
+ retry
265
+ else
266
+ raise TimeoutError
267
+ end
268
+ rescue IO::WaitWritable
269
+ if ssl_sock.wait_writable(timeout)
270
+ retry
271
+ else
272
+ raise TimeoutError
273
+ end
274
+ end
275
+
276
+ unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE || (
277
+ ctx.respond_to?(:verify_hostname) &&
278
+ !ctx.verify_hostname
279
+ )
272
280
  ssl_sock.post_connection_check(host)
273
281
  end
274
282
 
@@ -280,15 +288,16 @@ class Redis
280
288
  class Ruby
281
289
  include Redis::Connection::CommandHelper
282
290
 
283
- MINUS = "-".freeze
284
- PLUS = "+".freeze
285
- COLON = ":".freeze
286
- DOLLAR = "$".freeze
287
- ASTERISK = "*".freeze
291
+ MINUS = "-"
292
+ PLUS = "+"
293
+ COLON = ":"
294
+ DOLLAR = "$"
295
+ ASTERISK = "*"
288
296
 
289
297
  def self.connect(config)
290
298
  if config[:scheme] == "unix"
291
299
  raise ArgumentError, "SSL incompatible with unix sockets" if config[:ssl]
300
+
292
301
  sock = UNIXSocket.connect(config[:path], config[:connect_timeout])
293
302
  elsif config[:scheme] == "rediss" || config[:ssl]
294
303
  sock = SSLSocket.connect(config[:host], config[:port], config[:connect_timeout], config[:ssl_params])
@@ -300,10 +309,11 @@ class Redis
300
309
  instance.timeout = config[:read_timeout]
301
310
  instance.write_timeout = config[:write_timeout]
302
311
  instance.set_tcp_keepalive config[:tcp_keepalive]
312
+ instance.set_tcp_nodelay if sock.is_a? TCPSocket
303
313
  instance
304
314
  end
305
315
 
306
- if [:SOL_SOCKET, :SO_KEEPALIVE, :SOL_TCP, :TCP_KEEPIDLE, :TCP_KEEPINTVL, :TCP_KEEPCNT].all?{|c| Socket.const_defined? c}
316
+ if %i[SOL_SOCKET SO_KEEPALIVE SOL_TCP TCP_KEEPIDLE TCP_KEEPINTVL TCP_KEEPCNT].all? { |c| Socket.const_defined? c }
307
317
  def set_tcp_keepalive(keepalive)
308
318
  return unless keepalive.is_a?(Hash)
309
319
 
@@ -315,14 +325,13 @@ class Redis
315
325
 
316
326
  def get_tcp_keepalive
317
327
  {
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,
328
+ time: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE).int,
329
+ intvl: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL).int,
330
+ probes: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT).int
321
331
  }
322
332
  end
323
333
  else
324
- def set_tcp_keepalive(keepalive)
325
- end
334
+ def set_tcp_keepalive(keepalive); end
326
335
 
327
336
  def get_tcp_keepalive
328
337
  {
@@ -330,12 +339,21 @@ class Redis
330
339
  end
331
340
  end
332
341
 
342
+ # disables Nagle's Algorithm, prevents multiple round trips with MULTI
343
+ if %i[IPPROTO_TCP TCP_NODELAY].all? { |c| Socket.const_defined? c }
344
+ def set_tcp_nodelay
345
+ @sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
346
+ end
347
+ else
348
+ def set_tcp_nodelay; end
349
+ end
350
+
333
351
  def initialize(sock)
334
352
  @sock = sock
335
353
  end
336
354
 
337
355
  def connected?
338
- !! @sock
356
+ !!@sock
339
357
  end
340
358
 
341
359
  def disconnect
@@ -346,9 +364,7 @@ class Redis
346
364
  end
347
365
 
348
366
  def timeout=(timeout)
349
- if @sock.respond_to?(:timeout=)
350
- @sock.timeout = timeout
351
- end
367
+ @sock.timeout = timeout if @sock.respond_to?(:timeout=)
352
368
  end
353
369
 
354
370
  def write_timeout=(timeout)
@@ -363,7 +379,6 @@ class Redis
363
379
  line = @sock.gets
364
380
  reply_type = line.slice!(0, 1)
365
381
  format_reply(reply_type, line)
366
-
367
382
  rescue Errno::EAGAIN
368
383
  raise TimeoutError
369
384
  end
@@ -375,7 +390,7 @@ class Redis
375
390
  when COLON then format_integer_reply(line)
376
391
  when DOLLAR then format_bulk_reply(line)
377
392
  when ASTERISK then format_multi_bulk_reply(line)
378
- else raise ProtocolError.new(reply_type)
393
+ else raise ProtocolError, reply_type
379
394
  end
380
395
  end
381
396
 
@@ -394,6 +409,7 @@ class Redis
394
409
  def format_bulk_reply(line)
395
410
  bulklen = line.to_i
396
411
  return if bulklen == -1
412
+
397
413
  reply = encode(@sock.read(bulklen))
398
414
  @sock.read(2) # Discard CRLF.
399
415
  reply