redis 3.0.0 → 4.2.2

Sign up to get free protection for your applications and to get access to all the features.
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