redis 3.0.0 → 4.2.2

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.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +269 -0
  3. data/README.md +295 -58
  4. data/lib/redis.rb +1760 -451
  5. data/lib/redis/client.rb +355 -88
  6. data/lib/redis/cluster.rb +295 -0
  7. data/lib/redis/cluster/command.rb +81 -0
  8. data/lib/redis/cluster/command_loader.rb +34 -0
  9. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  10. data/lib/redis/cluster/node.rb +107 -0
  11. data/lib/redis/cluster/node_key.rb +31 -0
  12. data/lib/redis/cluster/node_loader.rb +37 -0
  13. data/lib/redis/cluster/option.rb +90 -0
  14. data/lib/redis/cluster/slot.rb +86 -0
  15. data/lib/redis/cluster/slot_loader.rb +49 -0
  16. data/lib/redis/connection.rb +4 -2
  17. data/lib/redis/connection/command_helper.rb +5 -10
  18. data/lib/redis/connection/hiredis.rb +12 -8
  19. data/lib/redis/connection/registry.rb +2 -1
  20. data/lib/redis/connection/ruby.rb +232 -63
  21. data/lib/redis/connection/synchrony.rb +41 -14
  22. data/lib/redis/distributed.rb +205 -70
  23. data/lib/redis/errors.rb +48 -0
  24. data/lib/redis/hash_ring.rb +31 -73
  25. data/lib/redis/pipeline.rb +74 -18
  26. data/lib/redis/subscribe.rb +24 -13
  27. data/lib/redis/version.rb +3 -1
  28. metadata +63 -160
  29. data/.gitignore +0 -10
  30. data/.order +0 -169
  31. data/.travis.yml +0 -50
  32. data/.travis/Gemfile +0 -11
  33. data/.yardopts +0 -3
  34. data/Rakefile +0 -392
  35. data/benchmarking/logging.rb +0 -62
  36. data/benchmarking/pipeline.rb +0 -51
  37. data/benchmarking/speed.rb +0 -21
  38. data/benchmarking/suite.rb +0 -24
  39. data/benchmarking/worker.rb +0 -71
  40. data/examples/basic.rb +0 -15
  41. data/examples/dist_redis.rb +0 -43
  42. data/examples/incr-decr.rb +0 -17
  43. data/examples/list.rb +0 -26
  44. data/examples/pubsub.rb +0 -31
  45. data/examples/sets.rb +0 -36
  46. data/examples/unicorn/config.ru +0 -3
  47. data/examples/unicorn/unicorn.rb +0 -20
  48. data/redis.gemspec +0 -41
  49. data/test/blocking_commands_test.rb +0 -42
  50. data/test/command_map_test.rb +0 -30
  51. data/test/commands_on_hashes_test.rb +0 -21
  52. data/test/commands_on_lists_test.rb +0 -20
  53. data/test/commands_on_sets_test.rb +0 -77
  54. data/test/commands_on_sorted_sets_test.rb +0 -109
  55. data/test/commands_on_strings_test.rb +0 -83
  56. data/test/commands_on_value_types_test.rb +0 -99
  57. data/test/connection_handling_test.rb +0 -189
  58. data/test/db/.gitignore +0 -1
  59. data/test/distributed_blocking_commands_test.rb +0 -46
  60. data/test/distributed_commands_on_hashes_test.rb +0 -10
  61. data/test/distributed_commands_on_lists_test.rb +0 -22
  62. data/test/distributed_commands_on_sets_test.rb +0 -83
  63. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  64. data/test/distributed_commands_on_strings_test.rb +0 -48
  65. data/test/distributed_commands_on_value_types_test.rb +0 -87
  66. data/test/distributed_commands_requiring_clustering_test.rb +0 -148
  67. data/test/distributed_connection_handling_test.rb +0 -23
  68. data/test/distributed_internals_test.rb +0 -15
  69. data/test/distributed_key_tags_test.rb +0 -52
  70. data/test/distributed_persistence_control_commands_test.rb +0 -26
  71. data/test/distributed_publish_subscribe_test.rb +0 -92
  72. data/test/distributed_remote_server_control_commands_test.rb +0 -53
  73. data/test/distributed_scripting_test.rb +0 -102
  74. data/test/distributed_sorting_test.rb +0 -20
  75. data/test/distributed_test.rb +0 -58
  76. data/test/distributed_transactions_test.rb +0 -32
  77. data/test/encoding_test.rb +0 -18
  78. data/test/error_replies_test.rb +0 -59
  79. data/test/helper.rb +0 -188
  80. data/test/helper_test.rb +0 -22
  81. data/test/internals_test.rb +0 -214
  82. data/test/lint/blocking_commands.rb +0 -124
  83. data/test/lint/hashes.rb +0 -162
  84. data/test/lint/lists.rb +0 -143
  85. data/test/lint/sets.rb +0 -96
  86. data/test/lint/sorted_sets.rb +0 -201
  87. data/test/lint/strings.rb +0 -157
  88. data/test/lint/value_types.rb +0 -106
  89. data/test/persistence_control_commands_test.rb +0 -26
  90. data/test/pipelining_commands_test.rb +0 -195
  91. data/test/publish_subscribe_test.rb +0 -153
  92. data/test/remote_server_control_commands_test.rb +0 -104
  93. data/test/scripting_test.rb +0 -78
  94. data/test/sorting_test.rb +0 -45
  95. data/test/support/connection/hiredis.rb +0 -1
  96. data/test/support/connection/ruby.rb +0 -1
  97. data/test/support/connection/synchrony.rb +0 -17
  98. data/test/support/redis_mock.rb +0 -92
  99. data/test/support/wire/synchrony.rb +0 -24
  100. data/test/support/wire/thread.rb +0 -5
  101. data/test/synchrony_driver.rb +0 -57
  102. data/test/test.conf +0 -9
  103. data/test/thread_safety_test.rb +0 -32
  104. data/test/transactions_test.rb +0 -244
  105. data/test/unknown_commands_test.rb +0 -14
  106. data/test/url_param_test.rb +0 -64
@@ -1,23 +1,27 @@
1
- require "redis/connection/registry"
2
- require "redis/errors"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry"
4
+ require_relative "../errors"
3
5
  require "hiredis/connection"
4
6
  require "timeout"
5
7
 
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
13
+ connect_timeout = (config.fetch(:connect_timeout, 0) * 1_000_000).to_i
12
14
 
13
15
  if config[:scheme] == "unix"
14
- connection.connect_unix(config[:path], Integer(config[:timeout] * 1_000_000))
16
+ connection.connect_unix(config[:path], connect_timeout)
17
+ elsif config[:scheme] == "rediss" || config[:ssl]
18
+ raise NotImplementedError, "SSL not supported by hiredis driver"
15
19
  else
16
- connection.connect(config[:host], config[:port], Integer(config[:timeout] * 1_000_000))
20
+ connection.connect(config[:host], config[:port], connect_timeout)
17
21
  end
18
22
 
19
23
  instance = new(connection)
20
- instance.timeout = config[:timeout]
24
+ instance.timeout = config[:read_timeout]
21
25
  instance
22
26
  rescue Errno::ETIMEDOUT
23
27
  raise TimeoutError
@@ -28,7 +32,7 @@ class Redis
28
32
  end
29
33
 
30
34
  def connected?
31
- @connection && @connection.connected?
35
+ @connection&.connected?
32
36
  end
33
37
 
34
38
  def timeout=(timeout)
@@ -54,7 +58,7 @@ class Redis
54
58
  rescue Errno::EAGAIN
55
59
  raise TimeoutError
56
60
  rescue RuntimeError => err
57
- raise ProtocolError.new(err.message)
61
+ raise ProtocolError, err.message
58
62
  end
59
63
  end
60
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,44 +1,48 @@
1
- require "redis/connection/registry"
2
- require "redis/connection/command_helper"
3
- require "redis/errors"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry"
4
+ require_relative "command_helper"
5
+ require_relative "../errors"
4
6
  require "socket"
7
+ require "timeout"
8
+
9
+ begin
10
+ require "openssl"
11
+ rescue LoadError
12
+ # Not all systems have OpenSSL support
13
+ end
5
14
 
6
15
  class Redis
7
16
  module Connection
8
17
  module SocketMixin
9
-
10
- CRLF = "\r\n".freeze
18
+ CRLF = "\r\n"
11
19
 
12
20
  def initialize(*args)
13
21
  super(*args)
14
22
 
15
- @timeout = nil
16
- @buffer = ""
23
+ @timeout = @write_timeout = nil
24
+ @buffer = "".dup
17
25
  end
18
26
 
19
27
  def timeout=(timeout)
20
- if timeout && timeout > 0
21
- @timeout = timeout
22
- else
23
- @timeout = nil
24
- end
28
+ @timeout = (timeout if timeout && timeout > 0)
29
+ end
30
+
31
+ def write_timeout=(timeout)
32
+ @write_timeout = (timeout if timeout && timeout > 0)
25
33
  end
26
34
 
27
35
  def read(nbytes)
28
36
  result = @buffer.slice!(0, nbytes)
29
37
 
30
- while result.bytesize < nbytes
31
- result << _read_from_socket(nbytes - result.bytesize)
32
- end
38
+ result << _read_from_socket(nbytes - result.bytesize) while result.bytesize < nbytes
33
39
 
34
40
  result
35
41
  end
36
42
 
37
43
  def gets
38
- crlf = nil
39
-
40
- while (crlf = @buffer.index(CRLF)) == nil
41
- @buffer << _read_from_socket(1024)
44
+ while (crlf = @buffer.index(CRLF)).nil?
45
+ @buffer << _read_from_socket(16_384)
42
46
  end
43
47
 
44
48
  @buffer.slice!(0, crlf + CRLF.bytesize)
@@ -47,18 +51,57 @@ class Redis
47
51
  def _read_from_socket(nbytes)
48
52
  begin
49
53
  read_nonblock(nbytes)
50
-
51
- rescue Errno::EWOULDBLOCK, Errno::EAGAIN
54
+ rescue IO::WaitReadable
52
55
  if IO.select([self], nil, nil, @timeout)
53
56
  retry
54
57
  else
55
58
  raise Redis::TimeoutError
56
59
  end
60
+ rescue IO::WaitWritable
61
+ if IO.select(nil, [self], nil, @timeout)
62
+ retry
63
+ else
64
+ raise Redis::TimeoutError
65
+ end
57
66
  end
67
+ rescue EOFError
68
+ raise Errno::ECONNRESET
69
+ end
58
70
 
71
+ def _write_to_socket(data)
72
+ begin
73
+ write_nonblock(data)
74
+ rescue IO::WaitWritable
75
+ if IO.select(nil, [self], nil, @write_timeout)
76
+ retry
77
+ else
78
+ raise Redis::TimeoutError
79
+ end
80
+ rescue IO::WaitReadable
81
+ if IO.select([self], nil, nil, @write_timeout)
82
+ retry
83
+ else
84
+ raise Redis::TimeoutError
85
+ end
86
+ end
59
87
  rescue EOFError
60
88
  raise Errno::ECONNRESET
61
89
  end
90
+
91
+ def write(data)
92
+ return super(data) unless @write_timeout
93
+
94
+ length = data.bytesize
95
+ total_count = 0
96
+ loop do
97
+ count = _write_to_socket(data)
98
+
99
+ total_count += count
100
+ return total_count if total_count >= length
101
+
102
+ data = data.byteslice(count..-1)
103
+ end
104
+ end
62
105
  end
63
106
 
64
107
  if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
@@ -66,7 +109,6 @@ class Redis
66
109
  require "timeout"
67
110
 
68
111
  class TCPSocket < ::TCPSocket
69
-
70
112
  include SocketMixin
71
113
 
72
114
  def self.connect(host, port, timeout)
@@ -79,42 +121,46 @@ class Redis
79
121
  end
80
122
  end
81
123
 
82
- class UNIXSocket < ::UNIXSocket
124
+ if defined?(::UNIXSocket)
83
125
 
84
- # This class doesn't include the mixin, because JRuby raises
85
- # Errno::EAGAIN on #read_nonblock even when IO.select says it is
86
- # readable. This behavior shows in 1.6.6 in both 1.8 and 1.9 mode.
87
- # Therefore, fall back on the default Unix socket implementation,
88
- # without timeouts.
126
+ class UNIXSocket < ::UNIXSocket
127
+ include SocketMixin
89
128
 
90
- def self.connect(path, timeout)
91
- Timeout.timeout(timeout) do
92
- sock = new(path)
93
- sock
129
+ def self.connect(path, timeout)
130
+ Timeout.timeout(timeout) do
131
+ sock = new(path)
132
+ sock
133
+ end
134
+ rescue Timeout::Error
135
+ raise TimeoutError
136
+ end
137
+
138
+ # JRuby raises Errno::EAGAIN on #read_nonblock even when IO.select
139
+ # says it is readable (1.6.6, in both 1.8 and 1.9 mode).
140
+ # Use the blocking #readpartial method instead.
141
+
142
+ def _read_from_socket(nbytes)
143
+ readpartial(nbytes)
144
+ rescue EOFError
145
+ raise Errno::ECONNRESET
94
146
  end
95
- rescue Timeout::Error
96
- raise TimeoutError
97
147
  end
148
+
98
149
  end
99
150
 
100
151
  else
101
152
 
102
153
  class TCPSocket < ::Socket
103
-
104
154
  include SocketMixin
105
155
 
106
- def self.connect(host, port, timeout)
107
- # Limit lookup to IPv4, as Redis doesn't yet do IPv6...
108
- addr = ::Socket.getaddrinfo(host, nil, Socket::AF_INET)
109
- sock = new(::Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
110
- sockaddr = ::Socket.pack_sockaddr_in(port, addr[0][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])
111
159
 
112
160
  begin
113
161
  sock.connect_nonblock(sockaddr)
114
162
  rescue Errno::EINPROGRESS
115
- if IO.select(nil, [sock], nil, timeout) == nil
116
- raise TimeoutError
117
- end
163
+ raise TimeoutError if IO.select(nil, [sock], nil, timeout).nil?
118
164
 
119
165
  begin
120
166
  sock.connect_nonblock(sockaddr)
@@ -124,12 +170,43 @@ class Redis
124
170
 
125
171
  sock
126
172
  end
173
+
174
+ def self.connect(host, port, timeout)
175
+ # Don't pass AI_ADDRCONFIG as flag to getaddrinfo(3)
176
+ #
177
+ # From the man page for getaddrinfo(3):
178
+ #
179
+ # If hints.ai_flags includes the AI_ADDRCONFIG flag, then IPv4
180
+ # addresses are returned in the list pointed to by res only if the
181
+ # local system has at least one IPv4 address configured, and IPv6
182
+ # addresses are returned only if the local system has at least one
183
+ # IPv6 address configured. The loopback address is not considered
184
+ # for this case as valid as a configured address.
185
+ #
186
+ # We do want the IPv6 loopback address to be returned if applicable,
187
+ # even if it is the only configured IPv6 address on the machine.
188
+ # Also see: https://github.com/redis/redis-rb/pull/394.
189
+ addrinfo = ::Socket.getaddrinfo(host, nil, Socket::AF_UNSPEC, Socket::SOCK_STREAM)
190
+
191
+ # From the man page for getaddrinfo(3):
192
+ #
193
+ # Normally, the application should try using the addresses in the
194
+ # order in which they are returned. The sorting function used
195
+ # within getaddrinfo() is defined in RFC 3484 [...].
196
+ #
197
+ addrinfo.each_with_index do |ai, i|
198
+ begin
199
+ return connect_addrinfo(ai, port, timeout)
200
+ rescue SystemCallError
201
+ # Raise if this was our last attempt.
202
+ raise if addrinfo.length == i + 1
203
+ end
204
+ end
205
+ end
127
206
  end
128
207
 
129
208
  class UNIXSocket < ::Socket
130
-
131
- # This class doesn't include the mixin to keep its behavior in sync
132
- # with the JRuby implementation.
209
+ include SocketMixin
133
210
 
134
211
  def self.connect(path, timeout)
135
212
  sock = new(::Socket::AF_UNIX, Socket::SOCK_STREAM, 0)
@@ -138,9 +215,7 @@ class Redis
138
215
  begin
139
216
  sock.connect_nonblock(sockaddr)
140
217
  rescue Errno::EINPROGRESS
141
- if IO.select(nil, [sock], nil, timeout) == nil
142
- raise TimeoutError
143
- end
218
+ raise TimeoutError if IO.select(nil, [sock], nil, timeout).nil?
144
219
 
145
220
  begin
146
221
  sock.connect_nonblock(sockaddr)
@@ -154,33 +229,125 @@ class Redis
154
229
 
155
230
  end
156
231
 
232
+ if defined?(OpenSSL)
233
+ class SSLSocket < ::OpenSSL::SSL::SSLSocket
234
+ include SocketMixin
235
+
236
+ def self.connect(host, port, timeout, ssl_params)
237
+ # Note: this is using Redis::Connection::TCPSocket
238
+ tcp_sock = TCPSocket.connect(host, port, timeout)
239
+
240
+ ctx = OpenSSL::SSL::SSLContext.new
241
+
242
+ # The provided parameters are merged into OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
243
+ ctx.set_params(ssl_params || {})
244
+
245
+ ssl_sock = new(tcp_sock, ctx)
246
+ ssl_sock.hostname = host
247
+
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
+ )
274
+ ssl_sock.post_connection_check(host)
275
+ end
276
+
277
+ ssl_sock
278
+ end
279
+ end
280
+ end
281
+
157
282
  class Ruby
158
283
  include Redis::Connection::CommandHelper
159
284
 
160
- MINUS = "-".freeze
161
- PLUS = "+".freeze
162
- COLON = ":".freeze
163
- DOLLAR = "$".freeze
164
- ASTERISK = "*".freeze
285
+ MINUS = "-"
286
+ PLUS = "+"
287
+ COLON = ":"
288
+ DOLLAR = "$"
289
+ ASTERISK = "*"
165
290
 
166
291
  def self.connect(config)
167
292
  if config[:scheme] == "unix"
168
- sock = UNIXSocket.connect(config[:path], config[:timeout])
293
+ raise ArgumentError, "SSL incompatible with unix sockets" if config[:ssl]
294
+
295
+ sock = UNIXSocket.connect(config[:path], config[:connect_timeout])
296
+ elsif config[:scheme] == "rediss" || config[:ssl]
297
+ sock = SSLSocket.connect(config[:host], config[:port], config[:connect_timeout], config[:ssl_params])
169
298
  else
170
- sock = TCPSocket.connect(config[:host], config[:port], config[:timeout])
299
+ sock = TCPSocket.connect(config[:host], config[:port], config[:connect_timeout])
171
300
  end
172
301
 
173
302
  instance = new(sock)
174
- instance.timeout = config[:timeout]
303
+ instance.timeout = config[:read_timeout]
304
+ instance.write_timeout = config[:write_timeout]
305
+ instance.set_tcp_keepalive config[:tcp_keepalive]
306
+ instance.set_tcp_nodelay if sock.is_a? TCPSocket
175
307
  instance
176
308
  end
177
309
 
310
+ if %i[SOL_SOCKET SO_KEEPALIVE SOL_TCP TCP_KEEPIDLE TCP_KEEPINTVL TCP_KEEPCNT].all? { |c| Socket.const_defined? c }
311
+ def set_tcp_keepalive(keepalive)
312
+ return unless keepalive.is_a?(Hash)
313
+
314
+ @sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
315
+ @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, keepalive[:time])
316
+ @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, keepalive[:intvl])
317
+ @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, keepalive[:probes])
318
+ end
319
+
320
+ def get_tcp_keepalive
321
+ {
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
325
+ }
326
+ end
327
+ else
328
+ def set_tcp_keepalive(keepalive); end
329
+
330
+ def get_tcp_keepalive
331
+ {
332
+ }
333
+ end
334
+ end
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
+
178
345
  def initialize(sock)
179
346
  @sock = sock
180
347
  end
181
348
 
182
349
  def connected?
183
- !! @sock
350
+ !!@sock
184
351
  end
185
352
 
186
353
  def disconnect
@@ -191,9 +358,11 @@ class Redis
191
358
  end
192
359
 
193
360
  def timeout=(timeout)
194
- if @sock.respond_to?(:timeout=)
195
- @sock.timeout = timeout
196
- end
361
+ @sock.timeout = timeout if @sock.respond_to?(:timeout=)
362
+ end
363
+
364
+ def write_timeout=(timeout)
365
+ @sock.write_timeout = timeout
197
366
  end
198
367
 
199
368
  def write(command)
@@ -204,7 +373,6 @@ class Redis
204
373
  line = @sock.gets
205
374
  reply_type = line.slice!(0, 1)
206
375
  format_reply(reply_type, line)
207
-
208
376
  rescue Errno::EAGAIN
209
377
  raise TimeoutError
210
378
  end
@@ -216,7 +384,7 @@ class Redis
216
384
  when COLON then format_integer_reply(line)
217
385
  when DOLLAR then format_bulk_reply(line)
218
386
  when ASTERISK then format_multi_bulk_reply(line)
219
- else raise ProtocolError.new(reply_type)
387
+ else raise ProtocolError, reply_type
220
388
  end
221
389
  end
222
390
 
@@ -235,6 +403,7 @@ class Redis
235
403
  def format_bulk_reply(line)
236
404
  bulklen = line.to_i
237
405
  return if bulklen == -1
406
+
238
407
  reply = encode(@sock.read(bulklen))
239
408
  @sock.read(2) # Discard CRLF.
240
409
  reply